拓宽AI游戏创作主窗口快捷入口

在主窗口头部显示当前预览状态

补充黑板、画板资产导入和资产读取草稿快捷入口

为Trace产物提供读取命令草稿

同步AI游戏创作App文档和共享决策记录
This commit is contained in:
AIGameCreator App
2026-07-03 01:43:35 +08:00
parent 998e33ca3d
commit e3fd0d483d
4 changed files with 93 additions and 18 deletions
+51 -13
View File
@@ -9323,6 +9323,7 @@ export function App() {
<small>{localProject.projectPath}</small>
) : null}
<small>run: {agentRunStatus}</small>
<small>preview: {previewStatus}</small>
<small>{workspaceStatus}</small>
</div>
<div className="chat-header-actions">
@@ -9416,6 +9417,13 @@ export function App() {
>
</button>
<button
type="button"
disabled={!localProject}
onClick={() => prepareChatCommandDraft('/import-canvas-asset ')}
>
</button>
<button
type="button"
disabled={!localProject}
@@ -9472,6 +9480,13 @@ export function App() {
>
</button>
<button
type="button"
disabled={!localProject}
onClick={() => prepareChatCommandDraft('/remember blackboard ')}
>
</button>
<button
type="button"
disabled={!localProject}
@@ -9578,19 +9593,31 @@ export function App() {
{visibleMainProjectAssets.length > 0 ? (
<div className="file-list" aria-label="最近项目资产">
{visibleMainProjectAssets.map((asset) => (
<button
key={`${asset.id}:${asset.localPath}`}
type="button"
aria-label={asset.localPath}
onClick={() =>
void executeProjectFileReadChat(asset.localPath)
}
>
<span>{asset.localPath}</span>
<small>
{`${asset.kind} · ${asset.mediaType} · ${asset.source.kind}`}
</small>
</button>
<Fragment key={`${asset.id}:${asset.localPath}`}>
<button
type="button"
aria-label={asset.localPath}
onClick={() =>
void executeProjectFileReadChat(asset.localPath)
}
>
<span>{asset.localPath}</span>
<small>
{`${asset.kind} · ${asset.mediaType} · ${asset.source.kind}`}
</small>
</button>
<button
type="button"
aria-label={`填入读取 ${asset.localPath}`}
onClick={() =>
prepareChatCommandDraft(
projectFileActionDrafts(asset.localPath).readCommand,
)
}
>
</button>
</Fragment>
))}
</div>
) : null}
@@ -10129,6 +10156,17 @@ export function App() {
{agentRunTrace.artifacts.slice(0, 8).map((artifact) => (
<small key={artifact.path}>
{`${artifact.path} · ${artifact.sizeBytes}B · ${artifact.checksum}`}
{isSafeProjectRelativePath(artifact.path) ? (
<button
type="button"
aria-label={`填入读取产物 ${artifact.path}`}
onClick={() =>
prepareChatCommandDraft(`/read ${artifact.path}`)
}
>
</button>
) : null}
</small>
))}
{agentRunTrace.artifacts.length > 8 ? (
@@ -224,6 +224,13 @@ describe('AI 游戏创作 App 界面边界', () => {
(screen.getByRole('button', { name: '打开画板' }) as HTMLButtonElement)
.disabled,
).toBe(false);
expect(
(
screen.getByRole('button', {
name: '导入画板资产',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '任务' }) as HTMLButtonElement)
.disabled,
@@ -252,6 +259,10 @@ describe('AI 游戏创作 App 界面边界', () => {
(screen.getByRole('button', { name: '黑板' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '记到黑板' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '快照' }) as HTMLButtonElement)
.disabled,
@@ -1841,6 +1852,7 @@ describe('AI 游戏创作 App 界面边界', () => {
await screen.findByText('已打开:/tmp/authorized-game');
expect(screen.getByText('未命名游戏原型')).not.toBeNull();
expect(screen.getByText('/tmp/authorized-game')).not.toBeNull();
expect(screen.getByText('preview: 未启动')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '能力' }));
expect(await screen.findByText(/Native Runtime 能力/)).not.toBeNull();
@@ -1876,6 +1888,9 @@ describe('AI 游戏创作 App 界面边界', () => {
fireEvent.click(screen.getByRole('button', { name: '生成美术' }));
expect(composerInput).toHaveProperty('value', '/generate-art ');
await waitFor(() => expect(document.activeElement).toBe(composerInput));
fireEvent.click(screen.getByRole('button', { name: '导入画板资产' }));
expect(composerInput).toHaveProperty('value', '/import-canvas-asset ');
await waitFor(() => expect(document.activeElement).toBe(composerInput));
fireEvent.click(
within(screen.getByLabelText('最近项目资产')).getByRole('button', {
name: 'assets/uploads/hero.txt',
@@ -1884,6 +1899,15 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
await screen.findByText(/文件:assets\/uploads\/hero\.txt/),
).not.toBeNull();
fireEvent.click(
within(screen.getByLabelText('最近项目资产')).getByRole('button', {
name: '填入读取 assets/uploads/hero.txt',
}),
);
expect(composerInput).toHaveProperty(
'value',
'/read assets/uploads/hero.txt',
);
fireEvent.click(screen.getByRole('button', { name: '任务' }));
expect(await screen.findByText(/任务拆分:/)).not.toBeNull();
@@ -1936,6 +1960,8 @@ describe('AI 游戏创作 App 界面边界', () => {
fireEvent.click(screen.getByRole('button', { name: '黑板' }));
expect(await screen.findByText(/黑板记忆:/)).not.toBeNull();
expect(screen.getByText(/跨 agent 共享约束/)).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '记到黑板' }));
expect(composerInput).toHaveProperty('value', '/remember blackboard ');
fireEvent.click(screen.getByRole('button', { name: '快照' }));
expect(await screen.findByText('project.checkpoint')).not.toBeNull();
@@ -2008,9 +2034,11 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
await screen.findByText('预览运行中:http://127.0.0.1:3210/'),
).not.toBeNull();
expect(screen.getByText('preview: 运行中:127.0.0.1:3210')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '停止预览' }));
expect(await screen.findByText('预览已停止。')).not.toBeNull();
expect(screen.getByText('preview: 已停止')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('run_limited_local_command', {
projectPath: '/tmp/authorized-game',
commandId: 'game.static_smoke',
@@ -2215,6 +2243,15 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(screen.getByLabelText('Agent artifacts').textContent).toContain(
'还有 1 个产物',
);
fireEvent.click(
within(screen.getByLabelText('Agent artifacts')).getByRole('button', {
name: '填入读取产物 exports/artifact-1.json',
}),
);
expect(screen.getByLabelText('创作想法')).toHaveProperty(
'value',
'/read exports/artifact-1.json',
);
fireEvent.click(screen.getByRole('button', { name: '刷新状态' }));
expect(
@@ -21,7 +21,7 @@
- 背景:AI 游戏创作 App 已有 Godcoder 式本地工程护栏、项目黑板、角色私有记忆、manifest 和 run trace;新增结构化对话记录、agent 状态列表和单 agent 对话入口时,需要避免引入平行状态源或提前承诺后台 runner 能力。
- 决策:v1 结构化对话记录统一使用本地 `.agent/conversations/` append-only JSONL。普通聊天写 `.agent/conversations/project.jsonl`;从 agent 状态列表进入单个 agent 后,用户消息、agent 回复、工具建议和错误只写对应 `.agent/conversations/agents/<agentId>.jsonl`。Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单和 `.agent/run.latest.json` / `.agent/runs/<runId>.json` 的 step、taskGraph、passPlans、lifecycleStatus 派生,并把 `taskGraph.tasks` 的任务状态与 active / carry-over / ready 编排标记显示在主窗口和单 agent 对话入口中;单 agent 最近证据里的安全相对输入 / 输出路径只填入 `/read <path>` 草稿,仍由用户发送并走既有 `file.read` / `agent.trace_read` 权限流。不新增独立状态数据库。项目黑板和角色私有记忆继续只保存稳定摘要,不承载原始对话流水。
- 补充:App 启动先进入独立启动器窗口;用户选择工作区后,Tauri 关闭启动器并打开主窗口,主窗口读取该工作区的主 conversation、manifest 和 run trace。启动器“新建项目”先调用 `init_local_game_project` 初始化成功,默认项目名取目标文件夹名,再写最近项目并打开主窗口;遇到非空目录时先弹确认,取消或初始化失败则不打开主窗口、不写最近项目。最近工作区只保存在本机 WebView storage,可单项移除或清空,不进入项目文件或共享记忆;已初始化项目优先显示 manifest 项目名并保留路径副信息,`.agent/run.latest.json` 可读时显示最近 run 状态。最近项目路径缺失、不是目录、缺少可读 `.agent/manifest.json` 或检查失败时禁用打开,刷新只重新执行只读检查;“显示”只用系统文件管理器打开已确认存在的本地目录,未初始化但存在的目录也可显示,避免把历史路径误当新项目重建。
- 补充:主窗口“显示目录”复用同一只读目录打开能力,只打开当前本地项目目录,不初始化项目、不写项目文件、不切换工作区;主窗口头部只读显示 manifest 项目名、项目路径最近 `.agent/run.latest.json` 的 run 状态摘要,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。最近项目资产入口只读展示 localPath、kind、mediaType 和 source.kind,点击仍走原 `file.read` 权限流。
- 补充:主窗口“显示目录”复用同一只读目录打开能力,只打开当前本地项目目录,不初始化项目、不写项目文件、不切换工作区;主窗口头部只读显示 manifest 项目名、项目路径最近 `.agent/run.latest.json` 的 run 状态摘要和当前预览状态,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。最近项目资产入口只读展示 localPath、kind、mediaType 和 source.kind,点击仍走原 `file.read` 权限流;旁边的“读取命令”只填入 `/read <path>` 草稿,不直接读取文件或绕过权限。主窗口“记到黑板”同样只填入 `/remember blackboard ` 草稿,项目黑板写入仍走聊天确认
- 补充:启动器和主窗口共用同一个运行时配置弹窗,配置只读写 Tauri 应用配置目录中的 `game-creator.config.json`,不写入项目文件或对话历史。
- 补充:启动器“打开”只进入已初始化且 `.agent/manifest.json` 可读的 AI 游戏项目;路径不存在、不是文件夹或只是普通文件夹时不打开主窗口、不创建目录,用户需要创建或初始化时走“新建项目”。
- 影响范围:`apps/ai-game-creator-shell` 的主窗口 agent 状态列表、单 agent 对话入口、本地项目文件结构、共享契约和 AI 游戏创作 App 实施计划。
@@ -100,7 +100,7 @@ game-project/
## v1 验收
- 用户能创建本地 Web 游戏项目。
- 用户侧看到当前工作区项目名 / 路径、聊天框、上传入口、Agent 状态列表和单 Agent 对话;最近项目资产入口显示本地路径、kind、mediaType 和来源类型,开发环境通过独立窗口查看任务拆分、专业组细节、产物、文件面板、嵌入预览和错误日志。
- 用户侧看到当前工作区项目名 / 路径、最近 run 与预览状态、聊天框、上传入口、Agent 状态列表和单 Agent 对话;最近项目资产入口显示本地路径、kind、mediaType 和来源类型,并可一键填入 `/read` 草稿,开发环境通过独立窗口查看任务拆分、专业组细节、产物、文件面板、嵌入预览和错误日志。
- 生成代码和资产进入用户本地项目目录。
- 本地 HTTP 预览能启动,并在外部浏览器展示可玩原型。
- 美术/音乐资产能从画板链路回流到本地项目。
@@ -166,13 +166,13 @@ game-project/
- 聊天区待确认命令必须先记录 `permission.pending`,用户点击后再记录 `permission.confirm``permission.cancel`;已授权本地项目内的权限事件同步追加到 `.agent/logs/command.log`,待确认卡片显示本地写入目标路径。
- 聊天输入 `/status` 会读取 `.agent/manifest.json` 并在聊天里汇总项目目录、任务状态、资产数量、预览状态和最近命令,不向普通用户暴露任务或文件面板。
- 聊天输入 `/files` 会通过 `file.list` 只读列出本地项目内的文件摘要;主窗口最近项目文件可一键读取,也可一键填入 `/read``/asset-register` 草稿,但资产登记仍必须走聊天确认;`/checkpoints` 复用 `file.list` / `file.read` 只读列出最近 checkpoint id、文件数、大小和可复制的 `/diff` / `/restore` 命令;普通用户仍不暴露文件读写面板。
- 聊天输入 `/assets` 会读取 `.agent/manifest.json` 并在聊天里列出本地项目资产路径、类型和来源,资产列表消息可一键填入首个资产的 `/read` 草稿;`/asset-register 路径 [kind] [mediaType]` 可确认后登记项目内已有资产;普通用户仍不暴露资产面板。
- 聊天输入 `/assets` 会读取 `.agent/manifest.json` 并在聊天里列出本地项目资产路径、类型和来源,资产列表消息和主窗口最近项目资产入口都可一键填入对应资产的 `/read` 草稿;`/asset-register 路径 [kind] [mediaType]` 可确认后登记项目内已有资产;普通用户仍不暴露资产面板。
- 聊天输入 `/read 本地相对路径` 会通过 `file.read` 只读返回项目内文本文件内容并在聊天中截断长文本;普通用户仍不暴露文件写入或删除能力。
- 聊天输入 `/tasks` 会读取 `.agent/manifest.json` 并在聊天里列出专业组、角色、任务状态、产物交接和下一步可执行任务;普通用户仍不暴露任务面板。
- 聊天输入 `/trace` 会通过 `agent.trace_read` 只读读取 `.agent/run.latest.json`,在聊天里汇总最近一次 loop 的轮次、stopReason、nextStep、active/carry-over 任务、repairRoutes、agent 建议命令和最近 stepactive、carry-over、repairRoutes 和 dependencyWaves 必须把 taskId 映射成专业组 / 角色 / 任务名,避免普通用户只能看到内部 id;普通用户仍不暴露开发 trace 面板。
- 主窗口可从 agent 状态列表进入单个 agent 对话;该入口只加载目标 agent 的 conversation JSONL,发送消息后追加到同一 agent conversation,不开启平行任务图、不 fork run,也不归档历史会话。
- v1 通过独立启动器窗口选择本地项目后再进入主窗口;主窗口只承载当前工作区的聊天、配置和 agent 状态,切换项目时关闭当前主窗口并回到启动器,主窗口按钮和 `/switch-project` 聊天命令都复用同一 Tauri 启动器入口,避免在主窗口内用遮罩面板混合多个工作区上下文。
- 主窗口可通过系统文件管理器显示当前项目目录,也可在聊天输入 `/open-project` 走同一只读打开动作;该操作只打开本地目录,不初始化项目、不写项目文件、不切换工作区。主窗口头部显示最近 `.agent/run.latest.json` 的 run 状态摘要,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。
- 主窗口可通过系统文件管理器显示当前项目目录,也可在聊天输入 `/open-project` 走同一只读打开动作;该操作只打开本地目录,不初始化项目、不写项目文件、不切换工作区。主窗口头部显示最近 `.agent/run.latest.json` 的 run 状态摘要和当前项目预览状态,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。
- 启动器和主窗口共用同一个运行时配置弹窗,读写 Tauri 应用配置目录中的 `game-creator.config.json`;API Key 仍不进入本地项目、trace、manifest 或聊天记录。
- 启动器支持手动输入绝对路径,也支持通过 Tauri 原生目录选择器回填项目路径;用户取消目录选择时不覆盖已经输入的路径。
- 启动器最近项目列表只来自本机 WebView storage,可移除单项或清空;这些操作不触碰项目目录,也不触发打开主窗口。启动器会只读检查最近项目路径,已初始化项目优先显示 `.agent/manifest.json` 里的项目名并保留路径副信息;如果 `.agent/run.latest.json` 可读,则显示最近 run 的 status / stopReason;缺失、非目录、未初始化或检查失败的条目禁用打开,用户可移除后重新选择。刷新最近项目只重新执行这组只读检查,不改项目目录或最近项目列表;刷新未完成时按钮显示“刷新中”并禁用,最近项目条目显示“检查中”且临时禁用打开 / 显示操作,避免重复触发并让用户知道检查仍在进行;“显示”只通过系统文件管理器打开已确认存在的本地目录,未初始化但存在的目录也可显示,缺失、非目录或检查失败时禁用,不初始化项目、不写最近项目、不打开主窗口。
@@ -181,7 +181,7 @@ game-project/
- 聊天输入 `/smoke` 会生成待确认的 `command.run_limited` 内置命令,当前只映射到白名单 `game.static_smoke`,不开放任意命令解析。
- 聊天输入 `/run` 会生成待确认的 `game.run_local` 内置命令,确认后复用白名单 `game.static_smoke` 运行当前 `game/index.html`,通过后启动只读本地 HTTP 预览并交给外部浏览器;该命令不开放任意 shell。
- 聊天输入 `/preview` 会生成待确认的 `preview.start` 内置命令,确认后启动只读本地 HTTP 预览并交给外部浏览器;`/open-preview` 在本地项目已初始化后会生成待确认的 `preview.open`,并且只打开当前已授权项目对应的 `127.0.0.1` 本地预览;`/preview-status` 只查询当前已授权项目对应的本地 HTTP 预览并写入 `preview.status` 命令日志;`/preview-stop` 只停止当前项目预览,不打开、展示或停止其它项目遗留的全局预览,不向普通用户暴露预览面板。
- 聊天输入 `/memory [short|long|blackboard]` 读取短期、长期或黑板记忆;`/remember [short|long|blackboard] 内容` 生成待确认的 `memory.write` 并追加短期、长期或黑板记忆,未写 scope 时默认追加长期记忆;`/memory-set [short|long|blackboard] 内容` 生成待确认的 `memory.write` 并覆盖保存对应记忆;`/forget-memory [short|long|blackboard]` 生成待确认的 `memory.delete`
- 聊天输入 `/memory [short|long|blackboard]` 读取短期、长期或黑板记忆;`/remember [short|long|blackboard] 内容` 生成待确认的 `memory.write` 并追加短期、长期或黑板记忆,未写 scope 时默认追加长期记忆;主窗口“记到黑板”只填入 `/remember blackboard ` 草稿,仍由用户补内容并走聊天确认;`/memory-set [short|long|blackboard] 内容` 生成待确认的 `memory.write` 并覆盖保存对应记忆;`/forget-memory [short|long|blackboard]` 生成待确认的 `memory.delete`
- 聊天输入 `/canvas 画板项目ID` 会生成待确认的 `canvas.project_open`,只打开本机 Genarrative 编辑器里的指定画板项目,不开放任意 URL;确认后聊天先反馈正在打开,再回写真实打开 URL。画板项目 ID 为空或包含控制字符时在聊天侧直接拒绝。
- 聊天输入 `/sync-canvas-project 画板项目ID` 会生成待确认的 `canvas.project_sync`,通过 External Editor API 把该画板项目资源下载到 `assets/canvas-sync/` 并登记为画板来源资产;画板项目 ID 为空或包含控制字符时在聊天侧直接拒绝。
- 聊天输入 `/generate-art 提示词` 会生成待确认的 `canvas.asset_generate`,通过 External Editor API 生成首版美术素材并写入 `assets/canvas-generated/`;提示词为空时在聊天侧直接拒绝。