diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 2a86a5760..96e2ea1f8 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -15,6 +15,8 @@ import { Archive, Bell, BookOpen, + ChevronDown, + ChevronUp, CircleHelp, Contact, FileText, @@ -575,13 +577,14 @@ interface LocalConversationResult { function createLocalConversationDraftMessage( content: string, + updatedAt = Date.now(), ): LocalConversationMessageRecord { return { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content, agentId: null, - updatedAt: Date.now(), + updatedAt, }; } @@ -860,6 +863,7 @@ function AgentRuntimeStatusPanel({ onRefreshRuntime?: () => void; }) { const [showAllRecentEvents, setShowAllRecentEvents] = useState(false); + const [collapsed, setCollapsed] = useState(false); if (!runtime && error) { return (
@@ -921,7 +925,23 @@ function AgentRuntimeStatusPanel({
{`${runtime.status} / ${runtime.phase}`} - {runtime.sessionId} +
+ {runtime.sessionId} + +
{onCancelRuntimeTask || onRetryRuntimeTask || @@ -977,6 +997,8 @@ function AgentRuntimeStatusPanel({ ) : null} + {collapsed ? null : ( +
{`task: ${runtime.taskId} · ${delegationSource ?? runtime.source}`} {runtime.runId ? {`run: ${runtime.runId}`} : null} {currentGoal ?

{`当前目标:${currentGoal}`}

: null} @@ -1087,6 +1109,8 @@ function AgentRuntimeStatusPanel({ ) : null} {runtime.error ?

{runtime.error}

: null} {error ?

{error}

: null} +
+ )}
); } @@ -4195,6 +4219,10 @@ export function WorkspaceLauncher({ let savedUserResult: LocalConversationResult | null = null; let stopStreamListen: (() => void) | null = null; let streamListenDisposed = false; + let streamFrameId: number | null = null; + let pendingStreamDraftText = ''; + let pendingStreamFinishReason: string | null = null; + const streamDraftUpdatedAt = Date.now(); try { savedUserResult = await invoke( 'append_local_conversation_message', @@ -4235,7 +4263,7 @@ export function WorkspaceLauncher({ ) { return; } - if (payload.runtimeState) { + if (payload.runtimeState && payload.status !== 'delta') { setAgentChatRuntime((current) => normalizeAgentRuntimeState(payload.runtimeState!, current), ); @@ -4253,23 +4281,46 @@ export function WorkspaceLauncher({ if (payload.status === 'delta') { const draftText = payload.accumulatedText || payload.deltaText; if (draftText) { - setAgentChatMessages([ - ...savedUserMessages, - createLocalConversationDraftMessage(draftText), - ]); + pendingStreamDraftText = draftText; + } + pendingStreamFinishReason = payload.finishReason ?? null; + if (streamFrameId === null) { + streamFrameId = window.requestAnimationFrame(() => { + streamFrameId = null; + if (agentChatLoadVersionRef.current !== saveVersion) { + return; + } + if (pendingStreamDraftText) { + setAgentChatMessages([ + ...savedUserMessages, + createLocalConversationDraftMessage( + pendingStreamDraftText, + streamDraftUpdatedAt, + ), + ]); + } + setAgentChatStatus( + pendingStreamFinishReason + ? `Agent 回复结束:${pendingStreamFinishReason}` + : '正在接收 Agent 回复', + ); + }); } - setAgentChatStatus( - payload.finishReason - ? `Agent 回复结束:${payload.finishReason}` - : '正在接收 Agent 回复', - ); return; } if (payload.status === 'completed') { + if (streamFrameId !== null) { + window.cancelAnimationFrame(streamFrameId); + streamFrameId = null; + } setAgentChatStatus(payload.runtimeSummary ?? 'Agent 回复完成,正在保存'); return; } if (payload.status === 'failed') { + if (streamFrameId !== null) { + window.cancelAnimationFrame(streamFrameId); + streamFrameId = null; + } setAgentChatStatus( payload.runtimeSummary ?? 'Agent 流式回复失败,正在记录错误', ); @@ -4326,10 +4377,14 @@ export function WorkspaceLauncher({ ); } } + if (streamFrameId !== null) { + window.cancelAnimationFrame(streamFrameId); + streamFrameId = null; + } setAgentChatStatus('正在保存 Agent 回复'); setAgentChatMessages([ ...savedUserMessages, - createLocalConversationDraftMessage(reply.replyText), + createLocalConversationDraftMessage(reply.replyText, streamDraftUpdatedAt), ]); const assistantResult = await invoke( 'append_local_conversation_message', @@ -4405,6 +4460,9 @@ export function WorkspaceLauncher({ } finally { streamListenDisposed = true; stopStreamListen?.(); + if (streamFrameId !== null) { + window.cancelAnimationFrame(streamFrameId); + } if (agentChatLoadVersionRef.current === saveVersion) { setAgentChatBusy(false); } @@ -5000,11 +5058,9 @@ export function WorkspaceLauncher({
0 - ? 'launcher-main launcher-main-with-promo' - : 'launcher-main' - } + className={`launcher-main${ + launcherNotifications.length > 0 ? ' launcher-main-with-promo' : '' + }${launcherView === 'agent-chat' ? ' launcher-main-agent-chat' : ''}`} > {launcherNotifications.length > 0 ? (
diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 397d2ac07..c6a85b5a7 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -295,6 +295,12 @@ textarea { overflow: hidden; } +.launcher-main-agent-chat { + height: 100vh; + min-height: 0; + padding-bottom: 0; +} + .launcher-promo { display: flex; align-items: center; @@ -1037,7 +1043,13 @@ textarea { } .launcher-agent-chat-page { + grid-template-rows: auto minmax(0, 1fr); + box-sizing: border-box; + height: 100vh; + max-height: 100vh; padding-top: 92px; + padding-bottom: 24px; + overflow: hidden; } .launcher-agent-chat-page > header .launcher-project-list-actions button { @@ -1062,13 +1074,16 @@ textarea { grid-template-columns: minmax(220px, 0.3fr) minmax(0, 1fr); gap: 12px; width: 100%; + height: 100%; min-width: 0; - min-height: min(620px, calc(100vh - 168px)); + min-height: 0; + overflow: hidden; } .launcher-agent-chat-sidebar, .launcher-agent-chat-main { min-width: 0; + min-height: 0; border: 1px solid #e5e7eb; border-radius: 8px; background: #fff; @@ -1076,9 +1091,11 @@ textarea { .launcher-agent-chat-sidebar { display: grid; - align-content: start; + grid-template-rows: auto minmax(0, 1fr); + align-content: stretch; gap: 12px; padding: 12px; + overflow: hidden; } .launcher-agent-chat-project { @@ -1106,8 +1123,9 @@ textarea { .launcher-agent-picker { display: grid; gap: 8px; - max-height: 450px; + min-height: 0; overflow: auto; + overscroll-behavior: contain; } .launcher-agent-picker button { @@ -1150,6 +1168,7 @@ textarea { .launcher-agent-chat-main { display: grid; grid-template-rows: auto auto auto minmax(0, 1fr) auto; + height: 100%; overflow: hidden; } @@ -1261,6 +1280,9 @@ textarea { gap: 10px; padding: 14px; overflow: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; + contain: layout paint; } .launcher-agent-chat-messages .message { @@ -1302,6 +1324,11 @@ textarea { .launcher-agent-runtime-stack { display: grid; gap: 8px; + max-height: min(44vh, 420px); + overflow: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; + contain: layout paint; } .launcher-agent-runtime-stack:empty { @@ -1364,6 +1391,45 @@ textarea { gap: 10px; } +.agent-runtime-status .agent-runtime-status-header-actions { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.agent-runtime-status-header-actions small { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.agent-runtime-status .agent-runtime-collapse-toggle { + display: grid; + width: 28px; + height: 28px; + flex: 0 0 auto; + padding: 0; + border: 1px solid #d8dde5; + border-radius: 7px; + background: #fff; + color: #44536a; + cursor: pointer; + place-items: center; +} + +.agent-runtime-status .agent-runtime-collapse-toggle:hover, +.agent-runtime-status .agent-runtime-collapse-toggle:focus-visible { + border-color: #aeb8c7; + background: #f3f6fa; +} + +.agent-runtime-status .agent-runtime-status-details { + display: grid; + gap: 6px; +} + .agent-runtime-status .agent-runtime-actions { display: flex; flex-wrap: wrap; @@ -1725,7 +1791,21 @@ textarea { .launcher-agent-chat-layout { grid-template-columns: 1fr; + height: auto; min-height: auto; + overflow: visible; + } + + .launcher-agent-chat-page { + grid-template-rows: auto auto; + height: auto; + max-height: none; + padding-bottom: 24px; + overflow: visible; + } + + .launcher-agent-chat-sidebar { + overflow: visible; } .launcher-agent-picker { @@ -1733,6 +1813,7 @@ textarea { } .launcher-agent-chat-main { + height: auto; min-height: 620px; } diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index e4e2643c5..e5c631141 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -2049,6 +2049,32 @@ describe('AI 游戏创作 App 界面边界', () => { name: '重试', }) as HTMLButtonElement).disabled, ).toBe(true); + const runtimePanel = screen.getByLabelText('Agent Runtime 状态'); + const collapseRuntimeButton = within(runtimePanel).getByRole('button', { + name: '折叠 Runtime 详情', + }); + expect(collapseRuntimeButton.getAttribute('aria-expanded')).toBe('true'); + fireEvent.click(collapseRuntimeButton); + expect( + within(runtimePanel) + .getByRole('button', { name: '展开 Runtime 详情' }) + .getAttribute('aria-expanded'), + ).toBe('false'); + expect( + within(runtimePanel).queryByText('当前目标:完成角色规范阶段'), + ).toBeNull(); + expect(within(runtimePanel).queryByText('计划进度')).toBeNull(); + expect(within(runtimePanel).getByText('running / action')).not.toBeNull(); + expect( + (within(runtimeActions).getByRole('button', { + name: '取消任务', + }) as HTMLButtonElement).disabled, + ).toBe(false); + fireEvent.click( + within(runtimePanel).getByRole('button', { + name: '展开 Runtime 详情', + }), + ); expect(screen.getByText('计划进度')).not.toBeNull(); expect( screen.getByText('#1 completed · 读取项目笔记 · file.read:ok · 已读取 game/notes.txt'), diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index ef36edbfa..6e66411bc 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -21,6 +21,7 @@ - 背景:开发用单 Agent 聊天已经能真实调用各 Agent 的 LLM 路由并持久化对话,但 Agent 仍主要表现为同步问答,用户无法明确投递一个任务让某个 Agent 独立运行,也无法同时启动多个 Agent 的工作。 - 决策:在现有 `.agent/runtime` 和 `.agent/conversations` 基础上新增单 Agent 后台任务入口。Tauri 命令 `start_game_creator_agent_runtime_task` 立即写入该 Agent 的 runtime state/event/task history,追加用户任务到 `.agent/conversations/agents/.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:Agent 按轮输出 `thinkingSummary / plan / actions / response`,Runtime 按白名单和项目权限策略执行工具并记录 `action / observation` 事件,再把已有 observation 放回下一轮 prompt,让 Agent 修正计划、继续行动或用空 actions + response 收束;当前后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复。完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;`memory.write` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加共享黑板,`agent.message` 写目标 Agent 对话,`agent.delegate` 把任务投递到目标 Agent 的独立后台队列;策略拒绝时不执行工具并把 `blocked` observation 回给 Agent;策略要求确认时不执行工具,而是持久化精确待确认动作并暂停该 Agent 队列,待开发者确认或拒绝后在同一 run 续跑。每个 Agent 的任务历史落在 `.agent/runtime/tasks/.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `pending / running / completed / failed`,Runtime state 增加 `nextStep`,UI 在 Runtime 面板和主 Agent 状态卡展示当前任务、动作、下一步与最近任务。不同 Agent 使用独立 `.agent/runtime/locks/.lock`,允许并行运行;同一 Agent 已有运行任务时,新任务会先进入该 Agent 的 pending 队列,当前 drain 持锁完成后串行继续下一条 pending。该能力仍不是独立 OS 进程或跨重启离线常驻 worker。 - 2026-07-10 补充:后台 Runtime 每次追加 `.agent/runtime/events/.jsonl` 后会通过 Tauri `game-creator-agent-runtime-update` 事件广播当前 `AgentRuntimeResult`;开发单 Agent 聊天页、项目内 Agent 对话弹窗和主窗口 Agent 状态列表都只把该事件作为实时 UI 通知并复用前端 runtime 归一化合并,事实源仍是 `.agent/runtime/agents`、`events` 和 `tasks` 文件。 +- 2026-07-11 补充:开发单 Agent 聊天页桌面端固定在当前视口内,Agent 列表、Runtime 状态和聊天记录使用各自的有界滚动区,避免长状态详情把整个 WebView 撑高并在实时事件更新时触发整页布局。Runtime 面板详情可折叠且折叠时不渲染详情 DOM,但状态标题与任务控制按钮继续保留;连续流式 delta 合并到动画帧更新,并跳过 delta 内重复的 Runtime state 提交。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `preview.start`,让 Agent 在完成写盘或静态自检后能按策略自行启动当前项目的 `127.0.0.1` 本地 HTTP 预览。该工具复用 `preview.start` 权限策略、项目写锁、共享 `PreviewRegistry`、manifest 预览状态、`.agent/logs/preview.log` 和 run trace 追加逻辑;写入 `.agent/agent.db` 的审计类型为 `agent.runtime.preview.start`。发给 LLM 的 observation 只包含 localhost URL 和端口,不包含用户项目绝对路径。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `canvas.asset_generate`,让美术类 Agent 可在 loop 中自行请求生成首版美术素材。该工具读取 AppData / Tauri 配置中的 `editorApi`,复用 `canvas.asset_generate` 权限策略、项目写锁、External Editor API 生成和下载链路、manifest 资产登记以及 `canvas.asset_generate` 本地索引记录;另写 `agent.runtime.canvas.asset_generate` 记录到 `.agent/agent.db`,标明触发的 agent 与本地素材路径。API Key 不进入 prompt observation、manifest、agent.db 或日志;策略要求确认或拒绝时不会调用外部 API。 - 补充:规范 Agent ID 统一使用 manifest taskId,例如 `art-asset-plan` 和 `code-prototype`;历史前端曾使用的 `group-role` 别名只在 Tauri command 层兼容并映射到规范 taskId。主窗口 Agent 状态列表通过 `read_game_creator_agent_runtimes` 批量读取 `.agent/runtime/agents/.json` 和最近任务,把每个 Agent 的 Runtime 状态、当前动作和最近 task 直接显示在状态卡片和 `/agents` 汇总里。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 099e65e05..b91ba798d 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -52,6 +52,7 @@ Agent Runtime 负责: - 2026-07-10 补充:`recentEvents` 接入前端归一态和 Runtime 状态面板,事件事实源仍是 `.agent/runtime/events/.jsonl`;面板按时间展示最近 `thinking_summary / plan / action / observation / response / error` 事件,现在能同时看到 Agent 的计划、最近观察、最近事件、最近工具动作和任务队列。 - 2026-07-10 补充:后台 Runtime 每次追加 `.agent/runtime/events/.jsonl` 后会通过 Tauri `game-creator-agent-runtime-update` 事件广播当前 `AgentRuntimeResult`,开发单 Agent 聊天页、项目内 Agent 对话弹窗和主窗口 Agent 状态卡用同一套前端归一化逻辑合并状态;该事件只做实时 UI 通知,`.agent/runtime/agents`、`events` 和 `tasks` 仍是重开项目后的事实源。 - 2026-07-10 补充:后台 Agent loop 的统一语义事件类型为 `thinking_summary / plan / action / observation / response / error`。普通失败和 loop 预算耗尽都会追加 `error` 事件,并继续保留 `turn.failed / turn.budget_exhausted` 生命周期事件兼容既有读取方;开发窗口、项目内 Agent 对话弹窗和主窗口状态卡通过现有最近事件列表直接展示统一错误事件及其安全详情。状态面板默认保持最新 4 条的紧凑视图,当前后端返回的最近事件超过 4 条时可展开查看全部返回记录,确保同一 run 的六类语义事件不会因 UI 硬截断而无法检查。 +- 2026-07-11 补充:开发单 Agent 聊天页在桌面视口内固定壳层高度,Agent 列表、Runtime 状态和聊天记录分别承担局部滚动,避免长 Runtime 详情持续撑高整个 WebView。Runtime 状态面板支持折叠详情,折叠时只卸载目标、计划、事件、动作和任务等详情 DOM,仍保留状态标题与取消、重试、确认、拒绝、刷新操作;流式聊天的连续 delta 通过 `requestAnimationFrame` 合并为每帧最多一次消息更新,delta 不重复提交未变化的 Runtime state,控制流式回复期间的布局与重绘范围。 - 2026-07-10 补充:Agent Runtime state / result 新增 `taskQueue`,从 `.agent/runtime/tasks/.jsonl` 中每个 `runId` 的最新记录汇总 `total / pending / running / completed / failed / latestRunId`;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都读取该摘要,用于判断同一 Agent 是否仍有排队任务。该字段是运行观测摘要,不新增调度器、SQLite 或独立 worker。 - 2026-07-10 补充:Runtime 新增 `agent.schedule_ready` 调度入口。开发构建可在权限确认后扫描 manifest ready task,把依赖已完成且仍为 `pending` 的任务标成 `running`,并按 taskId 投递到对应 Agent 的既有后台队列;source 固定为 `agent-ready-task-scheduler`,审计记录写 `agent.runtime.ready_task.scheduled`。该入口只把 manifest ready task 接入现有 per-agent 队列、锁、JSONL、LLM loop、工具策略和事件流,不新增独立 worker,也不会在默认确认策略下静默启动。 - 2026-07-10 补充:主窗口 Agent 状态栏的“调度 Ready”只在开发模式显示。点击后复用项目策略确认弹窗,确认通过才调用 `schedule_game_creator_agent_ready_tasks`,并把返回的 Runtime 合并回 Agent 状态卡;普通用户窗口继续只展示状态和单 Agent 对话入口,不直接暴露 ready-task 调度按钮。