diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index 21f7c42e0..048986210 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -393,12 +393,23 @@ pub(crate) fn inspect_local_project_directory( is_godot_project: godot_project_root.is_some(), godot_project_root, project_name: game_creator_project_name(root), + modified_at: project_directory_modified_at(root), manifest_error: game_creator_project_manifest_error(root), recent_run_status: recent_run_trace.as_ref().map(|trace| trace.status.clone()), recent_run_stop_reason: recent_run_trace.map(|trace| trace.stop_reason), }) } +fn project_directory_modified_at(root: &Path) -> Option { + root.metadata() + .ok()? + .modified() + .ok()? + .duration_since(std::time::UNIX_EPOCH) + .ok() + .map(|duration| duration.as_millis().min(u128::from(u64::MAX)) as u64) +} + pub(crate) fn is_game_creator_project_directory(root: &Path) -> bool { if !root.is_dir() { return false; diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index b139eed49..e0dbd9757 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -179,6 +179,7 @@ struct LocalProjectDirectoryStatus { is_godot_project: bool, godot_project_root: Option, project_name: Option, + modified_at: Option, manifest_error: Option, recent_run_status: Option, recent_run_stop_reason: Option, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs index 671ac80ed..1167a8440 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs @@ -1397,6 +1397,7 @@ fn project_directory_status_distinguishes_missing_file_and_dir() { is_godot_project: false, godot_project_root: None, project_name: None, + modified_at: None, manifest_error: None, recent_run_status: None, recent_run_stop_reason: None, @@ -1412,6 +1413,7 @@ fn project_directory_status_distinguishes_missing_file_and_dir() { assert!(!file_status.is_godot_project); assert_eq!(file_status.godot_project_root, None); assert_eq!(file_status.project_name, None); + assert!(file_status.modified_at.is_some()); assert_eq!(file_status.manifest_error, None); assert_eq!(file_status.recent_run_status, None); fs::remove_file(&root).expect("remove file"); diff --git a/apps/ai-game-creator-shell/src/app/types.ts b/apps/ai-game-creator-shell/src/app/types.ts index cd3b5c62a..d02b130b7 100644 --- a/apps/ai-game-creator-shell/src/app/types.ts +++ b/apps/ai-game-creator-shell/src/app/types.ts @@ -96,6 +96,7 @@ export interface LocalProjectDirectoryStatus { isGodotProject: boolean; godotProjectRoot: string | null; projectName: string | null; + modifiedAt?: number | null; manifestError?: string | null; recentRunStatus: string | null; recentRunStopReason: string | null; diff --git a/apps/ai-game-creator-shell/src/features/app-shell/model.ts b/apps/ai-game-creator-shell/src/features/app-shell/model.ts index 74b98a650..c43b26af6 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/model.ts +++ b/apps/ai-game-creator-shell/src/features/app-shell/model.ts @@ -61,6 +61,7 @@ export type RecentProjectRow = { status: string; projectKind: 'web' | 'godot' | 'unknown'; godotProjectRoot: string | null; + modifiedAt: number | null; recentRunStatus: string | null; recentRunStopReason: string | null; canReveal: boolean; @@ -262,6 +263,7 @@ export function buildRecentProjectRows( ? 'web' : 'unknown', godotProjectRoot: directoryStatus?.godotProjectRoot ?? null, + modifiedAt: directoryStatus?.modifiedAt ?? null, recentRunStatus: directoryStatus?.recentRunStatus ?? null, recentRunStopReason: directoryStatus?.recentRunStopReason ?? null, canReveal, diff --git a/apps/ai-game-creator-shell/src/view/home/index.tsx b/apps/ai-game-creator-shell/src/view/home/index.tsx index 1d4960f95..5ab475324 100644 --- a/apps/ai-game-creator-shell/src/view/home/index.tsx +++ b/apps/ai-game-creator-shell/src/view/home/index.tsx @@ -5,8 +5,8 @@ import { Gamepad2, Image, type LucideIcon, - Plus, Sparkles, + ArrowUp, } from 'lucide-react'; import type { FormEvent } from 'react'; import { useRef, useState } from 'react'; @@ -33,7 +33,6 @@ type HomeCreationTypeItem = { creationType: HomeCreationType; label: string; placeholder: string; - emptyPrompt: string; icon: LucideIcon; }; @@ -42,21 +41,18 @@ const HOME_CREATION_TYPE_ITEMS: readonly HomeCreationTypeItem[] = [ creationType: 'game', label: '做游戏', placeholder: '今天想把什么灵感做成游戏', - emptyPrompt: '请输入游戏灵感或上传参考素材', icon: Gamepad2, }, { creationType: 'art', label: '做素材', placeholder: '今天想做什么样的美术素材', - emptyPrompt: '请输入素材需求或上传参考图', icon: Image, }, { creationType: 'doc', label: '做方案', placeholder: '今天有什么设计需要帮你整理', - emptyPrompt: '请输入方案需求或上传资料', icon: FileText, }, ]; @@ -65,9 +61,30 @@ export type HomeProjectRow = { path: string; name: string; status: string; + projectKind: 'web' | 'godot' | 'unknown'; + modifiedAt: number | null; canOpen: boolean; }; +function formatProjectUpdatedAt(modifiedAt: number | null) { + if (!modifiedAt || !Number.isFinite(modifiedAt) || modifiedAt <= 0) { + return '未记录'; + } + const date = new Date(modifiedAt); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}/${month}/${day}`; +} + +function projectTypeLabel(projectKind: HomeProjectRow['projectKind']) { + return projectKind === 'godot' + ? 'Godot 游戏' + : projectKind === 'web' + ? 'Web 游戏' + : '本地项目'; +} + type HomeViewProps = { hasPromo: boolean; status: string; @@ -113,7 +130,6 @@ export default function HomeView({ const referencedAttachments = richTextToAttachments(homeRichText); const prompt = richTextToPrompt(homeRichText); if (!prompt && referencedAttachments.length === 0) { - onStatusChange(activeCreationType.emptyPrompt); return; } homeCreationBusyRef.current = true; @@ -143,26 +159,30 @@ export default function HomeView({ return (
-
- - logo +
+ + logo
-

+

陶泥儿

-

- {activeCreationType.placeholder} +

+ 你的游戏创作管家

@@ -171,10 +191,10 @@ export default function HomeView({ const isActive = item.creationType === homeCreationType; return (
+ {status ? ( + + {status} + + ) : null}
@@ -226,9 +248,6 @@ export default function HomeView({

最近项目

-

- 选择一个项目继续创作 -

))} @@ -296,29 +325,23 @@ export default function HomeView({
-
- -

- 灵感推荐 -

-
-

- 暂无灵感 -

-
+ +

+ 灵感推荐 +

+
-
- 暂无灵感 -
+
); diff --git a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts index c9e169960..b565179be 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts @@ -26,12 +26,12 @@ import { } from './harness'; export function registerClientHomeTests() { - it('keeps the empty inspiration section without reading the main-site showcase', async () => { + it('keeps the empty inspiration module without requesting the retired feed', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch'); renderLauncherAt('/?launcher'); - const inspirationSection = screen.getByLabelText('灵感推荐'); - expect(within(inspirationSection).getAllByText('暂无灵感')).toHaveLength(2); + expect(screen.getByLabelText('灵感推荐')).not.toBeNull(); + expect(screen.queryByText('暂无灵感')).toBeNull(); await act(async () => { await Promise.resolve(); }); @@ -1315,19 +1315,21 @@ export function registerHomeProjectCreationTests() { expect(gameType.getAttribute('aria-pressed')).toBe('true'); expect(artType.getAttribute('aria-pressed')).toBe('false'); expect(documentType.getAttribute('aria-pressed')).toBe('false'); - expect(screen.getAllByText('今天想把什么灵感做成游戏')).toHaveLength(2); + expect(screen.getByText('你的游戏创作管家')).not.toBeNull(); + expect(screen.getAllByText('今天想把什么灵感做成游戏')).toHaveLength(1); fireEvent.click(documentType); expect(documentType.getAttribute('aria-pressed')).toBe('true'); - expect(screen.getAllByText('今天有什么设计需要帮你整理')).toHaveLength(2); + expect(screen.getByText('你的游戏创作管家')).not.toBeNull(); + expect(screen.getAllByText('今天有什么设计需要帮你整理')).toHaveLength(1); fireEvent.click(screen.getByRole('button', { name: '开启创作' })); - expect(await screen.findByText('请输入方案需求或上传资料')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'create_automatic_local_game_project', ); fireEvent.click(artType); expect(gameType.getAttribute('aria-pressed')).toBe('false'); expect(artType.getAttribute('aria-pressed')).toBe('true'); - expect(screen.getAllByText('今天想做什么样的美术素材')).toHaveLength(2); + expect(screen.getByText('你的游戏创作管家')).not.toBeNull(); + expect(screen.getAllByText('今天想做什么样的美术素材')).toHaveLength(1); const promptInput = screen.getByLabelText('创作想法'); nativeClipboardMock.text = '你好,今天多少号'; @@ -1494,9 +1496,7 @@ export function registerHomeProjectCreationTests() { }); expect((createButton as HTMLButtonElement).disabled).toBe(true); expect(screen.queryByLabelText('陶泥儿首页对话')).toBeNull(); - expect(screen.getByLabelText('最近项目').textContent).toContain( - '选择一个项目继续创作', - ); + expect(screen.getByLabelText('最近项目').textContent).toContain('最近项目'); expect(screen.getByLabelText('最近项目').textContent).not.toContain( '正在创建工作区', ); diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 2141a0b9d..bf0eb1038 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -662,7 +662,9 @@ game-project/ - Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单、`.agent/run.latest.json` / `.agent/runs/.json` 的 step、taskGraph、passPlans、lifecycleStatus,以及 `read_game_creator_agent_runtimes` 批量读取的 `.agent/runtime/agents/.json` 和最近任务派生;v1 不新增独立状态数据库,也不承诺完整后台 runner。 - App 启动先检查平台登录态;登录后进入同一个客户端首页,不再有面向用户的启动器 / 主窗口切换概念。首页按 `做游戏` / `做素材` / `做方案` 保存 `game` / `art` / `doc` 初始意图,输入状态按文字与附件 token 的顺序保存,附件以文件名 token 内嵌在输入框中而非堆叠在下方;提交时才将 token 转为 LLM 可读的 `{1st attachment}` 引用。普通 `Enter` 在输入法组合态结束后自动在系统“文档/Genarrative GameAgent”下原子分配唯一工作区、初始化本地项目、导入附件,并把首条需求直接投递给 active `project-supervisor` Session 的后台 Runtime,随后写入最近项目并切到项目开发页;`Shift+Enter` 保留换行。Windows 使用系统 Documents 路径,Linux 使用 XDG Documents,macOS 使用用户 Documents;前端不得显示或持久化 `/tmp` 作为默认项目路径,精确 `/tmp/genarrative-ai-game-draft` 只允许由测试显式注入为 fixture。普通与 game-chat release 都必须在 Vite 生成最新 `frontendDist` 后、Tauri 嵌入资源前扫描实际构建产物;命中该旧 Linux 默认路径或无法安全遍历产物时构建失败,禁止复用 gitignored 的旧 `dist`。原“开启创作”加号按钮保持手动选择目录:点击后弹出原生目录选择,目标目录存在且非空时必须二次确认,再调用 `init_local_game_project` 完成同一后续链路。项目页和首页通用“打开项目”没有手填默认路径;picker 未携带已有工作区时由操作系统决定初始位置。缺少 active Session 时先创建并激活;成功后清空首页草稿,取消或创建失败时在首页回显状态,首页响应式断点与应用外壳统一为 `760px`。两条流程都不调用 `generate_local_game_draft`、`generate_platform_art_asset`、一次性 `chat_with_game_creator_agent` 或 legacy 项目对话 append。 - 首页发送、项目组目录选择和本地文件选择必须使用 Tauri 非阻塞原生 picker,并把选择器绑定到当前 `client` 窗口;禁止在同步 command 中调用 `blocking_pick_folder` / `blocking_pick_file` 阻塞 WebView 事件循环。选择器打开期间保留首页草稿可编辑,取消后恢复“开启创作”按钮并回显“已取消”。 -- 当前尚未定义 GameAgent 独立的灵感数据源;首页保留“灵感推荐”区块并显示无数据状态,但不得展示或请求主站 `/creation` 的陶泥儿精选 `/api/editor/showcase/resources`。后续接入前必须先明确独立数据契约和交互验收。 +- 当前尚未定义 GameAgent 独立的灵感数据源;首页保留“灵感推荐”模块及空白内容容器,但不显示无数据文案,也不得展示或请求主站 `/creation` 的陶泥儿精选 `/api/editor/showcase/resources`。后续接入前必须先明确独立数据契约和交互验收。 +- 首页正式桌面版以约 `814px` 的居中内容栏组织品牌区、创作类型、输入框、最近项目与灵感推荐;品牌区和类型按钮在内容栏左缘对齐,输入框和下方模块仍保持整体居中。标题使用深色暖橙层级,固定副标题为“你的游戏创作管家”;选中的创作类型使用实心暖橙按钮,未选中项使用浅色描边,输入框使用更舒展的单行创作起始比例。 +- 最近项目固定展示至多三个横向信息卡:左侧为本地渐变封面占位,右侧只投影项目名称、项目类型、目录真实修改时间和已有最近运行状态。目录检查通过 `modifiedAt` 提供修改时间;没有封面资产时不得假装读取了项目封面,也不新增封面持久化字段。 - debug 构建启动后在用户 `client` 窗口之外额外打开 `developer` 窗口;该窗口用于开发者单独选择 Agent、管理对应 active/archived Session 并读取历史,用户消息和真实 Agent 回复只持久化到 `.agent/conversations/agents//` 下的规范 Session。普通用户窗口不得出现 `Agent 聊天` 导航、picker 或工具台入口。 - 首页最近项目只展示最近 3 个有效项目;项目组页在同一窗口使用紧凑桌面项目表格管理最近项目。顶部工具栏只放本地搜索、“打开项目”“新建项目”,不常驻路径输入框、目录显示按钮或独立 Godot 入口。两个项目动作都先打开绑定当前 `client` 的非阻塞原生目录选择器:“打开项目”读取已有 GameAgent 项目,或自动识别根目录 / 一层直接子目录中的唯一 Godot 工程并导入;普通未初始化目录提示改用“新建项目”。“新建项目”在用户选择工作区根后沿用非空目录确认,不自动重建无效历史路径。项目表格只投影名称、路径、GameAgent / Godot 类型、目录 / manifest / Runtime 状态;可打开行点击进入项目,显示目录和移除最近记录收进行尾更多菜单。搜索仅过滤当前行,不修改 storage;空状态不追加第三个目录选择入口。正式验收只覆盖 `1280×720` 最小横屏和 `1280×800` 默认窗口,列表内部滚动且无页面级溢出。 - 项目开发页保留左侧栏和顶部栏,顶部展示项目名、路径和最近 run 状态;中间只挂载 active `project-supervisor` Session 的正式对话面、Runtime 状态、确认/Needs input 和专业 Agent 协作只读状态,底部保留附件导入结果。真正的项目开发画布仍未落地;专业 Agent picker、完整计划和工具台继续留在开发入口。