import type { ComponentType } from 'react'; import type { AuthUser } from '../../../../../packages/shared/src/contracts/auth'; import type { GameCreationAppManifest, GameCreationAppPreviewState, } from '../../../../../packages/shared/src/contracts/gameCreationApp'; import type { ChatMessage, LauncherImportedAttachment, LocalProjectDirectoryStatus, } from '../../app/types'; import type { HomeCreationType } from '../../view/home'; import type { LauncherView } from '../../view/layout'; import type { ProjectAgentResultSummary, ProjectAgentRuntimeSummary, } from '../../view/project-development'; import type { ProjectManifestSnapshotMetadata } from '../../view/project-development/projectResourceLiveUpdateModel'; import { isAbsoluteProjectPath } from '../project-summary/projectSummary'; const RECENT_WORKSPACES_STORAGE_KEY = 'genarrative-ai-game-creator.recent-workspaces.v1'; const SUPERVISOR_CHAT_DRAFT_STORAGE_PREFIX = 'genarrative.supervisor-chat.draft'; export type WorkspaceLauncherProps = { currentUser: AuthUser; onLogout: () => void; initialView?: LauncherView; }; export type ProjectSupervisorComponentProps = { initialProjectPath?: string; initialProjectManifest?: GameCreationAppManifest; initialProjectKind?: 'web' | 'godot' | 'cocos'; initialSupervisorMessage?: string; initialCreationType?: HomeCreationType | null; initialAttachments?: LauncherImportedAttachment[]; orchestrationMode?: 'single-supervisor' | 'professional-dag'; projectSupervisorOnly?: boolean; planningStartMode?: boolean; /** * C7 当前游戏版本:由工作台壳持有,supervisor 里的 `@` 面板按它切「当前版本素材」。 * `null` 表示回退到 manifest 中最新的版本。 */ activeVersionId?: string | null; playRequest?: { projectPath: string; requestId: number; } | null; onPlayRequestHandled?: (requestId: number) => void; onManifestChange?: ( projectPath: string, manifest: GameCreationAppManifest, metadata?: ProjectManifestSnapshotMetadata, ) => void; onPreviewChange?: (preview: GameCreationAppPreviewState | null) => void; onAgentRuntimeSummariesChange?: ( summaries: ProjectAgentRuntimeSummary[], ) => void; onAgentResultsChange?: (results: ProjectAgentResultSummary[]) => void; onMakeGameFromApprovedGdd?: (projectPath: string) => Promise; onSwitchToGameRuntime?: (projectPath: string) => Promise; }; export type WorkspaceLauncherShellProps = WorkspaceLauncherProps & { ProjectSupervisor: ComponentType; }; export type RecentProjectRow = { path: string; name: string; status: string; projectKind: 'web' | 'cocos' | 'unity' | 'ue' | 'godot' | 'unknown'; cocosProjectRoot: string | null; godotProjectRoot: string | null; modifiedAt: number | null; recentRunStatus: string | null; recentRunStopReason: string | null; canReveal: boolean; canOpen: boolean; }; export type LauncherNotice = { title: string; message: string; }; function normalizeRecentWorkspaceList(values: unknown[]) { const recent: string[] = []; for (const value of values) { if (typeof value !== 'string') { continue; } const workspace = value.trim(); if ( !workspace || !isAbsoluteProjectPath(workspace) || recent.includes(workspace) ) { continue; } recent.push(workspace); if (recent.length >= 8) { break; } } return recent; } export function readRecentWorkspaces() { try { const raw = window.localStorage.getItem(RECENT_WORKSPACES_STORAGE_KEY); const parsed: unknown = raw ? JSON.parse(raw) : []; return Array.isArray(parsed) ? normalizeRecentWorkspaceList(parsed) : []; } catch { return []; } } export function writeRecentWorkspace(path: string) { const recent = normalizeRecentWorkspaceList([ path, ...readRecentWorkspaces(), ]); try { window.localStorage.setItem( RECENT_WORKSPACES_STORAGE_KEY, JSON.stringify(recent), ); } catch { // WebView storage can be unavailable in restricted test shells. } return recent; } export function removeRecentWorkspace(path: string) { const trimmedPath = path.trim(); const recent = readRecentWorkspaces().filter( (workspace) => workspace !== trimmedPath, ); try { if (recent.length > 0) { window.localStorage.setItem( RECENT_WORKSPACES_STORAGE_KEY, JSON.stringify(recent), ); } else { window.localStorage.removeItem(RECENT_WORKSPACES_STORAGE_KEY); } } catch { // WebView storage can be unavailable in restricted test shells. } return recent; } export function isTransientProjectOpenMessage( message: ChatMessage, projectPath: string, ) { return ( message.role === 'assistant' && message.text === `已设置本地项目:${projectPath}` ); } export function latestVisibleItems(items: T[], visibleCount: number) { if (items.length <= visibleCount) { return items; } return items.slice(items.length - visibleCount); } export function isDeveloperMode() { if (!import.meta.env.DEV) { return false; } const params = new URLSearchParams(window.location.search); return params.has('dev') || window.location.hash === '#dev'; } export function readInitialProjectPath() { const params = new URLSearchParams(window.location.search); return params.get('projectPath') ?? ''; } function supervisorChatDraftStorageKey(projectPath: string) { return `${SUPERVISOR_CHAT_DRAFT_STORAGE_PREFIX}:${projectPath}`; } export function readSupervisorChatDraft(projectPath: string) { const normalizedProjectPath = projectPath.trim(); if (!normalizedProjectPath) { return ''; } try { return ( window.sessionStorage.getItem( supervisorChatDraftStorageKey(normalizedProjectPath), ) ?? '' ); } catch { return ''; } } export function persistSupervisorChatDraft(projectPath: string, draft: string) { const normalizedProjectPath = projectPath.trim(); if (!normalizedProjectPath) { return; } try { const storageKey = supervisorChatDraftStorageKey(normalizedProjectPath); if (draft) { window.sessionStorage.setItem(storageKey, draft); } else { window.sessionStorage.removeItem(storageKey); } } catch { // A disabled session store must not block the development chat surface. } } export function buildRecentProjectRows( recentWorkspaces: string[], recentWorkspaceStatuses: Record< string, LocalProjectDirectoryStatus | null | undefined >, recentWorkspaceRefreshing: boolean, ): RecentProjectRow[] { return recentWorkspaces.map((workspace) => { const directoryStatus = recentWorkspaceStatuses[workspace]; const isPendingStatus = directoryStatus === undefined; const projectName = directoryStatus?.projectName || workspace.split(/[\\/]/).filter(Boolean).pop() || workspace; const status = recentWorkspaceRefreshing ? '检查中' : isPendingStatus ? '检查中' : directoryStatus === null ? '检查失败' : directoryStatus?.exists === false ? '未找到' : directoryStatus?.isDirectory === false ? '不是文件夹' : directoryStatus?.manifestError ? '无法读取' : (directoryStatus?.isGodotProject === true || directoryStatus?.isCocosProject === true) && directoryStatus?.isGameCreatorProject === false ? '可导入' : directoryStatus?.isGameCreatorProject === false ? '未初始化' : directoryStatus?.recentRunStatus ? formatRecentProjectRunStatus( directoryStatus.recentRunStatus, directoryStatus.recentRunStopReason, ) : directoryStatus?.isGodotProject ? '可打开' : '本地项目'; const canReveal = !recentWorkspaceRefreshing && Boolean(directoryStatus) && directoryStatus?.exists !== false && directoryStatus?.isDirectory !== false; return { path: workspace, name: projectName, status, projectKind: directoryStatus?.isCocosProject ? 'cocos' : directoryStatus?.isGodotProject ? 'godot' : directoryStatus?.isGameCreatorProject ? 'web' : 'unknown', godotProjectRoot: directoryStatus?.godotProjectRoot ?? null, cocosProjectRoot: directoryStatus?.cocosProjectRoot ?? null, modifiedAt: directoryStatus?.modifiedAt ?? null, recentRunStatus: directoryStatus?.recentRunStatus ?? null, recentRunStopReason: directoryStatus?.recentRunStopReason ?? null, canReveal, canOpen: !recentWorkspaceRefreshing && Boolean(directoryStatus) && directoryStatus?.exists !== false && directoryStatus?.isDirectory !== false && !directoryStatus?.manifestError && (directoryStatus?.isGameCreatorProject !== false || directoryStatus?.isGodotProject === true || directoryStatus?.isCocosProject === true), }; }); } function formatRecentProjectRunStatus( status: string, stopReason: string | null, ) { const statusLabel = { completed: '已完成', done: '已完成', failed: '运行失败', running: '运行中', pending: '等待运行', cancelled: '已取消', }[status] ?? status; const stopReasonLabel = stopReason ? ({ 'preview-running': '预览运行中', completed: '已完成', failed: '运行失败', cancelled: '已取消', }[stopReason] ?? stopReason) : null; return stopReasonLabel && stopReasonLabel !== statusLabel ? `${statusLabel} · ${stopReasonLabel}` : statusLabel; }