diff --git a/CONTEXT.md b/CONTEXT.md index da2aca812..724d95de3 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -174,6 +174,14 @@ _Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI ## 项目开发对话(DirectProject) +**DirectProject 专属聊天模块**: +AGC 普通项目聊天的独立容器,拥有 DirectProject 的聊天状态、运行态订阅、历史读取、发送队列、附件和中止交互,并把聊天投影交给专属表现层渲染;它不承接 Supervisor、Design Agent 或 Planning V2 的运行态。 +_Avoid_: 把 DirectProject 作为项目总控聊天的一个布尔分支、把四种 Agent 会话抽象成同一事实源 + +**项目工作台布局**: +承载本地项目的资源工作区、项目级工具和独立聊天产品路径的外层界面;布局拥有跨面板的账户/钱包入口,聊天模块只负责项目对话,不嵌套账户展示。 +_Avoid_: 把钱包入口塞进聊天设置、让聊天组件拥有工作台级账户状态 + **项目对话历史**: AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。 _Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本 diff --git a/apps/admin-web/src/api/adminApiClient.test.ts b/apps/admin-web/src/api/adminApiClient.test.ts index 61c881989..16b39ca89 100644 --- a/apps/admin-web/src/api/adminApiClient.test.ts +++ b/apps/admin-web/src/api/adminApiClient.test.ts @@ -81,14 +81,12 @@ test('模板批量导入走 multipart,不预设 JSON Content-Type', async () = }, ], }; - const fetchMock = vi - .fn() - .mockImplementation( - async () => - new Response(JSON.stringify({ ok: true, data: imported }), { - status: 200, - }), - ); + const fetchMock = vi.fn().mockImplementation( + async () => + new Response(JSON.stringify({ ok: true, data: imported }), { + status: 200, + }), + ); vi.stubGlobal('fetch', fetchMock); const form = new FormData(); diff --git a/apps/ai-game-creator-shell/scripts/agc-global-version.test.mjs b/apps/ai-game-creator-shell/scripts/agc-global-version.test.mjs index 2d14f5963..abb218558 100644 --- a/apps/ai-game-creator-shell/scripts/agc-global-version.test.mjs +++ b/apps/ai-game-creator-shell/scripts/agc-global-version.test.mjs @@ -185,7 +185,12 @@ test('nextVersion 只在 patch 位递增', () => { test('ossutil 参数默认使用 v1 签名,并可按需带 region 与 v4', () => { const base = { - args: ['cp', '--force', '/tmp/a.json', 'oss://agc-dev/agc/global-version.json'], + args: [ + 'cp', + '--force', + '/tmp/a.json', + 'oss://agc-dev/agc/global-version.json', + ], endpoint: 'oss-rg-china-mainland.aliyuncs.com', accessKeyId: 'id', accessKeySecret: 'secret', diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index 57c325aef..eaba4e582 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -107,10 +107,6 @@ const appInvokeSources = readSourceFiles( new URL('../src/', import.meta.url), new Set(['.ts', '.tsx']), ); -const appEntrypointSource = fs.readFileSync( - new URL('../src/main.tsx', import.meta.url), - 'utf8', -); const tauriHandlerSource = fs.readFileSync( new URL('../src-tauri/src/main.rs', import.meta.url), 'utf8', @@ -135,6 +131,45 @@ const rustSharedContractSource = fs.readFileSync( ); const allowedUncalledTauriCommands = [ 'append_direct_project_conversation_message', + // Supervisor 调试窗口、开发者面板、专业 Agent 对话与旧命令聊天的前端调用方已随 + // Supervisor 前端链路整体删除;命令本身仍注册在 Rust 侧并由 native Runtime、CLI + // swarm 与 Rust 测试使用,保留 present,仅不再出现在 App 前端源码里。 + 'answer_game_creator_agent_runtime_user_input', + 'cancel_game_creator_agent_runtime_task', + 'chat_with_game_creator_role_agent', + 'chat_with_game_creator_role_agent_stream', + 'check_game_creator_llm_config', + 'confirm_game_creator_agent_runtime_task', + 'diff_local_project_checkpoint', + 'get_game_creation_agent_capabilities', + 'get_limited_local_commands', + 'list_local_project_export_packages', + 'read_game_creator_agent_runtime', + 'read_local_agent_memory', + 'read_local_game_memory', + 'reject_game_creator_agent_runtime_task', + 'retry_game_creator_agent_runtime_task', + 'schedule_game_creator_agent_ready_tasks', + 'start_game_creator_agent_runtime_task', + 'steer_game_creator_agent_runtime_task', + 'write_local_agent_memory', + 'write_local_game_memory', + 'write_local_project_file', + // Agent 运行时会话 / 目标 / 协作命令由 native 侧与 CLI swarm 驱动,前端没有调用方。 + 'archive_game_creator_agent_session', + 'clear_game_creator_agent_goal', + 'compact_game_creator_agent_runtime_context', + 'confirm_retry_game_creator_agent_runtime_task', + 'create_game_creator_agent_session', + 'edit_game_creator_agent_goal', + 'fork_game_creator_agent_session', + 'list_game_creator_agent_sessions', + 'pause_game_creator_agent_goal', + 'read_game_creator_agent_goal', + 'resume_game_creator_agent_goal', + 'set_active_game_creator_agent_session', + 'start_game_creator_agent_goal', + 'start_game_creator_supervisor_runtime_task', // TODO: Remove the retired binding command after the legacy runtime path is removed. 'bind_components', 'chat_with_game_creator_agent', @@ -164,6 +199,26 @@ const allowedUncalledTauriCommands = [ 'call_agc_plugin', 'read_agc_plugin_panel', 'set_agc_plugin_enabled', + // 下面这些命令的调用方只有随 Project Supervisor 前端链路一起删除的旧命令聊天入口; + // 现在 App 前端、工作台与策划聊天都没有接线(检查点 / 恢复 / 索引 / 导出包 / + // 画板同步 / 素材登记 / 权限策略 / 本地草案 / 平台美术),Rust 侧只剩注册与实现, + // `*_at` helper 仍由 Rust 用例覆盖。接回新入口还是删除属于 native 能力取舍,先按 + // native-only 登记,避免孤儿检查一直报错。 + // 预览不在本清单:`activate_local_game_preview` 已按 ADR 回接到 App 的「运行」入口。 + 'build_local_project_index', + 'control_agent_run', + 'create_local_project_checkpoint', + 'export_local_project_package', + 'generate_local_game_draft', + 'generate_platform_art_asset', + 'import_canvas_asset', + 'import_canvas_export', + 'open_canvas_project', + 'register_local_asset', + 'restore_local_project_checkpoint', + 'run_limited_local_command', + 'sync_canvas_project_assets', + 'write_project_permission_policy', ]; const sourceExtensions = new Set([ '.json', @@ -1470,13 +1525,7 @@ const eventCapability = JSON.parse( ); const eventCapabilityWindows = new Set(eventCapability.windows ?? []); const eventCapabilityPermissions = new Set(eventCapability.permissions ?? []); -for (const windowLabel of [ - 'client', - 'developer', - 'main', - 'launcher', - 'supervisor-chat', -]) { +for (const windowLabel of ['client', 'main', 'launcher']) { if (!eventCapabilityWindows.has(windowLabel)) { throw new Error( `AI game creator shell event capability missing window: ${windowLabel}`, @@ -1859,20 +1908,6 @@ if ( ); } -for (const snippet of [ - 'import.meta.env.DEV', - 'supervisorChatMode', - 'supervisorChatOnly', - 'open_project_supervisor_chat_window', - 'index.html?supervisor-chat&projectPath=', -]) { - if (!`${appEntrypointSource}\n${tauriRustSource}`.includes(snippet)) { - throw new Error( - `AI game creator shell developer window guardrail drifted: ${snippet}`, - ); - } -} - if (tauriHandlerSource.includes('open_developer_window(app.handle())?')) { throw new Error( 'AI game creator normal startup must not automatically open the developer window', @@ -1905,31 +1940,17 @@ for (const snippet of [ '官方账号服务(固定)', 'runtime_config.save', "'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'", - "'activate_local_game_preview'", - '已切换到客户端运行视图', 'async function executeRunLocal', 'function needsInitializedChatProject', - 'function resolvePendingCommandProjectPath', - 'resolveChatProjectPath(localProject) ?? draftProjectPath', - '`permission.cancel ${command.id} missing-project`', "'/remember [short|long|blackboard] 内容:追加短期、长期或黑板记忆'", "'/memory-set [short|long|blackboard] 内容:覆盖保存对应记忆'", 'function parseRememberInput', "'/trace 或 /loop:查看最近一次 Agent loop trace'", - 'async function executeAgentTraceChat', - "relativePath: '.agent/logs/command.log'", "'permission.pending'", "'permission.confirm'", "'permission.cancel'", "'command.auto'", - "'agent.run_status'", 'function summarizeAgentRunTrace', - '工具调用:${agentRunTrace.toolCallCount}/${agentRunTrace.maxToolCalls}', - 'agentRunTrace.error ?', - 'className="trace-error"', - 'agentRunTrace.taskGraph.repairRoutes.map', - "in: ${step.inputPaths.join(', ') || 'none'}", - "out: ${step.outputPaths.join(', ') || 'none'}", ]) { if (!appSource.includes(snippet)) { throw new Error( diff --git a/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs b/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs index 68fc0909a..20cf4a1ea 100644 --- a/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs +++ b/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs @@ -1192,7 +1192,7 @@ async function main() { function isDirectModuleExecution() { return Boolean( process.argv[1] && - resolve(process.argv[1]) === fileURLToPath(import.meta.url), + resolve(process.argv[1]) === fileURLToPath(import.meta.url), ); } diff --git a/apps/ai-game-creator-shell/src-tauri/capabilities/developer.json b/apps/ai-game-creator-shell/src-tauri/capabilities/developer.json deleted file mode 100644 index ca9fdae42..000000000 --- a/apps/ai-game-creator-shell/src-tauri/capabilities/developer.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "developer", - "description": "开发窗口允许打开本地素材选择对话框。", - "windows": ["developer"], - "permissions": ["dialog:allow-open"] -} diff --git a/apps/ai-game-creator-shell/src-tauri/capabilities/events.json b/apps/ai-game-creator-shell/src-tauri/capabilities/events.json index 8e31538b8..fa5b4f1d3 100644 --- a/apps/ai-game-creator-shell/src-tauri/capabilities/events.json +++ b/apps/ai-game-creator-shell/src-tauri/capabilities/events.json @@ -2,6 +2,6 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "events", "description": "允许客户端窗口订阅并取消订阅 Rust Runtime 事件。", - "windows": ["client", "developer", "main", "launcher", "supervisor-chat"], + "windows": ["client", "main", "launcher"], "permissions": ["core:event:allow-listen", "core:event:allow-unlisten"] } diff --git a/apps/ai-game-creator-shell/src-tauri/capabilities/window-chrome.json b/apps/ai-game-creator-shell/src-tauri/capabilities/window-chrome.json index a23607f6f..f7d700272 100644 --- a/apps/ai-game-creator-shell/src-tauri/capabilities/window-chrome.json +++ b/apps/ai-game-creator-shell/src-tauri/capabilities/window-chrome.json @@ -2,7 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "window-chrome", "description": "自绘标题栏允许执行当前窗口的基础控制和拖拽。", - "windows": ["client", "developer", "main", "launcher", "supervisor-chat"], + "windows": ["client", "main", "launcher"], "permissions": [ "core:window:allow-close", "core:window:allow-is-maximized", diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs index 6cf049bb3..35c52a8bc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs @@ -1180,7 +1180,6 @@ fn direct_codex_thread_delta_event( ) -> DirectThreadEvent { DirectThreadEvent::item_delta(item_id, kind, direct_thread_delta_text(root, delta)) } - /// 通知 → 回合事件的唯一分类函数:运行态读取器与单测共用这一份。 /// /// 读取器只负责"必须有 turnId 才处理"的前置条件与节流(活动 / 正文),分类不在这里之外 @@ -3358,8 +3357,22 @@ impl CodexAppServerConnection { codex_app_server_text_prompt(&request) .map_err(platform_llm::LlmError::InvalidRequest)? }; - let mut input = - codex_app_server_turn_input(&request, &prompt, &self.inner.workspace_path).await?; + let mut input = if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { + if let Some(item) = direct_user_item { + let canonical: DirectCodexUserItem = serde_json::from_value(item.clone()) + .map_err(|error| platform_llm::LlmError::InvalidRequest(error.to_string()))?; + direct_codex_user_item_to_codex_turn_input( + &self.inner.workspace_path, + &canonical, + self.inner._skill_roots.as_deref().unwrap_or_default(), + ) + .map_err(platform_llm::LlmError::InvalidRequest)? + } else { + codex_app_server_turn_input(&request, &prompt, &self.inner.workspace_path).await? + } + } else { + codex_app_server_turn_input(&request, &prompt, &self.inner.workspace_path).await? + }; if thread_created && self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { if let Some(client_turn_id) = direct_client_turn_id { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_attachments.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_attachments.rs index 0ef72c092..59639e93d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_attachments.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_attachments.rs @@ -2,9 +2,9 @@ //! 有项目路径或导入状态时输出路径映射;否则保持首页元数据文案。不灌正文。 pub(crate) const MAX_DIRECT_CODEX_ATTACHMENTS: usize = 8; -const MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS: usize = 160; -const MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96; -const MAX_DIRECT_CODEX_ATTACHMENT_LOCAL_PATH_CHARS: usize = 512; +pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS: usize = 160; +pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96; +pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_LOCAL_PATH_CHARS: usize = 512; const HOME_ATTACHMENT_HEADER: &str = prompt_text!("projectContext.attachments.homeHeader"); const PROJECT_ATTACHMENT_HEADER: &str = prompt_text!("projectContext.attachments.projectHeader"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/mod.rs index e33bfae72..946cb4de3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/mod.rs @@ -5,11 +5,11 @@ mod validation; mod wire; pub(crate) use model::{ - DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageItem, - DirectCodexUserRole, DirectCodexUserRuntimeRegionPart, + DirectCodexUserAttachmentReferencePart, DirectCodexUserContentPart, DirectCodexUserItem, + DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserRuntimeRegionPart, }; pub(crate) use validation::validate_direct_codex_user_item; pub(crate) use wire::{ - direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item, - direct_codex_user_item_to_wire_input, + direct_codex_user_item_to_codex_turn_input, direct_codex_user_item_to_prompt, + direct_codex_user_item_to_response_item, direct_codex_user_item_to_wire_input, }; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/model.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/model.rs index 266f330cc..b65f25ba4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/model.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/model.rs @@ -4,7 +4,7 @@ use ts_rs::TS; /// DirectProject 本轮 user input 的唯一结构化入口。 #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(tag = "type", deny_unknown_fields)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) enum DirectCodexUserItem { #[serde(rename = "message")] Message(DirectCodexUserMessageItem), @@ -12,7 +12,7 @@ pub(crate) enum DirectCodexUserItem { #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) struct DirectCodexUserMessageItem { pub(crate) role: DirectCodexUserRole, pub(crate) content: Vec, @@ -21,26 +21,43 @@ pub(crate) struct DirectCodexUserMessageItem { #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "lowercase")] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) enum DirectCodexUserRole { User, } #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(tag = "type", rename_all_fields = "camelCase", deny_unknown_fields)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) enum DirectCodexUserContentPart { #[serde(rename = "input_text")] InputText { text: String }, #[serde(rename = "agc_resource_reference")] AgcResourceReference { resource_id: String }, + #[serde(rename = "agc_skill_reference")] + AgcSkillReference { name: String }, #[serde(rename = "agc_runtime_region_reference")] AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart), + /// Uploaded project attachment kept inline in canonical content. + #[serde(rename = "agc_attachment_reference")] + AgcAttachmentReference(DirectCodexUserAttachmentReferencePart), } #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] +pub(crate) struct DirectCodexUserAttachmentReferencePart { + pub(crate) name: String, + pub(crate) media_type: String, + #[ts(type = "number")] + pub(crate) size: u64, + pub(crate) local_path: String, + pub(crate) status: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) struct DirectCodexUserRuntimeRegionPart { pub(crate) label: String, #[serde(default)] diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs index 9b410cc60..f63a13da1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs @@ -4,15 +4,20 @@ use super::model::{ }; use crate::agent::{ read_manifest_for_project, sanitize_attachment_local_path, GameCreationAppManifest, + MAX_DIRECT_CODEX_ATTACHMENTS, MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS, + MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS, }; use std::path::Path; pub(crate) const MAX_DIRECT_CODEX_REFERENCES: usize = 32; +// Skill 引用也是非文本 part,但没有走 reference_count:它每一条都会触发一次 +// `root//SKILL.md` 文件探测并往 turn input 里加一项,所以单独设上限。 +pub(crate) const MAX_DIRECT_CODEX_SKILL_REFERENCES: usize = 32; pub(crate) fn validate_direct_codex_user_item( root: &Path, item: &DirectCodexUserItem, -) -> Result<(), String> { +) -> Result { let DirectCodexUserItem::Message(message) = item; if !matches!(message.role, DirectCodexUserRole::User) { return Err("DirectProject 只接受 user message item".to_string()); @@ -20,38 +25,98 @@ pub(crate) fn validate_direct_codex_user_item( if message.id.trim().is_empty() { return Err("DirectProject user item 缺少稳定 id".to_string()); } - if message.content.is_empty() { + // 有效输入只判一整条 content:单个纯空白 `input_text` 是合法 part —— 编辑器里的段落 + // 分隔、软换行与 chip 后的分隔空格就是这样落进 canonical content 的,前端不为它过滤。 + if !content_has_meaningful_input(&message.content) { return Err("聊天内容不能为空".to_string()); } let manifest = read_manifest_for_project(root)?; let mut reference_count = 0usize; - let mut has_effective_content = false; + let mut attachment_count = 0usize; + let mut skill_count = 0usize; for part in &message.content { match part { - DirectCodexUserContentPart::InputText { text } => { - if !text.trim().is_empty() { - has_effective_content = true; - } - } + DirectCodexUserContentPart::InputText { .. } => {} DirectCodexUserContentPart::AgcResourceReference { resource_id } => { reference_count = reference_count.saturating_add(1); validate_resource_id_and_manifest(&manifest, resource_id)?; - has_effective_content = true; + } + DirectCodexUserContentPart::AgcSkillReference { name } => { + skill_count = skill_count.saturating_add(1); + if skill_count > MAX_DIRECT_CODEX_SKILL_REFERENCES { + return Err(format!( + "一次最多引用 {MAX_DIRECT_CODEX_SKILL_REFERENCES} 个 Skill" + )); + } + let name = name.trim(); + if name.is_empty() + || name.chars().count() > 120 + || matches!(name, "." | "..") + || name.chars().any(|character| { + character.is_control() + || character.is_whitespace() + || matches!(character, '/' | '\\' | ':' | '$') + }) + { + return Err("引用的 Skill 名称无效,请移除后重新选择".to_string()); + } } DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => { reference_count = reference_count.saturating_add(1); validate_runtime_region_reference(&manifest, reference)?; - has_effective_content = true; + } + DirectCodexUserContentPart::AgcAttachmentReference(reference) => { + attachment_count = attachment_count.saturating_add(1); + if attachment_count > MAX_DIRECT_CODEX_ATTACHMENTS { + return Err(format!( + "一次最多携带 {MAX_DIRECT_CODEX_ATTACHMENTS} 个附件" + )); + } + if reference.name.trim().is_empty() { + return Err("附件缺少文件名".to_string()); + } + let name = reference.name.trim(); + if name.chars().count() > MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS + || name.chars().any(char::is_control) + { + return Err("附件文件名无效或过长".to_string()); + } + let media_type = reference.media_type.trim(); + if media_type.is_empty() + || media_type.chars().count() > MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS + || media_type.chars().any(|character| { + !(character.is_ascii_alphanumeric() + || matches!(character, '/' | '+' | '-' | '.' | '_')) + }) + { + return Err("附件媒体类型无效或过长".to_string()); + } + let status = reference.status.trim(); + if status == "imported" && reference.local_path.trim().is_empty() { + return Err("已导入附件缺少项目路径".to_string()); + } + if !reference.local_path.trim().is_empty() { + sanitize_attachment_local_path(&reference.local_path) + .ok_or_else(|| "附件项目路径无效".to_string())?; + } + if !matches!(status, "imported" | "failed") { + return Err("附件状态无效".to_string()); + } } } } if reference_count > MAX_DIRECT_CODEX_REFERENCES { return Err(format!("一次最多引用 {MAX_DIRECT_CODEX_REFERENCES} 个素材")); } - if !has_effective_content { - return Err("聊天内容不能为空".to_string()); - } - Ok(()) + Ok(manifest) +} + +/// 整条 content 是否还有有效输入:任何一段非空白文本、或任何一个非文本 part 都算。 +pub(crate) fn content_has_meaningful_input(content: &[DirectCodexUserContentPart]) -> bool { + content.iter().any(|part| match part { + DirectCodexUserContentPart::InputText { text } => !text.trim().is_empty(), + _ => true, + }) } pub(crate) fn validate_resource_id_and_manifest( @@ -92,3 +157,173 @@ fn validate_runtime_region_reference( } Ok(()) } + +#[cfg(test)] +mod tests { + use super::{ + content_has_meaningful_input, validate_direct_codex_user_item, + MAX_DIRECT_CODEX_SKILL_REFERENCES, + }; + use crate::agent::direct_codex_user_item::model::DirectCodexUserContentPart; + use serde_json::json; + + fn input_text(text: &str) -> DirectCodexUserContentPart { + DirectCodexUserContentPart::InputText { + text: text.to_string(), + } + } + + #[test] + fn only_all_blank_content_counts_as_empty_input() { + // 空数组与「整条只有空白」是同一种空输入。 + assert!(!content_has_meaningful_input(&[])); + assert!(!content_has_meaningful_input(&[input_text(" \n ")])); + assert!(!content_has_meaningful_input(&[ + input_text("\n"), + input_text(" "), + ])); + } + + #[test] + fn whitespace_parts_are_valid_next_to_meaningful_input() { + // 段落分隔 / 软换行 / chip 后的分隔空格都是合法的单个 part。 + assert!(content_has_meaningful_input(&[ + input_text("\n"), + input_text("看"), + ])); + assert!(content_has_meaningful_input(&[ + input_text("看"), + input_text("\n\n"), + ])); + } + + #[test] + fn non_text_parts_always_count_as_input() { + assert!(content_has_meaningful_input(&[ + DirectCodexUserContentPart::AgcResourceReference { + resource_id: "asset-hero".to_string(), + }, + ])); + } + + #[test] + fn inline_attachment_count_is_bounded_independently() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "validation-test", "校验测试") + .expect("init project"); + let content = (0..=crate::agent::MAX_DIRECT_CODEX_ATTACHMENTS) + .map(|index| { + json!({ + "type": "agc_attachment_reference", + "name": format!("attachment-{index}.txt"), + "mediaType": "text/plain", + "size": 1, + "localPath": "", + "status": "failed" + }) + }) + .collect::>(); + let item = serde_json::from_value(json!({ + "type": "message", + "role": "user", + "content": content, + "id": "turn-1:user" + })) + .expect("deserialize user item"); + let error = validate_direct_codex_user_item(root.path(), &item) + .expect_err("too many inline attachments must be rejected"); + assert!(error.contains("最多携带"), "{error}"); + } + + #[test] + fn imported_attachment_requires_a_project_path() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "validation-test", "校验测试") + .expect("init project"); + let item = serde_json::from_value(json!({ + "type": "message", + "role": "user", + "content": [{ + "type": "agc_attachment_reference", + "name": "attachment.txt", + "mediaType": "text/plain", + "size": 1, + "localPath": "", + "status": "imported" + }], + "id": "turn-1:user" + })) + .expect("deserialize user item"); + let error = validate_direct_codex_user_item(root.path(), &item) + .expect_err("imported attachment without a project path must fail"); + assert!(error.contains("缺少项目路径"), "{error}"); + } + + #[test] + fn inline_skill_reference_count_is_bounded_independently() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "validation-test", "校验测试") + .expect("init project"); + let content = (0..=MAX_DIRECT_CODEX_SKILL_REFERENCES) + .map(|index| json!({ "type": "agc_skill_reference", "name": format!("skill-{index}") })) + .collect::>(); + let item = serde_json::from_value(json!({ + "type": "message", + "role": "user", + "content": content, + "id": "turn-1:user" + })) + .expect("deserialize user item"); + let error = validate_direct_codex_user_item(root.path(), &item) + .expect_err("too many skill references must be rejected"); + assert!(error.contains("最多引用"), "{error}"); + } + + #[test] + fn attachment_name_and_media_type_are_bounded_and_well_formed() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "validation-test", "校验测试") + .expect("init project"); + let long_name = "a".repeat(crate::agent::MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS + 1); + let cases = [ + ( + json!({ + "name": "bad\nname.txt", + "mediaType": "text/plain" + }), + "文件名", + ), + ( + json!({ + "name": "ok.txt", + "mediaType": "text/plain\nsecret" + }), + "媒体类型", + ), + ( + json!({ + "name": long_name, + "mediaType": "text/plain" + }), + "文件名", + ), + ]; + for (metadata, expected) in cases { + let mut value = metadata; + value["type"] = json!("agc_attachment_reference"); + value["size"] = json!(1); + value["localPath"] = json!(""); + value["status"] = json!("failed"); + let item = serde_json::from_value(json!({ + "type": "message", + "role": "user", + "content": [value], + "id": "turn-1:user" + })) + .expect("deserialize user item"); + let error = validate_direct_codex_user_item(root.path(), &item) + .expect_err("invalid attachment metadata must fail"); + assert!(error.contains(expected), "{error}"); + } + } +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs index b3d9e8d8d..d3e94388a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs @@ -1,6 +1,12 @@ -use super::model::{DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageItem}; +use super::model::{ + DirectCodexUserAttachmentReferencePart, DirectCodexUserContentPart, DirectCodexUserItem, + DirectCodexUserMessageItem, DirectCodexUserRuntimeRegionPart, +}; use super::validation::validate_direct_codex_user_item; -use crate::agent::{read_manifest_for_project, sanitize_attachment_local_path}; +use crate::agent::{ + read_manifest_for_project, sanitize_attachment_local_path, sanitize_attachment_media_type, + sanitize_attachment_name, GameCreationAppManifest, +}; use crate::ui_editor::persistence::{ generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND, UI_DESIGN_DOC_MEDIA_TYPE, @@ -55,54 +61,90 @@ fn direct_codex_user_item_to_response_content( .collect() } +fn resource_reference_summary( + manifest: &GameCreationAppManifest, + resource_id: &str, +) -> Result { + let resource_id = resource_id.trim(); + let asset = manifest + .assets + .iter() + .find(|asset| asset.id == resource_id) + .ok_or_else(|| "引用的素材已不存在,请移除后重新选择".to_string())?; + let path = sanitize_attachment_local_path(&asset.local_path) + .ok_or_else(|| "引用的素材路径无效,请移除后重新选择".to_string())?; + Ok(format!( + "[素材引用 resourceId={resource_id};项目路径={path}]" + )) +} + +fn runtime_region_summary(reference: &DirectCodexUserRuntimeRegionPart) -> String { + let resources = reference + .resource_ids + .iter() + .map(|id| id.trim()) + .collect::>() + .join(","); + let mut summary = format!("[运行画面区域:名称={} ", reference.label.trim()); + if let Some(run_id) = reference.run_id.as_deref() { + summary.push_str(&format!("运行标识={} ", run_id.trim())); + } + if let Some(role) = reference.element_role.as_deref() { + summary.push_str(&format!("角色={} ", role.trim())); + } + if let Some(text) = reference.text.as_deref() { + summary.push_str(&format!("文本={} ", text.trim())); + } + if !resources.is_empty() { + summary.push_str(&format!("关联素材={resources}")); + } + summary.push(']'); + summary +} + +/// 附件引用的安全摘要。 +/// +/// turn 输入与 history/prompt 投影共用这一份清洗:文件名取 basename 并去控制字符、 +/// media type 与项目路径同样过白名单,避免两条路径对同一个引用给出不同摘要。 +fn attachment_reference_summary(reference: &DirectCodexUserAttachmentReferencePart) -> String { + let name = sanitize_attachment_name(&reference.name); + let media_type = sanitize_attachment_media_type(&reference.media_type); + let mut summary = format!( + "[附件:名称={name};类型={media_type};大小={} 字节", + reference.size + ); + if let Some(local_path) = sanitize_attachment_local_path(&reference.local_path) { + summary.push_str(&format!(";项目路径={local_path}")); + } + summary.push_str(&format!(";状态={}", reference.status.trim())); + summary.push(']'); + summary +} + /// 将 canonical user item 转为 app-server `turn/start.input` 可接受的文本数组。 /// AGC 私有 part 只在这里投影为安全摘要,canonical item 本身不被修改。 pub(crate) fn direct_codex_user_item_to_wire_input( root: &Path, item: &DirectCodexUserItem, ) -> Result { - validate_direct_codex_user_item(root, item)?; - let manifest = read_manifest_for_project(root)?; + // validate 已经读过清单并返回它,不要再读一次(seed task 变更也会被重复触发)。 + let manifest = validate_direct_codex_user_item(root, item)?; let DirectCodexUserItem::Message(message) = item; let mut input = Vec::with_capacity(message.content.len()); for part in &message.content { let text = match part { DirectCodexUserContentPart::InputText { text } => text.clone(), DirectCodexUserContentPart::AgcResourceReference { resource_id } => { - let asset = manifest - .assets - .iter() - .find(|asset| asset.id == resource_id.trim()) - .ok_or_else(|| "引用的素材已不存在,请移除后重新选择".to_string())?; - let path = sanitize_attachment_local_path(&asset.local_path) - .ok_or_else(|| "引用的素材路径无效,请移除后重新选择".to_string())?; - format!( - "[素材引用 resourceId={};项目路径={path}]", - resource_id.trim() - ) + resource_reference_summary(&manifest, resource_id)? + } + DirectCodexUserContentPart::AgcSkillReference { name } => { + format!("${}", name.trim()) } DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => { - let resources = reference - .resource_ids - .iter() - .map(|id| id.trim()) - .collect::>() - .join(","); - let mut summary = format!("[运行画面区域:名称={} ", reference.label.trim()); - if let Some(run_id) = reference.run_id.as_deref() { - summary.push_str(&format!("运行标识={} ", run_id.trim())); - } - if let Some(role) = reference.element_role.as_deref() { - summary.push_str(&format!("角色={} ", role.trim())); - } - if let Some(text) = reference.text.as_deref() { - summary.push_str(&format!("文本={} ", text.trim())); - } - if !resources.is_empty() { - summary.push_str(&format!("关联素材={resources}")); - } - summary.push(']'); - summary + runtime_region_summary(reference) + } + DirectCodexUserContentPart::AgcAttachmentReference(reference) => { + attachment_reference_summary(reference) } }; input.push(serde_json::json!({ "type": "text", "text": text })); @@ -110,6 +152,55 @@ pub(crate) fn direct_codex_user_item_to_wire_input( Ok(Value::Array(input)) } +pub(crate) fn direct_codex_user_item_to_codex_turn_input( + root: &Path, + item: &DirectCodexUserItem, + skill_roots: &[std::path::PathBuf], +) -> Result { + let manifest = validate_direct_codex_user_item(root, item)?; + let DirectCodexUserItem::Message(message) = item; + let mut input = Vec::with_capacity(message.content.len()); + for part in &message.content { + match part { + DirectCodexUserContentPart::InputText { text } => { + input.push(serde_json::json!({ "type": "text", "text": text })); + } + DirectCodexUserContentPart::AgcResourceReference { resource_id } => { + input.push(serde_json::json!({ + "type": "text", + "text": resource_reference_summary(&manifest, resource_id)?, + })); + } + DirectCodexUserContentPart::AgcSkillReference { name } => { + let name = name.trim(); + let path = skill_roots + .iter() + .map(|root| root.join(name).join("SKILL.md")) + .find(|path| path.is_file()) + .ok_or_else(|| "引用的 Skill 当前不可用,请重新选择".to_string())?; + input.push(serde_json::json!({ + "type": "skill", + "name": name, + "path": path, + })); + } + DirectCodexUserContentPart::AgcRuntimeRegionReference(reference) => { + input.push(serde_json::json!({ + "type": "text", + "text": runtime_region_summary(reference), + })); + } + DirectCodexUserContentPart::AgcAttachmentReference(reference) => { + input.push(serde_json::json!({ + "type": "text", + "text": attachment_reference_summary(reference), + })); + } + } + } + Ok(Value::Array(input)) +} + pub(crate) fn direct_codex_user_item_to_prompt( root: &Path, item: &DirectCodexUserItem, @@ -118,17 +209,10 @@ pub(crate) fn direct_codex_user_item_to_prompt( let DirectCodexUserItem::Message(message) = item; let mut prompt = wire .as_array() - .ok_or_else(|| "DirectProject user item wire input 不是数组".to_string()) - .and_then(|parts| { - parts - .iter() - .map(|part| { - part.get("text") - .and_then(Value::as_str) - .ok_or_else(|| "DirectProject user item wire part 缺少 text".to_string()) - }) - .collect::>() - })?; + .ok_or_else(|| "DirectProject user item wire input 不是数组".to_string())? + .iter() + .filter_map(|part| part.get("text").and_then(Value::as_str)) + .collect::(); if let Some(code_context) = render_ui_design_code_context(root, message)? { prompt.push('\n'); prompt.push_str(&code_context); @@ -199,8 +283,9 @@ fn render_ui_design_code_context( mod tests { use super::{ direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item, - validate_direct_codex_user_item, + direct_codex_user_item_to_wire_input, validate_direct_codex_user_item, }; + use crate::agent::direct_codex_user_item::model::DirectCodexUserItem; use crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE; use serde_json::json; use shared_contracts::game_creation_app::{ @@ -283,6 +368,83 @@ mod tests { }) } + #[test] + fn ui_design_doc_reference_appends_generated_code_context() { + let (project, asset_id) = ui_design_doc_fixture(true); + let item: super::DirectCodexUserItem = + serde_json::from_value(user_item_with_resource_reference(&asset_id)) + .expect("canonical user item"); + let prompt = + direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection"); + assert!( + prompt.contains(&format!( + "[素材引用 resourceId={asset_id};项目路径=ui/design.json]" + )), + "{prompt}" + ); + assert!( + prompt.contains("请先阅读生成的带有文档的代码片段: ui/generated-"), + "{prompt}" + ); + } + + #[test] + fn history_projection_never_writes_generated_ui_design_code() { + let (project, asset_id) = ui_design_doc_fixture(true); + let item = user_item_with_resource_reference(&asset_id); + direct_codex_user_item_to_response_item(project.path(), &item).expect("history projection"); + let generated_root = + crate::resolve_local_project_path(project.path(), "ui").expect("resolve ui directory"); + let generated_files = std::fs::read_dir(generated_root) + .expect("read ui directory") + .filter_map(|entry| entry.ok()) + .filter(|entry| { + entry + .file_name() + .to_string_lossy() + .starts_with("generated-") + }) + .count(); + assert_eq!( + generated_files, 0, + "历史回读只做纯投影,不得生成 UI 设计代码" + ); + } + + #[test] + fn ui_design_generation_failure_keeps_reference_and_reports_error() { + let (project, asset_id) = ui_design_doc_fixture(false); + let item: super::DirectCodexUserItem = + serde_json::from_value(user_item_with_resource_reference(&asset_id)) + .expect("canonical user item"); + let prompt = + direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection"); + assert!(prompt.contains("素材引用 resourceId="), "{prompt}"); + assert!(prompt.contains("生成代码遇到错误"), "{prompt}"); + } + + #[test] + fn other_asset_kind_reference_does_not_generate_ui_design_code() { + let project = prompt_context_project(); + let asset_id = register_fixture_asset( + project.path(), + "assets/hero.png", + GameCreationAppAssetKind::Character, + "image/png", + ); + let item: super::DirectCodexUserItem = + serde_json::from_value(user_item_with_resource_reference(&asset_id)) + .expect("canonical user item"); + let prompt = + direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection"); + assert!(prompt.contains("素材引用 resourceId="), "{prompt}"); + assert!( + !prompt.contains("请先阅读生成的带有文档的代码片段"), + "{prompt}" + ); + assert!(!prompt.contains("生成代码遇到错误"), "{prompt}"); + } + #[test] fn standard_response_item_passes_through_without_agc_private_parts() { let item = json!({ @@ -297,6 +459,23 @@ mod tests { ); } + #[test] + fn response_item_projection_uses_input_text_not_turn_input_text() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试") + .expect("init project"); + let item = json!({ + "type": "message", + "role": "user", + "id": "turn-1:user", + "content": [{"type": "input_text", "text": "你好"}] + }); + let projected = direct_codex_user_item_to_response_item(root.path(), &item) + .expect("user response item should project"); + assert_eq!(projected["content"][0]["type"], "input_text"); + assert_ne!(projected["content"][0]["type"], "text"); + } + #[test] fn text_projection_preserves_empty_parts_line_breaks_and_trailing_whitespace() { let root = tempfile::tempdir().expect("temp project"); @@ -445,79 +624,97 @@ mod tests { } #[test] - fn ui_design_doc_reference_appends_generated_code_context() { - let (project, asset_id) = ui_design_doc_fixture(true); - let item: super::DirectCodexUserItem = - serde_json::from_value(user_item_with_resource_reference(&asset_id)) - .expect("canonical user item"); - let prompt = - direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection"); - assert!( - prompt.contains(&format!( - "[素材引用 resourceId={asset_id};项目路径=ui/design.json]" - )), - "{prompt}" - ); - assert!( - prompt.contains("请先阅读生成的带有文档的代码片段: ui/generated-"), - "{prompt}" - ); + fn attachment_parts_remain_in_canonical_order_when_projected() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试") + .expect("init project"); + let item = json!({ + "type": "message", + "role": "user", + "id": "turn-1:user", + "content": [ + {"type": "input_text", "text": "先看"}, + {"type": "agc_attachment_reference", "name": "notes.txt", "mediaType": "text/plain", "size": 4, "localPath": "assets/notes.txt", "status": "imported"} + ] + }); + let projected = direct_codex_user_item_to_response_item(root.path(), &item) + .expect("user response item should project"); + let content = projected["content"].as_array().expect("content array"); + assert_eq!(content.len(), 2); + assert!(content[0]["text"].as_str().unwrap().contains("先看")); + assert!(content[1]["text"].as_str().unwrap().contains("notes.txt")); } #[test] - fn history_projection_never_writes_generated_ui_design_code() { - let (project, asset_id) = ui_design_doc_fixture(true); - let item = user_item_with_resource_reference(&asset_id); - direct_codex_user_item_to_response_item(project.path(), &item).expect("history projection"); - let generated_root = - crate::resolve_local_project_path(project.path(), "ui").expect("resolve ui directory"); - let generated_files = std::fs::read_dir(generated_root) - .expect("read ui directory") - .filter_map(|entry| entry.ok()) - .filter(|entry| { - entry - .file_name() - .to_string_lossy() - .starts_with("generated-") - }) - .count(); - assert_eq!( - generated_files, 0, - "历史回读只做纯投影,不得生成 UI 设计代码" - ); + fn attachment_metadata_is_sanitized_before_prompt_projection() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试") + .expect("init project"); + let item = json!({ + "type": "message", + "role": "user", + "id": "turn-1:user", + "content": [{ + "type": "agc_attachment_reference", + "name": "C:\\tmp\\notes.md", + "mediaType": "text/plain", + "size": 4, + "localPath": "assets\\.\\notes.txt", + "status": "imported" + }] + }); + let wire = super::direct_codex_user_item_to_wire_input( + root.path(), + &serde_json::from_value(item).expect("deserialize user item"), + ) + .expect("attachment metadata should project"); + let text = wire[0]["text"].as_str().expect("wire text"); + assert!(text.contains("名称=notes.md"), "{text}"); + assert!(text.contains("类型=text/plain"), "{text}"); + assert!(text.contains("项目路径=assets/notes.txt"), "{text}"); } #[test] - fn ui_design_generation_failure_keeps_reference_and_reports_error() { - let (project, asset_id) = ui_design_doc_fixture(false); - let item: super::DirectCodexUserItem = - serde_json::from_value(user_item_with_resource_reference(&asset_id)) - .expect("canonical user item"); - let prompt = - direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection"); - assert!(prompt.contains("素材引用 resourceId="), "{prompt}"); - assert!(prompt.contains("生成代码遇到错误"), "{prompt}"); + fn whitespace_only_text_parts_survive_validation() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试") + .expect("init project"); + let item: DirectCodexUserItem = serde_json::from_value(json!({ + "type": "message", + "role": "user", + "id": "turn-1:user", + "content": [ + {"type": "input_text", "text": "先看"}, + {"type": "input_text", "text": "\n"}, + {"type": "input_text", "text": " "} + ] + })) + .expect("deserialize user item"); + let wire = direct_codex_user_item_to_wire_input(root.path(), &item) + .expect("whitespace-only part next to real text must pass"); + let parts = wire.as_array().expect("wire input array"); + assert_eq!(parts.len(), 3); + assert_eq!(parts[1]["text"].as_str(), Some("\n")); + assert_eq!(parts[2]["text"].as_str(), Some(" ")); } #[test] - fn other_asset_kind_reference_does_not_generate_ui_design_code() { - let project = prompt_context_project(); - let asset_id = register_fixture_asset( - project.path(), - "assets/hero.png", - GameCreationAppAssetKind::Character, - "image/png", - ); - let item: super::DirectCodexUserItem = - serde_json::from_value(user_item_with_resource_reference(&asset_id)) - .expect("canonical user item"); - let prompt = - direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection"); - assert!(prompt.contains("素材引用 resourceId="), "{prompt}"); - assert!( - !prompt.contains("请先阅读生成的带有文档的代码片段"), - "{prompt}" - ); - assert!(!prompt.contains("生成代码遇到错误"), "{prompt}"); + fn all_blank_content_is_rejected() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试") + .expect("init project"); + let item: DirectCodexUserItem = serde_json::from_value(json!({ + "type": "message", + "role": "user", + "id": "turn-1:user", + "content": [ + {"type": "input_text", "text": "\n"}, + {"type": "input_text", "text": " "} + ] + })) + .expect("deserialize user item"); + let error = direct_codex_user_item_to_wire_input(root.path(), &item) + .expect_err("all-blank content must fail closed"); + assert!(error.contains("不能为空"), "{error}"); } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs index 467b70fc1..43554cd85 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs @@ -30,11 +30,9 @@ pub(crate) fn normalize_direct_client_turn_id( #[tauri::command] pub(crate) async fn chat_with_game_creator_direct_codex( project_path: String, - prompt: String, - mut user_item: DirectCodexUserItem, + user_item: DirectCodexUserItem, creation_type: Option, client_turn_id: Option, - attachments: Option>, ) -> Result { let root = Path::new(project_path.trim()); let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?; @@ -43,74 +41,33 @@ pub(crate) async fn chat_with_game_creator_direct_codex( redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500) })?; let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone()); - let mut audit = DirectCodexTurnAudit::start( - root, - &turn_id, - &prompt, - attachments.as_deref().unwrap_or_default(), - ); - let attachments = attachments.unwrap_or_default(); - if !attachments.is_empty() { - let attachment_context = match render_direct_codex_user_prompt("", &attachments) { - Ok(context) => context, - Err(error) => { - audit.finish(false); - audit.flush().await; - return Err(error); - } - }; - let DirectCodexUserItem::Message(message) = &mut user_item; - message.content.push(DirectCodexUserContentPart::InputText { - text: attachment_context, - }); - } - if let Err(error) = validate_direct_codex_user_item(root, &user_item) { - audit.finish(false); - audit.flush().await; - return Err(error); - } - let user_prompt = match direct_codex_user_item_to_prompt(root, &user_item) { - Ok(prompt) => prompt, - Err(error) => { - audit.finish(false); - audit.flush().await; - return Err(error); - } - }; + validate_direct_codex_user_item(root, &user_item)?; + let user_prompt = direct_codex_user_item_to_prompt(root, &user_item)?; if user_prompt.trim().is_empty() { - audit.finish(false); - audit.flush().await; return Err("聊天内容不能为空".to_string()); } let canonical_user_item = // 创建类型来自结构化用户入口;实际工程和可信脚手架由宿主复核。 - match crate::environment_check::prepare_new_web_project_at(root, creation_type.as_deref()).await { + match crate::environment_check::prepare_new_web_project_at(root, creation_type.as_deref()) + .await + { Ok(_) => Some(serde_json::to_value(user_item).map_err(|error| error.to_string())?), - Err(error) => { - audit.finish(false); - audit.flush().await; - return Err(redact_agent_runtime_error(root, &error, 1800)); - } + Err(error) => return Err(redact_agent_runtime_error(root, &error, 1800)), }; let reply = match run_direct_game_creator_turn_at_with_creation_type_and_emitter( root, &user_prompt, creation_type.as_deref(), Some(&turn_emitter), - Some(&mut audit), + // DirectProject 的完整回合权威已经落在 project.jsonl;不再创建平行审计日志。 + None, canonical_user_item, ) .await { Ok(reply) => reply, - Err(error) => { - audit.finish(false); - audit.flush().await; - return Err(error); - } + Err(error) => return Err(error), }; - audit.finish(true); - audit.flush().await; turn_emitter.emit("completed", Some("none"), Some(reply.clone()), None); Ok(reply) } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_wire.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_wire.rs index d5a874c76..354dc8466 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_wire.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_wire.rs @@ -1,6 +1,6 @@ //! DirectProject 聊天事件的线上模型与投影。 //! -//! 前端消费的类型由 ts-rs 导出到 `src/features/project-workspace/generated/`, +//! 前端消费的类型由 ts-rs 导出到 `src/view/project-development/chat/generated/`, //! 与 Rust 定义同源:加一个字段不会只改一边。 //! //! 本模块只做三件事:挑字段、脱敏、截断。工具卡片的 `kind`、标题、折叠摘要、可见性与 @@ -29,7 +29,7 @@ const DIRECT_THREAD_PATH_MAX_CHARS: usize = 300; /// 一条文件变更。 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) struct DirectThreadFileChange { pub(crate) path: String, /// `add` | `update` | `delete` @@ -45,7 +45,7 @@ pub(crate) struct DirectThreadFileChange { /// 而 Tauri 的 JSON 通道传过来的是 `number`,因此统一标 `#[ts(as = "f64")]` 对齐。 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(tag = "itemType", rename_all_fields = "camelCase", deny_unknown_fields)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) enum DirectThreadItem { #[serde(rename = "message")] Message { @@ -179,7 +179,7 @@ impl DirectThreadItem { /// 增量正文属于哪类条目。 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) enum DirectThreadDeltaKind { /// assistant 正文。 Message, @@ -190,7 +190,7 @@ pub(crate) enum DirectThreadDeltaKind { /// 审批 / 提问请求与解决:本轮只透传,不并入聊天状态。 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) enum DirectThreadRequestKind { #[serde(rename = "approval.requested")] ApprovalRequested, @@ -235,7 +235,7 @@ impl DirectThreadRequestKind { /// (旧事件、没有开口用户条目、取消时拿不到 clientTurnId),此时前端不得补造。 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(tag = "type", rename_all_fields = "camelCase", deny_unknown_fields)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) enum DirectThreadEvent { #[serde(rename = "turn.started")] TurnStarted { @@ -398,7 +398,7 @@ impl DirectThreadEvent { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) struct DirectThreadSubscriptionBootstrap { pub(crate) subscription_id: String, /// 首屏历史锚点:`project.jsonl` 里最后一条原始 item id。 @@ -410,14 +410,14 @@ pub(crate) struct DirectThreadSubscriptionBootstrap { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) struct DirectThreadConsumeResult { pub(crate) events: Vec, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] pub(crate) struct DirectThreadHistorySlice { /// 脱敏条目,顺序即文件顺序;与运行态事件里的条目同形。 pub(crate) items: Vec, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs index 632cddbb7..7354b7ce8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs @@ -1,4 +1,4 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::borrow::Cow; use std::collections::BTreeSet; @@ -143,6 +143,13 @@ struct AgcSkillManifestEntry { sha256: String, } +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AgcSkillCatalogEntry { + pub(crate) name: String, + pub(crate) description: String, +} + fn is_safe_skill_relative_path(value: &str) -> bool { let path = Path::new(value); !value.is_empty() @@ -256,6 +263,21 @@ pub(crate) fn agc_skill_pack_fingerprint() -> Result { Ok(format!("{:x}", Sha256::digest(canonical_manifest.as_ref()))) } +/// 返回当前客户端随 AGC 一起启用的内置 Skill 候选。 +/// +/// 前端不得复制审核清单;Skill 名称和描述统一从经过校验的资源 manifest 派生。 +#[tauri::command] +pub(crate) fn list_agc_skill_catalog() -> Result, String> { + Ok(validated_skill_pack_manifest()? + .skills + .into_iter() + .map(|entry| AgcSkillCatalogEntry { + name: entry.name, + description: entry.purpose, + }) + .collect()) +} + pub(crate) fn render_agc_skill_pack_index() -> Result { let manifest = validated_skill_pack_manifest()?; let mut lines = vec![format!( @@ -350,6 +372,19 @@ mod tests { } } + #[test] + fn skill_catalog_is_derived_from_the_validated_manifest() { + let catalog = list_agc_skill_catalog().expect("skill catalog"); + assert_eq!(catalog.len(), AGC_SKILL_PACK_EXPECTED_NAMES.len()); + for expected_name in AGC_SKILL_PACK_EXPECTED_NAMES { + let entry = catalog + .iter() + .find(|entry| entry.name == expected_name) + .expect("expected bundled skill"); + assert!(!entry.description.trim().is_empty()); + } + } + #[test] fn skill_content_digest_is_stable_across_lf_and_crlf() { fn digest(bytes: &[u8]) -> String { 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 55ffaa90a..fa4163a4d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -887,37 +887,6 @@ pub(crate) async fn pick_local_project_directory( Ok(Some(path.to_string_lossy().into_owned())) } -#[tauri::command] -pub(crate) async fn pick_local_file(app: tauri::AppHandle) -> Result, String> { - let (sender, receiver) = tokio::sync::oneshot::channel(); - let mut dialog = app.dialog().file().set_title("选择本地文件"); - if let Some(window) = app.get_webview_window("client") { - dialog = dialog.set_parent(&window); - } - dialog.pick_file(move |path| { - let _ = sender.send(path); - }); - let Some(path) = receiver - .await - .map_err(|_| "本地文件选择器意外关闭".to_string())? - else { - return Ok(None); - }; - let path = path - .into_path() - .map_err(|error| format!("读取本地文件失败:{error}"))?; - #[cfg(windows)] - crate::register_game_creator_user_selected_path(&path, false); - if let Err(error) = - crate::prepare_game_creator_user_selected_path_for_read(&path, false, "用户选择文件") - { - #[cfg(windows)] - crate::revoke_game_creator_user_selected_path(&path); - return Err(error); - } - Ok(Some(path.to_string_lossy().into_owned())) -} - #[tauri::command] pub(crate) fn open_local_project_directory( app: tauri::AppHandle, @@ -5688,18 +5657,6 @@ pub(crate) fn write_local_project_file( write_local_project_file_at(root, relative_path.trim(), &content) } -#[tauri::command] -pub(crate) fn delete_local_project_file( - project_path: String, - relative_path: String, -) -> Result { - let root = Path::new(project_path.trim()); - enforce_project_permission_policy(root, "file.delete")?; - let _lock = acquire_project_write_lock(root, "file.delete")?; - advance_agent_runtime_project_revision_locked(root)?; - delete_local_project_file_at(root, relative_path.trim()) -} - #[tauri::command] pub(crate) fn read_local_game_memory( project_path: String, @@ -5748,18 +5705,6 @@ pub(crate) fn write_local_game_memory( write_local_game_memory_at(root, scope.trim(), &content) } -#[tauri::command] -pub(crate) fn delete_local_game_memory( - project_path: String, - scope: String, -) -> Result { - let root = Path::new(project_path.trim()); - enforce_project_permission_policy(root, "memory.delete")?; - let _lock = acquire_project_write_lock(root, "memory.delete")?; - advance_agent_runtime_project_revision_locked(root)?; - delete_local_game_memory_at(root, scope.trim()) -} - #[tauri::command] pub(crate) fn list_game_creator_agent_sessions( project_path: String, 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 679bffa1d..151a18cfe 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -2578,10 +2578,10 @@ fn main() { rename_local_game_project, suggest_automatic_project_name, polish_local_project_prompt, - pick_local_file, pick_client_extension_file, pick_client_extension_directory, list_client_extensions, + list_agc_skill_catalog, import_client_extension, set_client_extension_enabled, rename_client_extension, @@ -2696,12 +2696,10 @@ fn main() { read_local_project_media_preview, cancel_local_project_resource_preview_scope, write_local_project_file, - delete_local_project_file, read_local_game_memory, read_local_agent_memory, write_local_agent_memory, write_local_game_memory, - delete_local_game_memory, list_game_creator_agent_sessions, create_game_creator_agent_session, fork_game_creator_agent_session, @@ -2726,7 +2724,6 @@ fn main() { write_project_permission_policy, open_game_creator_workspace_window, open_game_creator_launcher_window, - open_project_supervisor_chat_window, start_local_game_preview, activate_local_game_preview, stop_local_game_preview, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs index a24590eb0..b0bbdcde2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs @@ -451,18 +451,6 @@ fn dispatch_static_delegate_plain_repair( ) } -#[test] -fn supervisor_chat_window_carries_encoded_project_path() { - assert_eq!( - supervisor_chat_window_url("/tmp/AI Game 项目").to_string(), - "index.html?supervisor-chat&projectPath=%2Ftmp%2FAI%20Game%20%E9%A1%B9%E7%9B%AE" - ); - assert_eq!( - supervisor_chat_window_url("/tmp/a&b?#%+c").to_string(), - "index.html?supervisor-chat&projectPath=%2Ftmp%2Fa%26b%3F%23%25%2Bc" - ); -} - #[test] fn project_supervisor_runtime_id_is_normalized_and_collected() { let root = unique_project_path(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/sessions.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/sessions.rs index 7ebba5ce9..af671e1c2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/sessions.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/sessions.rs @@ -2777,7 +2777,7 @@ fn agent_run_history_prunes_to_latest_hundred_traces() { } #[test] -fn developer_project_file_and_memory_mutations_advance_project_revision() { +fn developer_project_file_and_memory_writes_advance_project_revision() { let root = unique_project_path(); init_local_game_project_at(&root, "project-1", "开发面板 revision").expect("project init"); let project_path = root.to_string_lossy().into_owned(); @@ -2800,16 +2800,11 @@ fn developer_project_file_and_memory_mutations_advance_project_revision() { "项目记忆".to_string(), ) .expect("write project memory through command"); - delete_local_game_memory(project_path.clone(), "long".to_string()) - .expect("delete project memory through command"); - delete_local_project_file(project_path, "game/revision.txt".to_string()) - .expect("delete project file through command"); - assert_eq!( read_game_creator_agent_runtime_project_revision(&root) .expect("read developer mutation revision") .revision, - 5 + 3 ); fs::remove_dir_all(root).ok(); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/windows.rs b/apps/ai-game-creator-shell/src-tauri/src/windows.rs index 854970f6f..4406f2e29 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/windows.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/windows.rs @@ -119,13 +119,6 @@ pub(crate) fn launcher_window_url() -> tauri::WebviewUrl { tauri::WebviewUrl::App(PathBuf::from("index.html?launcher")) } -pub(crate) fn supervisor_chat_window_url(project_path: &str) -> tauri::WebviewUrl { - tauri::WebviewUrl::App(PathBuf::from(format!( - "index.html?supervisor-chat&projectPath={}", - percent_encode_query_value(project_path) - ))) -} - pub(crate) fn validate_workspace_window_project_path(project_path: &str) -> Result<&str, String> { let project_path = project_path.trim(); if project_path.is_empty() { @@ -193,52 +186,3 @@ pub(crate) fn open_game_creator_launcher_window( window.close().map_err(|error| error.to_string())?; Ok(()) } - -#[tauri::command] -pub(crate) fn open_project_supervisor_chat_window( - app: tauri::AppHandle, - project_path: String, -) -> Result<(), String> { - let project_path = validate_workspace_window_project_path(&project_path)?; - #[cfg(not(debug_assertions))] - { - let _ = app; - let _ = project_path; - return Err("项目总控对话窗口仅在开发构建中可用".to_string()); - } - #[cfg(debug_assertions)] - { - if let Some(existing) = app.get_webview_window("supervisor-chat") { - let mut current_url = existing.url().map_err(|error| error.to_string())?; - let current_project_path = current_url - .query_pairs() - .find_map(|(key, value)| (key == "projectPath").then(|| value.into_owned())); - if current_project_path.as_deref() != Some(project_path) { - current_url.set_query(Some(&format!( - "supervisor-chat&projectPath={}", - percent_encode_query_value(project_path) - ))); - current_url.set_fragment(None); - existing - .navigate(current_url) - .map_err(|error| error.to_string())?; - } - existing.show().map_err(|error| error.to_string())?; - existing.unminimize().map_err(|error| error.to_string())?; - existing.set_focus().map_err(|error| error.to_string())?; - return Ok(()); - } - tauri::WebviewWindowBuilder::new( - &app, - "supervisor-chat", - supervisor_chat_window_url(project_path), - ) - .title("项目总控 Agent 对话") - .decorations(false) - .inner_size(820.0, 720.0) - .min_inner_size(560.0, 480.0) - .build() - .map_err(|error| error.to_string())?; - Ok(()) - } -} diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 5285bb3c0..2403d3913 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -1,7 +1,6 @@ /* eslint-disable react-refresh/only-export-components -- Testable pure helpers currently share this legacy app module. */ import { - type ChangeEvent, type FormEvent, type UIEvent, useCallback, @@ -12,289 +11,86 @@ import { } from 'react'; import { - GAME_CREATION_AGENT_CAPABILITIES, - GAME_CREATION_APP_COMMANDS, - GAME_CREATION_APP_LIMITED_RUN_COMMANDS, - type GameCreationAgentCapabilityDescriptor, - type GameCreationAgentRunTrace, - type GameCreationAgentToolCallTrace, - type GameCreationAppAssetKind, type GameCreationAppCommandDescriptor, - type GameCreationAppLimitedRunCommandDescriptor, type GameCreationAppManifest, - type GameCreationAppPermission, type GameCreationAppPreviewState, type GameCreationAppPreviewStatus, - parseGameCreationAppAssetKind, } from '../../../packages/shared/src/contracts/gameCreationApp'; import { AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD, - AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT, - AGENT_RUN_HISTORY_MAX_COUNT, - AGENT_RUN_HISTORY_VISIBLE_STEP, CONVERSATION_INITIAL_VISIBLE_COUNT, CONVERSATION_VISIBLE_STEP, createLocalProjectId, - PROJECT_SUPERVISOR_AGENT_ID, seedManifest, } from './app/constants'; import { useEscapeToClose } from './app/dialogs'; +import { claimInitialTurnForPage } from './app/initialTurnClaims'; import { resolveTauriInvoke } from './app/tauri'; import type { - AgentBackgroundSubmitMode, - AgentProgressEvent, - AgentRunControlResult, - AgentRunHistoryItem, - AgentRuntimeResponseStream, AgentRuntimeResult, AgentRuntimeState, - AgentRuntimeSteerResult, - AgentRuntimeUserInputRequest, - AgentStatusCard, ChatMessage, DesignAgentInput, DesignClarificationRequest, DesignEvent, DesignView, - DirectTurnCancelView, - GameCreatorAgentRuntimeUpdateEvent, - GameCreatorChatAgentReply, - GameCreatorLlmConfigStatus, - GameCreatorManifestInvalidatedEvent, - GameCreatorRoleAgentChatStreamEvent, - GenerateLocalGameDraftResult, - ImportCanvasExportResult, InitLocalProjectResult, LauncherImportedAttachment, - LimitedLocalCommandResult, - ListLocalProjectFilesResult, - LocalAgentMemoryResult, - LocalConversationMessageRecord, LocalConversationResult, - LocalGameMemoryResult, LocalGameProjectRevisionStatus, LocalPreviewResult, LocalPreviewStatus, - LocalProjectCheckpointResult, - LocalProjectCheckpointSummary, - LocalProjectDiffResult, - LocalProjectExportPackageResult, - LocalProjectExportPackagesResult, - LocalProjectFileEntry, - LocalProjectFileMutationResult, LocalProjectFileResult, - LocalProjectIndexResult, LocalProjectKind, - LocalProjectRestoreResult, - MemoryScope, - MemoryWriteMode, - OpenCanvasProjectResult, - PendingCommand, PendingUiConfirmation, - ProjectPermissionPolicy, ProjectPermissionPolicyView, - SyncCanvasProjectAssetsResult, TauriInvoke, - UploadLocalAssetResult, } from './app/types'; -import { useWindowChrome } from './components/windowChromeContext'; import { agentConversationId, - agentRuntimeCancelStatus, - agentRuntimeConversationStatus, - agentRuntimeNeedsUserInput, - agentRuntimeStartedRunId, - agentRuntimeStartStatus, agentRuntimeStateFromResult, - agentRuntimeSteerStatus, - conversationContainsProjectSupervisorResponseStream, createAgentChatRunId, createDefaultChatMessages, - createLocalConversationDraftMessage, - ensureProjectSupervisorActiveSessionId, isAgentFinalizationMessageId, - isAgentRuntimeTerminalState, isMissingAgentRuntimeResumeCommandError, isRuntimeConfigMissingError, - matchingAgentRuntimeForSteer, mergeAgentRuntimeStateIntoMap, - mergeProjectSupervisorConversation, - mergeProjectSupervisorResponseStream, - normalizeAgentRuntimeState, - projectNameFromPath, projectProfessionalAgentLabel, - projectRuntimeVisibleError, - projectSupervisorPendingRepairMatchesProfessional, - readProjectSupervisorActiveSessionId, - resolveProjectSupervisorRuntimeSubmission, - sameAgentRuntimeRun, - submitProjectSupervisorRuntimeTask, taskRowsFromManifest, } from './features/agent-runtime'; import { - type DirectCodexTurnAttachment, - toDirectCodexTurnAttachments, -} from './features/app-shell/directCodexTurnAttachments'; -import { - isDeveloperMode, isTransientProjectOpenMessage, latestVisibleItems, - persistSupervisorChatDraft, - type ProjectSupervisorComponentProps, + type ProjectChatComponentProps, readInitialProjectPath, - readSupervisorChatDraft, type WorkspaceLauncherProps, writeRecentWorkspace, } from './features/app-shell/model'; -import { uploadLocalFilesAsAttachments } from './features/app-shell/useHomeProjectCreation'; import { WorkspaceLauncherShell } from './features/app-shell/WorkspaceLauncher'; import { - agentConversationReadDraftsFromManifest, - agentMemoryReadDraftsFromManifest, - commandDraftFromSuggestedToolCall, - deriveAgentStatusCards, - formatAgentDialogLlmStatus, - formatAgentLlmConfigWarning, - formatAgentRunControlError, - formatCodexRuntimeCapabilities, - formatLlmAgentStatusLine, - formatLlmRouteEndpoint, - isMissingAgentRunTraceError, projectAgentRuntimeSummaries, - readablePassArtifactsFromAgentRunTrace, - sameAgentStatusCard, - summarizeAgentAudit, - summarizeAgentCapabilities, - summarizeAgentConversationReadDrafts, - summarizeAgentLlmRoutes, - summarizeAgentMemoryReadDrafts, - summarizeAgentPassArtifactReadDrafts, summarizeAgentRunCompletionForChat, - summarizeAgentRunHistoryReadDrafts, - summarizeAgentStatusCardsForChat, - summarizeLimitedLocalCommands, - summarizeRunArtifactReadDrafts, } from './features/project-summary/agentPresentation'; import { - chatCommandHelp, - checkpointSummaryFromManifest, - commonAgentRunSupportReadDrafts, - commonProjectArtifactReadDrafts, - commonProjectInternalReadDrafts, - commonProjectLogReadDrafts, - firstReadableProjectAssetPath, - formatAgentRunStatus, - formatCanvasAssetSource, isAbsoluteProjectPath, - isSafeProjectRelativePath, - missingChatCommandArgumentMessage, - projectFileActionDrafts, projectPathHasControlCharacter, - projectPathsMatchForInvalidation, - readableArtifactsFromAgentRunTrace, - sortCheckpointManifestFiles, - summarizeAgentRunSupportFileReadDrafts, - summarizeCommonProjectArtifactReadDrafts, - summarizeCommonProjectLogReadDrafts, - summarizeMainProjectHeader, - summarizeProjectAssetCredits, - summarizeProjectAssets, - summarizeProjectAudioAssets, - summarizeProjectCheckpoint, - summarizeProjectCheckpoints, - summarizeProjectDiff, - summarizeProjectExportPackage, - summarizeProjectExportPackages, - summarizeProjectFileContent, - summarizeProjectFiles, - summarizeProjectIndex, - summarizeProjectInternalReadDrafts, - summarizeProjectPolicy, - summarizeProjectStatus, - summarizeProjectTasks, - summarizeProjectVisualAssets, } from './features/project-summary/projectSummary'; -import { AgentConversationOverlay } from './features/project-workspace/AgentConversationOverlay'; -import { - gameDraftStartedMessage, - isMissingProjectFileError, - parseAgentRunTrace, -} from './features/project-workspace/agentRunTrace'; -import { - chatQueueFullNotice, - createQueuedChatTurn, - dequeueChatTurn, - enqueueChatTurn, - isChatTurnQueueFull, - type QueuedChatTurn, - removeQueuedChatTurn, -} from './features/project-workspace/chatComposerQueue'; -import { DeveloperProjectPanels } from './features/project-workspace/DeveloperProjectPanels'; -import { DeveloperRuntimePanels } from './features/project-workspace/DeveloperRuntimePanels'; -import { - type DirectHistoryAnchorGate, - directHistoryAnchorGateToWaitFor, - reuseOrOpenDirectHistoryAnchorGate, -} from './features/project-workspace/directHistoryAnchorGate'; -import { readDirectHistoryPages } from './features/project-workspace/directHistoryPaging'; -import { - applyDirectThreadConsumeResult, - type DirectThreadChatState, - directThreadTurnMatchesUser, - emptyDirectThreadChatState, - finishDirectThreadTurn, - mergeDirectHistoryItems, - resolveDirectThreadBootstrap, - selectDirectChatEntries, -} from './features/project-workspace/directThreadChat'; -import { - type DirectThreadConsumeResult, - type DirectThreadHistorySlice, - type DirectThreadItem, - type DirectThreadSubscriptionBootstrap, -} from './features/project-workspace/directThreadEvents'; -import type { DirectCodexUserContentPart } from './features/project-workspace/generated'; +import { parseAgentRunTrace } from './features/project-workspace/agentRunTrace'; import { importDesignFiles } from './features/project-workspace/importDesignFiles'; -import { - appendMemoryContent, - memoryScopeLabel, - memoryScopePath, - parseOptionalMemoryScope, - parseRememberInput, -} from './features/project-workspace/memoryCommands'; -import { pendingCommandDetail } from './features/project-workspace/pendingCommandPresentation'; +import { parseRememberInput } from './features/project-workspace/memoryCommands'; import { isAgentTraceFilePath, - isProjectPolicyConfirmableCommandId, - isRegisteredGameCreationCommandId, - isSafeCanvasProjectId, - isSafeCheckpointId, needsInitializedChatProject, resolveChatProjectPath, - resolvePendingCommandProjectPath, } from './features/project-workspace/projectCommandPolicy'; -import { handleProjectSummaryChatCommand } from './features/project-workspace/projectSummaryCommands'; -import { ProjectSupervisorView } from './features/project-workspace/ProjectSupervisorView'; -import { ProjectWorkspaceChatPane } from './features/project-workspace/ProjectWorkspaceChatPane'; import type { ResourceReferenceInputHandle } from './features/project-workspace/ResourceReferenceInput'; -import type { - ChatComposerDraft, - ChatReference, -} from './features/project-workspace/resourceReferences'; import { - chatComposerDraftToDirectCodexUserItem, + directCodexContentToLegacyContentDto, + hasMeaningfulDirectCodexContent, RESOURCE_REFERENCE_INSERT_EVENT, - RESOURCE_REFERENCE_INSERT_MANY_EVENT, type ResourceReferenceInsertEventDetail, - type ResourceReferenceInsertManyEventDetail, } from './features/project-workspace/resourceReferences'; -import { SupervisorChatOnlyView } from './features/project-workspace/SupervisorChatOnlyView'; import { RuntimeConfigDialog } from './features/runtime-config/RuntimeConfigDialog'; -import { captureAgentRuntimeError } from './services/errorReporting'; -import { - currentPlatformSessionGeneration, - requestPlatformSessionRefresh, -} from './services/platformSession'; import { setAgcPluginProjectPath, startAvailableAgcEditorPlugins, @@ -308,87 +104,19 @@ import { type ProjectAgentResultSummary, type ProjectAgentRuntimeSummary, } from './view/project-development'; +import { DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX } from './view/project-development/chat/conversation/directCodexConversation'; +import { + directCodexAttachmentContentParts, + toDirectCodexTurnAttachments, +} from './view/project-development/chat/conversation/directCodexTurnAttachments'; +import { + type DirectProjectChatHandle, + DirectProjectChatView, + type DirectProjectInitialTurn, +} from './view/project-development/chat/DirectProjectChatView'; +import { PlanningChatView } from './view/project-development/planning/PlanningChatView'; import type { ProjectManifestSnapshotMetadata } from './view/project-development/projectResourceLiveUpdateModel'; -const initialSupervisorMessageClaimsByPage = new WeakMap>(); -const DIRECT_CODEX_PRODUCT_RUNTIME = true; -const DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX = 'direct-codex:'; -const DIRECT_CODEX_TURN_ALREADY_RUNNING_ERROR_PREFIX = - 'direct-codex-turn-already-running:'; -/** 与 Rust 侧 `DirectTaonierActiveInvocationGuard::enter` 的 else 分支文案保持一致。 */ -const DIRECT_CODEX_ANOTHER_TURN_RUNNING_ERROR_MARKER = - '当前项目已有另一条 Direct 客户端回合正在运行'; -// Platform access tokens are short lived. DirectProject can spend several -// minutes in image generation, build and browser validation, so keep the -// client-owned native session current while a turn is running. The singleflight -// refresh in platformSession.ts coalesces this with any 401-triggered refresh. -const DIRECT_CODEX_SESSION_KEEPALIVE_MS = 5 * 60 * 1000; - -function isDirectCodexAuthenticationRequired(error: unknown) { - const message = error instanceof Error ? error.message : String(error); - return ( - message.includes('authentication-required') || - message.includes('codex-app-server-error:unauthorized') || - /kind=codex-app-server-unauthorized(?=\s|$)/.test(message) || - message.includes('登录已失效') - ); -} - -async function withDirectCodexSessionRefresh(operation: () => Promise) { - const generation = currentPlatformSessionGeneration(); - try { - return await operation(); - } catch (error) { - if (!isDirectCodexAuthenticationRequired(error)) throw error; - if (currentPlatformSessionGeneration() !== generation) throw error; - const refresh = await requestPlatformSessionRefresh(); - if (refresh.status === 'failed') throw error; - if ( - refresh.status !== 'refreshed' || - currentPlatformSessionGeneration() !== refresh.generation - ) { - throw new Error('登录账号已变化,原对话请求已停止'); - } - return operation(); - } -} - -function directCodexConversationMessageId( - turnId: string, - role: ChatMessage['role'], -) { - return `${DIRECT_CODEX_CONVERSATION_MESSAGE_ID_PREFIX}${turnId}:${role}`; -} - -export const MAX_CHAT_COMPOSER_ATTACHMENTS = 8; - -export function isDirectCodexTurnAlreadyRunningError(error: unknown) { - const message = error instanceof Error ? error.message : String(error); - return message - .trimStart() - .startsWith(DIRECT_CODEX_TURN_ALREADY_RUNNING_ERROR_PREFIX); -} - -/** - * 另一条 Direct 回合占着这个项目时的拒绝。它与上面那条同 clientTurnId 的拒绝分属不同 - * 错误分类(Rust 侧刻意不带前缀),但对界面是同一件事:本项目现在有一条我们没接管的 - * 回合在跑。所以这里单独判定,让它也走"接管它 + 告诉用户出口"的处理。 - */ -export function isDirectCodexAnotherTurnRunningError(error: unknown) { - const message = error instanceof Error ? error.message : String(error); - return message.includes(DIRECT_CODEX_ANOTHER_TURN_RUNNING_ERROR_MARKER); -} - -/** - * 用户点了"终止"以后,正在 await 的回合命令会带着 app-server 的中断原因返回 - * (`Codex app-server turn 已中断`)。这类错误是用户主动取消,不是失败:界面要给 - * "已终止本次回合"而不是把中断当作异常写进运行错误与诊断。 - */ -export function isDirectCodexTurnInterruptedError(error: unknown) { - const message = error instanceof Error ? error.message : String(error); - return message.includes('turn 已中断') || message.includes('已终止本次回合'); -} - function isPersistableDirectCodexConversationMessage(message: ChatMessage) { if (!message.runtimeOwned) { return false; @@ -408,20 +136,6 @@ function isPersistableDirectCodexConversationMessage(message: ChatMessage) { return /^[a-z0-9][a-z0-9-]{5,159}$/iu.test(turnId); } -function claimInitialSupervisorMessageForPage(projectPath: string, scope = '') { - let claimedProjectPaths = initialSupervisorMessageClaimsByPage.get(window); - if (!claimedProjectPaths) { - claimedProjectPaths = new Set(); - initialSupervisorMessageClaimsByPage.set(window, claimedProjectPaths); - } - const claimKey = `${projectPath}\u0000${scope}`; - if (claimedProjectPaths.has(claimKey)) { - return false; - } - claimedProjectPaths.add(claimKey); - return true; -} - /** * 历史回读与"尚未落盘的运行时消息"合并。 * @@ -430,35 +144,8 @@ function claimInitialSupervisorMessageForPage(projectPath: string, scope = '') { * 就会被冲掉(界面上看不到初始需求,但回合其实已经跑起来了)。 * 这里把当前 messages 里"运行时拥有、且回读结果里没有"的消息保留在末尾(它们是最新的)。 */ -function mergeLoadedConversationWithPendingRuntimeMessages( - loaded: ChatMessage[], - current: ChatMessage[], -): ChatMessage[] { - if (current.length === 0) { - return loaded; - } - const loadedIds = new Set( - loaded - .map((message) => message.messageId) - .filter((id): id is string => Boolean(id)), - ); - const loadedTexts = new Set( - loaded.map((message) => `${message.role}\u0000${message.text}`), - ); - const pending = current.filter((message) => { - if (!message.runtimeOwned) { - return false; - } - if (message.messageId) { - return !loadedIds.has(message.messageId); - } - return !loadedTexts.has(`${message.role}\u0000${message.text}`); - }); - return pending.length > 0 ? [...loaded, ...pending] : loaded; -} export { AuthenticatedClient } from './app/AuthenticatedClient'; -export type { PendingCommand } from './app/types'; export { deriveAgentStatusCards, summarizeAgentAudit, @@ -468,31 +155,26 @@ export { isAbsoluteProjectPath } from './features/project-summary/projectSummary export { needsInitializedChatProject, parseRememberInput, - pendingCommandDetail, resolveChatProjectPath, - resolvePendingCommandProjectPath, }; export function WorkspaceLauncher(props: WorkspaceLauncherProps) { - return ; + return ; } type AppProps = { initialProjectPath?: string; initialProjectManifest?: GameCreationAppManifest; initialProjectKind?: LocalProjectKind; - orchestrationMode?: 'single-supervisor' | 'professional-dag'; - projectSupervisorOnly?: boolean; planningStartMode?: boolean; - activeVersionId?: ProjectSupervisorComponentProps['activeVersionId']; - supervisorChatOnly?: boolean; - initialSupervisorMessage?: string; - initialSupervisorMessageClaimScope?: string; + activeVersionId?: ProjectChatComponentProps['activeVersionId']; + initialPlanningPrompt?: string; + initialPlanningPromptClaimScope?: string; initialCreationType?: HomeCreationType | null; initialAttachments?: LauncherImportedAttachment[]; - onDesignFilesImportingChange?: ProjectSupervisorComponentProps['onDesignFilesImportingChange']; - playRequest?: ProjectSupervisorComponentProps['playRequest']; - onPlayRequestHandled?: ProjectSupervisorComponentProps['onPlayRequestHandled']; + onDesignFilesImportingChange?: ProjectChatComponentProps['onDesignFilesImportingChange']; + playRequest?: ProjectChatComponentProps['playRequest']; + onPlayRequestHandled?: ProjectChatComponentProps['onPlayRequestHandled']; onManifestChange?: ( projectPath: string, manifest: GameCreationAppManifest, @@ -505,40 +187,25 @@ type AppProps = { onAgentResultsChange?: (results: ProjectAgentResultSummary[]) => void; }; +/** + * 工作台壳里由立项策划链路接管的提交入参。 + * + * DirectProject 的回合入参(`@` 引用、附件、权限确认重跑)归聊天容器自己的 + * `DirectProjectTurnInput`,不再从这里穿过。 + */ type ExecuteChatAgentReplyInput = { prompt: string; clientTurnId?: string; - creationType?: HomeCreationType | null; - attachments?: DirectCodexTurnAttachment[]; - directPolicyChecked?: boolean; - references?: ChatReference[]; - userItem?: ReturnType; }; -/** - * `conversation.write` 策略确认后重跑同一轮直连回合的入参。 - * - * 确认弹窗会在**同一轮输入**上二次进入 `executeChatAgentReply`。这里从首轮的入参整体派生, - * 而不是手写一遍字段:一旦重跑时漏掉某一项(历史缺陷就是漏了 `references`), - * 用户在确认之后拿到的就不是他原本提交的那一轮——`@` 引用会被静默丢掉。 - */ -export function directCodexPolicyRetryInput( - input: ExecuteChatAgentReplyInput, -): ExecuteChatAgentReplyInput { - return { ...input, directPolicyChecked: true }; -} - export function App({ initialProjectPath: initialProjectPathOverride = '', initialProjectManifest, initialProjectKind = 'web', - orchestrationMode = 'professional-dag', - projectSupervisorOnly = false, planningStartMode = false, activeVersionId = null, - supervisorChatOnly = false, - initialSupervisorMessage = '', - initialSupervisorMessageClaimScope = '', + initialPlanningPrompt = '', + initialPlanningPromptClaimScope = '', initialCreationType = null, initialAttachments = [], onDesignFilesImportingChange, @@ -549,13 +216,13 @@ export function App({ onAgentRuntimeSummariesChange, onAgentResultsChange, }: AppProps = {}) { - const { setTitle: setWindowTitle } = useWindowChrome(); const [designAgentActive, setDesignAgentActive] = useState(planningStartMode); const designAgentActiveRef = useRef(planningStartMode); designAgentActiveRef.current = designAgentActive; const [designAgentView, setDesignAgentView] = useState( null, ); + const designAgentLaneRef = useRef(planningStartMode); const designAgentTurnRef = useRef<{ projectPath: string; clientTurnId: string; @@ -566,26 +233,28 @@ export function App({ const designAgentEventSubscriptionResolveRef = useRef<(() => void) | null>( null, ); - const directCodexProductRuntime = - DIRECT_CODEX_PRODUCT_RUNTIME && - projectSupervisorOnly && - !supervisorChatOnly && - !planningStartMode && - !designAgentActive; - const [devMode] = useState(() => - projectSupervisorOnly ? false : isDeveloperMode(), - ); + /** + * 本轮策划回合的思考归属。 + * + * 与 `designAgentTurnRef` 分开:那个记录回合本身,回合结束就清空;这个只用来判定 + * 「这条 reasoning 事件是不是本轮的」,回合结束后仍保留,最后一条思考不会在视图 + * 落盘前的空隙里被丢掉。切项目时随其它聊天状态一起清空。 + */ + const designAgentReasoningTurnRef = useRef<{ + projectPath: string; + clientTurnId: string; + } | null>(null); + // 项目开发工作台的普通项目固定使用 DirectProject,立项策划项目走策划聊天;两条 + // 链路各自独立,不通过兼容分支互相承载。 + const directProjectMode = !designAgentActive; const [initialProjectPath] = useState( () => initialProjectPathOverride || readInitialProjectPath(), ); - const eagerSupervisorProject = - projectSupervisorOnly && Boolean(initialProjectPath); + const eagerProject = Boolean(initialProjectPath); const [projectPath, setProjectPath] = useState(initialProjectPath); - const [workspaceProjectKind, setWorkspaceProjectKind] = - useState(initialProjectKind); const [localProject, setLocalProject] = useState(() => - eagerSupervisorProject + eagerProject ? { projectPath: initialProjectPath, manifestPath: `${initialProjectPath.replace(/[\\/]+$/, '')}/.agent/manifest.json`, @@ -596,12 +265,13 @@ export function App({ const localProjectPathRef = useRef(null); useEffect(() => { - if (supervisorChatOnly) return; const nextProjectPath = localProject?.projectPath ?? null; const previousProjectPath = localProjectPathRef.current; localProjectPathRef.current = nextProjectPath; // 未绑定项目时无需触发插件宿主;这也避免启动空首页时产生无意义的 Tauri 调用。 if (!nextProjectPath && !previousProjectPath) return; + // 工程类型只决定「有哪几个编辑器插件可用」,不影响要不要尝试拉起:可用性由插件宿主 + // 自己判(装配好的编辑器插件各自启动),未就绪时给出统一文案。 let active = true; void setAgcPluginProjectPath(nextProjectPath) .then(async () => { @@ -626,7 +296,7 @@ export function App({ } localProjectPathRef.current = null; }; - }, [localProject?.projectPath, supervisorChatOnly]); + }, [localProject?.projectPath]); const manifestRefreshMountedRef = useRef(true); const manifestRefreshStatesRef = useRef( @@ -665,15 +335,10 @@ export function App({ if (JSON.stringify(snapshot) === JSON.stringify(current)) return; setManifest(snapshot); }, [initialProjectManifest]); - const [projectStatus, setProjectStatus] = useState( - eagerSupervisorProject ? '已初始化' : '未初始化', - ); - const [preview, setPreview] = useState(null); - const [previewStatus, setPreviewStatus] = useState('未启动'); - const initialSupervisorMessageLatchRef = useRef({ + const initialPlanningPromptLatchRef = useRef({ projectPath: initialProjectPath, - prompt: initialSupervisorMessage.trim(), - claimScope: initialSupervisorMessageClaimScope, + prompt: initialPlanningPrompt.trim(), + claimScope: initialPlanningPromptClaimScope, creationType: initialCreationType, attachments: toDirectCodexTurnAttachments(initialAttachments), }); @@ -683,7 +348,6 @@ export function App({ nextPreview: LocalPreviewResult | null, status: GameCreationAppPreviewStatus = nextPreview ? 'running' : 'stopped', ) { - setPreview(nextPreview); onPreviewChange?.( nextPreview ? { @@ -695,97 +359,9 @@ export function App({ ); } - const [chatInput, setChatInput] = useState(() => - supervisorChatOnly && initialProjectPath - ? readSupervisorChatDraft(initialProjectPath) - : '', - ); - const [chatReferences, setChatReferences] = useState([]); - /** - * 输入盒待发送附件(direct-codex 回合附件):上传成功后先生成 chip,随下次提交一起 - * 交给 `chat_with_game_creator_direct_codex` 的 `attachments`。附件只存在于前端状态, - * 提交后即清空——后端协议不变。 - */ - const [chatAttachments, setChatAttachments] = useState< - DirectCodexTurnAttachment[] - >([]); - const [chatAttachmentNotice, setChatAttachmentNotice] = useState(''); - const [chatFilesImporting, setChatFilesImporting] = useState(false); - /** 回合运行中再次发送的消息:FIFO 本地队列,当前回合结束后依次发出。 */ - const [chatTurnQueue, setChatTurnQueue] = useState([]); - const chatTurnQueueRef = useRef([]); - chatTurnQueueRef.current = chatTurnQueue; - const [chatComposerNotice, setChatComposerNotice] = useState(''); - const [directCodexTurnCancelling, setDirectCodexTurnCancelling] = - useState(false); - const queuedChatTurnSequenceRef = useRef(0); - const [chatContent, setChatContent] = useState( - [], - ); const chatComposerRef = useRef(null); - /** - * 切项目即清空只属于上一个项目的输入盒状态:待发附件的 `localPath` 是**项目相对**的, - * 队列也属于刚结束的那条对话;留着会把 A 项目的附件路径带进 B 项目的下一个回合。 - */ - useEffect(() => { - setChatAttachments([]); - setChatContent([]); - setChatAttachmentNotice(''); - setChatComposerNotice(''); - setChatTurnQueue([]); - chatTurnQueueRef.current = []; - }, [localProject?.projectPath]); const [chatAgentBusy, setChatAgentBusy] = useState(false); - const directCodexConversationTurnSequenceRef = useRef(0); - /** - * DirectProject 聊天真相源:历史切片与运行态事件归并成同一份条目。 - * - * 运行态只来自 Thread Manager(`subscribe` 的 bootstrap + `notify` 唤醒的 `consume`); - * 前端不再订阅 DirectRuntime 的回合进度事件,也不再各存一份回合流 / 工具卡片。 - */ - const [directThreadChat, setDirectThreadChat] = - useState(() => emptyDirectThreadChatState()); - /** 最新回合是否在跑:历史里留下的半截回合一律按已结束渲染。 */ - const directTurnRunning = directThreadChat.turnRunning; - /** 界面忙碌判定:本地正在跑这次 invoke,或订阅告诉还有一条回合没结束。 */ - const supervisorChatBusy = - chatAgentBusy || (directCodexProductRuntime && directTurnRunning); - const chatAgentBusyRef = useRef(chatAgentBusy); - const directTurnRunningRef = useRef(directTurnRunning); - chatAgentBusyRef.current = chatAgentBusy; - directTurnRunningRef.current = directTurnRunning; - const previousDirectTurnRunningRef = useRef(directTurnRunning); - const directTurnCompletionPendingRef = useRef(false); - const [projectSupervisorSessionId, setProjectSupervisorSessionId] = useState< - string | null - >(null); - - function createDirectCodexConversationTurnId() { - directCodexConversationTurnSequenceRef.current += 1; - let randomId = ''; - try { - randomId = globalThis.crypto?.randomUUID?.().trim() ?? ''; - } catch { - // The timestamp and in-page sequence remain unique enough for a local - // conversation append when WebView crypto is unavailable. - } - if (/^[a-z0-9][a-z0-9-]{5,159}$/iu.test(randomId)) { - return randomId; - } - return `${Date.now().toString(36)}-${directCodexConversationTurnSequenceRef.current.toString(36)}`; - } - - /** 换项目 / 清空对话:聊天条目回到初始态(运行态与已加载历史一起清掉)。 */ - function resetDirectThreadChat() { - setDirectThreadChat(emptyDirectThreadChatState()); - } - - const [projectSupervisorRuntime, setProjectSupervisorRuntime] = - useState(null); - const [projectSupervisorResponseStream, setProjectSupervisorResponseStream] = - useState(null); - const [projectSupervisorRuntimeError, setProjectSupervisorRuntimeError] = - useState(''); + const [projectChatError, setProjectChatError] = useState(''); const [designAgentTransientReply, setDesignAgentTransientReplyVisible] = useState(''); const designAgentTransientReplyTargetRef = useRef(''); @@ -796,11 +372,6 @@ export function App({ view: DesignView; } | null>(null); const [designAgentReasoning, setDesignAgentReasoning] = useState(''); - const designAgentReasoningTurnRef = useRef<{ - projectPath: string; - clientTurnId: string; - text: string; - } | null>(null); function setDesignAgentTransientReplyTarget(next: string) { designAgentTransientReplyTargetRef.current = next; @@ -855,6 +426,83 @@ export function App({ return () => window.clearInterval(timer); }, []); + /** + * 策划 Agent 的实时事件流:本轮流式正文、思考过程和回合中途的视图都靠它推给界面。 + * + * 订阅建立是异步的,而回合由一个 invoke 发起;`designAgentEventSubscriptionReady()` + * 让回合等监听器挂好再开始,避免开头几个事件丢掉。事件只认当前项目;有在跑的回合时 + * 还要认本轮 `clientTurnId`,迟到的上一轮事件不会画到这一轮上。 + */ + useEffect(() => { + const ready = createDesignAgentEventSubscriptionReady(); + if (!canSubscribeTauriEvents() || !designAgentActive) { + resolveDesignAgentEventSubscriptionReady(); + return () => { + if (designAgentEventSubscriptionReadyRef.current === ready) { + designAgentEventSubscriptionReadyRef.current = null; + createDesignAgentEventSubscriptionReady(); + } + }; + } + let cleanup: (() => void) | null = null; + let disposed = false; + void subscribeTauriEvent('design-agent-update', (event) => { + const payload = event.payload; + const tracked = designAgentTurnRef.current; + if ( + payload.projectPath !== localProjectPathRef.current || + (tracked && payload.clientTurnId !== tracked.clientTurnId) + ) { + return; + } + if ( + (payload.kind === 'text' || payload.kind === 'tool') && + payload.text + ) { + setDesignAgentTransientReplyTarget(payload.text); + } + if (payload.reasoningText != null) { + const reasoningTurn = designAgentReasoningTurnRef.current; + if ( + reasoningTurn?.projectPath === payload.projectPath && + reasoningTurn.clientTurnId === payload.clientTurnId + ) { + setDesignAgentReasoning(payload.reasoningText); + } + } + if (payload.view) { + applyDesignAgentViewAfterTransient( + payload.view, + payload.projectPath, + payload.clientTurnId, + ); + } + }) + .then((unlisten) => { + resolveDesignAgentEventSubscriptionReady(); + if (disposed) { + unlisten(); + return; + } + cleanup = unlisten; + }) + .catch(() => { + resolveDesignAgentEventSubscriptionReady(); + }); + return () => { + disposed = true; + cleanup?.(); + resolveDesignAgentEventSubscriptionReady(); + if (designAgentEventSubscriptionReadyRef.current === ready) { + designAgentEventSubscriptionReadyRef.current = null; + createDesignAgentEventSubscriptionReady(); + } + }; + // 订阅只跟着「是否走策划 Agent」这条链路;回调里的项目/回合判据都走 refs, + // 把它们写进依赖会在每轮回复时重订事件。 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [designAgentActive]); + function designMessagesToChat(view: DesignView): ChatMessage[] { const reasoningByMessageId = new Map(); for (const entry of view.reasoningEntries ?? []) { @@ -875,7 +523,7 @@ export function App({ reasoningText: reasoningByMessageId.get(message.id)?.join('\n\n'), updatedAt: Date.now(), })); - const initialPrompt = initialSupervisorMessageLatchRef.current.prompt; + const initialPrompt = initialPlanningPromptLatchRef.current.prompt; if ( initialPrompt && !messages.some( @@ -893,10 +541,11 @@ export function App({ } function applyDesignView(view: DesignView, projectPath: string) { + designAgentLaneRef.current = true; + setDesignAgentView(view); designAgentActiveRef.current = true; setDesignAgentActive(true); - setDesignAgentView(view); - setProjectSupervisorRuntimeError(view.session.lastError ?? ''); + setProjectChatError(view.session.lastError ?? ''); setChatAgentBusy(view.running); const conversation = designMessagesToChat(view); setMessages(conversation); @@ -992,7 +641,7 @@ export function App({ ) { const invoke = resolveTauriInvoke(); if (!invoke) { - setProjectSupervisorRuntimeError('需要在 Tauri App 内运行。'); + setProjectChatError('需要在 Tauri App 内运行。'); return; } designAgentTurnRef.current = { @@ -1002,12 +651,11 @@ export function App({ designAgentReasoningTurnRef.current = { projectPath: nextProjectPath, clientTurnId, - text: '', }; designAgentPendingViewRef.current = null; await designAgentEventSubscriptionReady(); setChatAgentBusy(true); - setProjectSupervisorRuntimeError(''); + setProjectChatError(''); setDesignAgentTransientReplyTarget(''); setDesignAgentReasoning(''); try { @@ -1028,7 +676,7 @@ export function App({ if (isRuntimeConfigMissingError(message)) { requestRuntimeConfigOpen(); } - setProjectSupervisorRuntimeError(message); + setProjectChatError(message); } finally { if (!designAgentPendingViewRef.current) { designAgentTurnRef.current = null; @@ -1038,59 +686,11 @@ export function App({ } } - const [projectSupervisorExpectedRunId, setProjectSupervisorExpectedRunId] = - useState(null); - const chatInputRef = useRef(null); - const supervisorChatMessagesRef = useRef(null); - const supervisorChatShouldFollowLatestRef = useRef(true); - const [assetStatus, setAssetStatus] = useState('未上传'); - const [uploadedAssets, setUploadedAssets] = useState< - UploadLocalAssetResult[] - >([]); - const [assetLocalPath, setAssetLocalPath] = useState('assets/hero.png'); - const [assetKind, setAssetKind] = useState('unknown'); - const [assetMediaType, setAssetMediaType] = useState( - 'application/octet-stream', - ); - const [assetSourceKind, setAssetSourceKind] = useState('generated'); - const [assetCanvasProjectId, setAssetCanvasProjectId] = useState(''); - const [assetResourceId, setAssetResourceId] = useState(''); - const [assetObjectId, setAssetObjectId] = useState(''); - const [assetGenerationPrompt, setAssetGenerationPrompt] = - useState('首版核心美术素材'); - const [canvasExportPath, setCanvasExportPath] = useState( - '/tmp/canvas-export.zip', - ); - const [editorBaseUrl, setEditorBaseUrl] = useState('http://127.0.0.1:3000'); - const [memoryScope, setMemoryScope] = useState('long'); - const [memoryDraft, setMemoryDraft] = useState(''); - const [memoryStatus, setMemoryStatus] = useState('未读取'); - const [limitedCommandStatus, setLimitedCommandStatus] = useState('未运行'); - const [limitedLocalCommands, setLimitedLocalCommands] = useState< - GameCreationAppLimitedRunCommandDescriptor[] - >([...GAME_CREATION_APP_LIMITED_RUN_COMMANDS]); - const [projectFiles, setProjectFiles] = useState([]); - const [projectCheckpoints, setProjectCheckpoints] = useState< - LocalProjectCheckpointSummary[] - >([]); - const [filePath, setFilePath] = useState('game/index.html'); - const [fileDraft, setFileDraft] = useState(''); - const [fileStatus, setFileStatus] = useState('未读取'); - const [agentRunTrace, setAgentRunTrace] = - useState(null); - const [agentRunHistory, setAgentRunHistory] = useState( - [], - ); - const [agentRunHistoryFiles, setAgentRunHistoryFiles] = useState< - LocalProjectFileEntry[] - >([]); - const [agentRunHistoryVisibleCount, setAgentRunHistoryVisibleCount] = - useState(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT); - const [agentRunHistoryOverflowCount, setAgentRunHistoryOverflowCount] = - useState(0); - const [agentRunHistoryLoadingMore, setAgentRunHistoryLoadingMore] = - useState(false); - const [agentRunStatus, setAgentRunStatus] = useState('未运行'); + const [chatFilesImporting, setChatFilesImporting] = useState(false); + const [chatFileImportNotice, setChatFileImportNotice] = useState(''); + const planningChatMessagesRef = useRef(null); + const planningChatShouldFollowLatestRef = useRef(true); + const directProjectChatRef = useRef(null); const [agentRuntimeById, setAgentRuntimeById] = useState< Record >({}); @@ -1098,54 +698,15 @@ export function App({ agentRuntimeByIdRef.current = agentRuntimeById; const [professionalAgentResultsById, setProfessionalAgentResultsById] = useState>({}); - const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false); - const [llmConfigStatus, setLlmConfigStatus] = - useState(null); const [workspaceStatus, setWorkspaceStatus] = useState( - eagerSupervisorProject ? `已打开:${initialProjectPath}` : '请选择工作区', + eagerProject ? `已打开:${initialProjectPath}` : '请选择工作区', ); - const [selectedAgent, setSelectedAgent] = useState( - null, - ); - const selectedAgentIdRef = useRef(null); - selectedAgentIdRef.current = selectedAgent?.id ?? null; - const [agentConversationInput, setAgentConversationInput] = useState(''); - const [agentConversationStatus, setAgentConversationStatus] = - useState('未选择 agent'); - const [agentConversationMessages, setAgentConversationMessages] = useState< - LocalConversationMessageRecord[] - >([]); - const [agentConversationRuntime, setAgentConversationRuntime] = - useState(null); - const [agentConversationSessionId, setAgentConversationSessionId] = useState< - string | null - >(null); - const [agentConversationRunSubmitMode, setAgentConversationRunSubmitMode] = - useState('steer'); - const [agentConversationRuntimeError, setAgentConversationRuntimeError] = - useState(''); - const [agentConversationVisibleCount, setAgentConversationVisibleCount] = - useState(CONVERSATION_INITIAL_VISIBLE_COUNT); - const [agentConversationSaving, setAgentConversationSaving] = useState(false); - const [agentConversationBackgroundBusy, setAgentConversationBackgroundBusy] = - useState(false); - const [agentMemoryStatus, setAgentMemoryStatus] = useState('未读取'); - const [agentMemoryContent, setAgentMemoryContent] = useState(''); const [messages, setMessages] = useState( createDefaultChatMessages, ); const [conversationVisibleCount, setConversationVisibleCount] = useState( CONVERSATION_INITIAL_VISIBLE_COUNT, ); - const [directHistoryHasMore, setDirectHistoryHasMore] = useState(false); - const directHistoryOldestItemIdRef = useRef(null); - const directHistoryLoadingRef = useRef(false); - const directHistoryAnchorGateRef = useRef( - null, - ); - const [pendingCommand, setPendingCommand] = useState( - null, - ); const [pendingUiConfirmation, setPendingUiConfirmation] = useState(null); const [pendingNonEmptyProjectCreate, setPendingNonEmptyProjectCreate] = @@ -1153,17 +714,12 @@ export function App({ projectPath: string; announceToChat: boolean; } | null>(null); - const [commandLog, setCommandLog] = useState([ - `${GAME_CREATION_APP_COMMANDS.length} 个命令已登记权限。`, - ]); - const [projectLogStatus, setProjectLogStatus] = useState('未读取'); - const [projectLogContent, setProjectLogContent] = useState(''); const [conversationWriteVersion, setConversationWriteVersion] = useState(0); const savedConversationCountRef = useRef( - eagerSupervisorProject ? createDefaultChatMessages().length : 0, + eagerProject ? createDefaultChatMessages().length : 0, ); const savedConversationProjectPathRef = useRef( - eagerSupervisorProject ? initialProjectPath : null, + eagerProject ? initialProjectPath : null, ); const projectConversationWriteConfirmedRef = useRef(null); const projectConversationWriteCancelledRef = useRef<{ @@ -1173,19 +729,6 @@ export function App({ const latestMessagesRef = useRef([]); const conversationWriteInFlightRef = useRef(false); const projectScopeVersionRef = useRef(0); - const projectSupervisorHistoryLoadVersionRef = useRef(0); - const projectSupervisorRuntimeResumeProjectPathRef = useRef( - null, - ); - const projectSupervisorSessionIdRef = useRef(null); - projectSupervisorSessionIdRef.current = projectSupervisorSessionId; - const projectSupervisorRuntimeRef = useRef(null); - projectSupervisorRuntimeRef.current = projectSupervisorRuntime; - const projectSupervisorExpectedRunIdRef = useRef(null); - projectSupervisorExpectedRunIdRef.current = projectSupervisorExpectedRunId; - const projectSupervisorResponseStreamRef = - useRef(null); - projectSupervisorResponseStreamRef.current = projectSupervisorResponseStream; const refreshManifest = useCallback( (nextProjectPath = localProjectPathRef.current ?? ''): Promise => { @@ -1255,143 +798,21 @@ export function App({ refreshStates.clear(); }; }, []); - const projectSupervisorRuntimeSyncingRef = useRef(new Set()); - const projectSupervisorRefreshConversationRef = useRef< - | (( - invoke: TauriInvoke, - projectPath: string, - sessionId: string, - ) => Promise) - | null - >(null); const executeChatAgentReplyRef = useRef< (input: ExecuteChatAgentReplyInput) => Promise >(async () => undefined); - const agentConversationSavingRef = useRef(false); - const agentConversationBackgroundBusyRef = useRef(false); - const agentConversationLoadVersionRef = useRef(0); - const agentConversationSessionIdRef = useRef(null); - agentConversationSessionIdRef.current = agentConversationSessionId; const agentRuntimeResumeProjectPathRef = useRef(null); - const agentRunHistoryLoadingMoreRef = useRef(false); const initialProjectOpenedRef = useRef(false); const pendingUiConfirmationActionRef = useRef<(() => void) | null>(null); - const queueRunLocalShortcutRef = useRef<() => void>(() => undefined); const executeRunLocalRef = useRef<(announceToChat: boolean) => void>( () => undefined, ); + const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false); function requestRuntimeConfigOpen() { - if (projectSupervisorOnly && !supervisorChatOnly) { - return; - } setRuntimeConfigOpen(true); } - const updateProjectSupervisorRuntime = useCallback( - ( - runtime: AgentRuntimeState | null, - previous = projectSupervisorRuntimeRef.current, - ) => { - const nextRuntime = runtime - ? normalizeAgentRuntimeState(runtime, previous) - : null; - projectSupervisorRuntimeRef.current = nextRuntime; - setProjectSupervisorRuntime(nextRuntime); - }, - [], - ); - - const updateProjectSupervisorResponseStream = useCallback( - ( - incoming: AgentRuntimeResponseStream | null | undefined, - runtime: AgentRuntimeState, - ) => { - let nextStream = mergeProjectSupervisorResponseStream( - projectSupervisorResponseStreamRef.current, - incoming, - runtime, - ); - const candidateStream = nextStream; - if ( - candidateStream && - latestMessagesRef.current.some( - (message) => - message.runtimeOwned && - message.role === 'assistant' && - message.text === candidateStream.accumulatedText && - typeof message.updatedAt === 'number' && - message.updatedAt >= candidateStream.startedAt, - ) - ) { - // Keep the existing supervisor-chat behavior for restored history. - nextStream = null; - } - projectSupervisorResponseStreamRef.current = nextStream; - setProjectSupervisorResponseStream(nextStream); - }, - [], - ); - - function resetProjectSupervisorState() { - projectSupervisorHistoryLoadVersionRef.current += 1; - projectSupervisorRuntimeResumeProjectPathRef.current = null; - projectSupervisorSessionIdRef.current = null; - projectSupervisorRuntimeRef.current = null; - projectSupervisorExpectedRunIdRef.current = null; - projectSupervisorResponseStreamRef.current = null; - projectSupervisorRuntimeSyncingRef.current.clear(); - setProjectSupervisorSessionId(null); - setProjectSupervisorRuntime(null); - setProjectSupervisorExpectedRunId(null); - setProjectSupervisorResponseStream(null); - setProjectSupervisorRuntimeError(''); - designAgentPendingViewRef.current = null; - designAgentReasoningTurnRef.current = null; - setDesignAgentReasoning(''); - setDesignAgentActive(planningStartMode); - designAgentActiveRef.current = planningStartMode; - designAgentTurnRef.current = null; - setDesignAgentView(null); - } - - function syncTerminalProjectSupervisorConversation( - invoke: TauriInvoke, - projectPath: string, - runtime: AgentRuntimeState, - ) { - if (!isAgentRuntimeTerminalState(runtime) || !runtime.sessionId) { - return; - } - const syncKey = `${projectPath}\n${runtime.sessionId}\n${runtime.runId}`; - const syncAlreadyStarted = - projectSupervisorRuntimeSyncingRef.current.has(syncKey); - if (syncAlreadyStarted) { - return; - } - const refreshConversation = projectSupervisorRefreshConversationRef.current; - if (!refreshConversation) { - return; - } - projectSupervisorRuntimeSyncingRef.current.add(syncKey); - void refreshConversation(invoke, projectPath, runtime.sessionId) - .then(() => {}) - .catch((error) => { - projectSupervisorRuntimeSyncingRef.current.delete(syncKey); - if ( - localProjectPathRef.current === projectPath && - projectSupervisorSessionIdRef.current === runtime.sessionId - ) { - setProjectSupervisorRuntimeError( - `项目总控 Agent 对话刷新失败:${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - }); - } - - useEscapeToClose(closeAgentConversation, selectedAgent !== null); useEscapeToClose(cancelUiCommandConfirmation, pendingUiConfirmation !== null); useEscapeToClose( cancelProjectCreateInNonEmptyFolder, @@ -1415,702 +836,43 @@ export function App({ return; } initialProjectOpenedRef.current = true; - if (projectSupervisorOnly) { - if (directCodexProductRuntime) { - if (!isAbsoluteProjectPath(initialProjectPath)) { - setWorkspaceStatus('请提供工作区绝对路径'); - return; - } - if (projectPathHasControlCharacter(initialProjectPath)) { - setWorkspaceStatus('工作区路径不能包含控制字符'); - return; - } - // This component is mounted inside the already-created project - // workbench. Hydrate localProject before the first direct turn so the - // direct runtime cannot silently create a second workspace. - void openWorkspace( - initialProjectPath, - false, - 'open', - initialProjectKind, - ); - return; - } - if (!isAbsoluteProjectPath(initialProjectPath)) { - setWorkspaceStatus('请提供工作区绝对路径'); - return; - } - if (projectPathHasControlCharacter(initialProjectPath)) { - setWorkspaceStatus('工作区路径不能包含控制字符'); - return; - } - localProjectPathRef.current = initialProjectPath; - void loadProjectConversation(initialProjectPath).finally(() => { - if ( - localProjectPathRef.current === initialProjectPath && - !planningStartMode - ) { - void refreshAgentRunTrace(initialProjectPath); - } - }); + if (!isAbsoluteProjectPath(initialProjectPath)) { + setWorkspaceStatus('请提供工作区绝对路径'); return; } - void openWorkspace( - initialProjectPath, - false, - initialProjectKind !== 'web' ? 'open' : 'create', - initialProjectKind, - ); + if (projectPathHasControlCharacter(initialProjectPath)) { + setWorkspaceStatus('工作区路径不能包含控制字符'); + return; + } + if (directProjectMode) { + // This component is mounted inside the already-created project workbench. + // Hydrate localProject before the first direct turn so the direct runtime + // cannot silently create a second workspace. + void openWorkspace(initialProjectPath, false, 'open', initialProjectKind); + return; + } + localProjectPathRef.current = initialProjectPath; + void loadProjectConversation(initialProjectPath).finally(() => { + if ( + localProjectPathRef.current === initialProjectPath && + !planningStartMode + ) { + void refreshAgentRunTrace(initialProjectPath); + } + }); // Initial project opening is guarded by initialProjectOpenedRef. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialProjectPath, projectSupervisorOnly]); - - useEffect(() => { - if (!directCodexProductRuntime || !supervisorChatBusy) { - return; - } - const timer = window.setInterval(() => { - void requestPlatformSessionRefresh().catch(() => { - // The active DirectProject turn will surface the original auth error; - // keepalive must not replace it with an unrelated background error. - }); - }, DIRECT_CODEX_SESSION_KEEPALIVE_MS); - return () => window.clearInterval(timer); - }, [supervisorChatBusy, directCodexProductRuntime]); - - /** - * DirectProject 聊天真相源。 - * - * `subscribe` 的 bootstrap 就是此刻要处理的事件(游标已在队尾),此后只由 - * `game-creator-direct-thread-notify` 唤醒 `consume`;前端不再订阅 DirectRuntime 的回合 - * 进度事件,也不轮询"有没有回合在跑"。 - */ - useEffect(() => { - if (!directCodexProductRuntime) { - return; - } - const projectPath = localProject?.projectPath ?? null; - const directInvoke = resolveTauriInvoke(); - // 闸门先建(或复用首屏读取已经开好的那道):同一轮渲染里首屏会等订阅回执里的锚点。 - const anchorGate = reuseOrOpenDirectHistoryAnchorGate( - directHistoryAnchorGateRef.current, - projectPath ?? '', - ); - directHistoryAnchorGateRef.current = anchorGate; - if (!projectPath || !directInvoke || !canSubscribeTauriEvents()) { - // 订阅不可用:首屏退化成"取文件尾",不阻塞加载。 - anchorGate.settle(null); - return; - } - let disposed = false; - let cleanup: (() => void) | null = null; - let subscriptionId: string | null = null; - let consuming = false; - let consumeAgain = false; - // 通知先于 subscribe 回执到达时,前端还不知道自己的 subscriptionId,没法立刻 consume。 - // 记一笔欠账,拿到回执后立刻补一次,事件就不会卡在队伍里等下一次通知。 - let notifyBeforeSubscription = false; - - const bootstrap = async () => { - const result = await directInvoke( - 'subscribe_direct_project_thread', - { projectPath }, - ); - if (disposed) { - anchorGate.settle(null); - return; - } - subscriptionId = result.subscriptionId; - // 首屏边界:回执里这一刻的最后一条已完成条目(含该条)。 - anchorGate.settle(result.lastCompletedItemId ?? null); - setDirectThreadChat((state) => - resolveDirectThreadBootstrap(state, result), - ); - if (notifyBeforeSubscription) { - notifyBeforeSubscription = false; - void consume(); - } - }; - const consume = async () => { - if (!subscriptionId || disposed) return; - if (consuming) { - consumeAgain = true; - return; - } - consuming = true; - try { - do { - consumeAgain = false; - const result = await directInvoke( - 'consume_direct_project_thread', - { subscriptionId }, - ); - if (disposed) return; - setDirectThreadChat((state) => - applyDirectThreadConsumeResult(state, result), - ); - } while (consumeAgain && !disposed); - } catch (error) { - // 订阅被别人顶掉时重订一次;通知允许丢,下一次通知会再唤醒。 - if (!disposed && String(error).includes('SUBSCRIPTION_EXPIRED')) { - subscriptionId = null; - try { - await bootstrap(); - } catch { - /* 让下一次通知再试。 */ - } - } - } finally { - consuming = false; - } - }; - const setup = async () => { - try { - const unlisten = await subscribeTauriEvent<{ subscriptionId: string }>( - 'game-creator-direct-thread-notify', - (event) => { - if (event.payload.subscriptionId === subscriptionId) { - void consume(); - return; - } - if (!subscriptionId) notifyBeforeSubscription = true; - }, - ); - if (disposed) { - unlisten(); - return; - } - cleanup = unlisten; - await bootstrap(); - } catch { - // 历史仍可使用;订阅失败不伪造忙碌态,锚点缺失时首屏按文件尾取尾屏。 - anchorGate.settle(null); - } - }; - // 换项目就是换一份聊天:先回到初始态,再订阅新线程。 - setDirectThreadChat(emptyDirectThreadChatState()); - void setup(); - return () => { - disposed = true; - cleanup?.(); - // 首屏可能还在等这道闸门(例如切项目打断了订阅),不能让它永远等下去。 - anchorGate.settle(null); - }; - }, [directCodexProductRuntime, localProject?.projectPath]); - - useEffect(() => { - if (projectSupervisorOnly && !directCodexProductRuntime) { - return; - } - if (!canSubscribeTauriEvents()) { - return; - } - let cleanup: (() => void) | null = null; - let disposed = false; - void subscribeTauriEvent( - 'game-creator-agent-progress', - (event) => { - if (event.payload.projectPath !== localProjectPathRef.current) { - return; - } - if (directCodexProductRuntime) { - // DirectProject 的进度已经从线程事件流进聊天条目,这条通用进度事件不再参与。 - return; - } - setMessages((current) => [ - ...current, - { role: 'assistant', text: event.payload.message }, - ]); - }, - ) - .then((unlisten) => { - if (disposed) { - unlisten(); - return; - } - cleanup = unlisten; - }) - .catch((error) => { - if (disposed) { - return; - } - setAgentRunStatus( - `实时状态不可用:${ - error instanceof Error ? error.message : String(error) - }`, - ); - }); - return () => { - disposed = true; - cleanup?.(); - }; - }, [directCodexProductRuntime, projectSupervisorOnly]); - - useEffect(() => { - const invoke = resolveTauriInvoke(); - if ( - !canSubscribeTauriEvents() || - directCodexProductRuntime || - designAgentActive - ) { - return; - } - let cleanup: (() => void) | null = null; - let disposed = false; - void subscribeTauriEvent( - 'game-creator-agent-runtime-update', - (event) => { - const payload = event.payload; - if (payload.projectPath !== localProjectPathRef.current) { - return; - } - if (payload.manifestInvalidated) { - void refreshManifest(payload.projectPath); - } - const nextRuntime = agentRuntimeStateFromResult(payload.runtime); - if (payload.agentId === PROJECT_SUPERVISOR_AGENT_ID) { - const expectedSessionId = projectSupervisorSessionIdRef.current; - const currentRuntime = projectSupervisorRuntimeRef.current; - const expectedRunId = projectSupervisorExpectedRunIdRef.current; - const sameRun = - currentRuntime !== null && - sameAgentRuntimeRun(nextRuntime, currentRuntime); - const isExpectedRun = - expectedRunId !== null && nextRuntime.runId === expectedRunId; - if ( - nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID || - nextRuntime.runId !== payload.runId || - (expectedSessionId !== null && - nextRuntime.sessionId !== expectedSessionId) || - (currentRuntime !== null && - ((!sameRun && !isExpectedRun) || - (sameRun && nextRuntime.updatedAt < currentRuntime.updatedAt))) - ) { - return; - } - if (expectedSessionId === null && nextRuntime.sessionId) { - projectSupervisorSessionIdRef.current = nextRuntime.sessionId; - setProjectSupervisorSessionId(nextRuntime.sessionId); - } - updateProjectSupervisorRuntime(nextRuntime); - if (isExpectedRun) { - projectSupervisorExpectedRunIdRef.current = null; - setProjectSupervisorExpectedRunId(null); - } - updateProjectSupervisorResponseStream( - payload.runtime.responseStream, - nextRuntime, - ); - setProjectSupervisorRuntimeError(''); - if (invoke) { - syncTerminalProjectSupervisorConversation( - invoke, - payload.projectPath, - nextRuntime, - ); - } - } - rememberAgentRuntimeState(nextRuntime); - if (payload.agentId !== selectedAgentIdRef.current) { - return; - } - if ( - agentConversationSessionIdRef.current !== null && - payload.runtime.state.sessionId !== - agentConversationSessionIdRef.current - ) { - return; - } - setAgentConversationRuntime((current) => - normalizeAgentRuntimeState(nextRuntime, current), - ); - setAgentConversationRuntimeError(''); - }, - ) - .then((unlisten) => { - if (disposed) { - unlisten(); - return; - } - cleanup = unlisten; - }) - .catch((error) => { - if (disposed) { - return; - } - setAgentConversationRuntimeError( - `Runtime 订阅不可用:${ - error instanceof Error ? error.message : String(error) - }`, - ); - }); - return () => { - disposed = true; - cleanup?.(); - }; - // The subscription deliberately invokes the current render's helper; its - // mutable state is read through refs, so resubscribing for that helper is - // neither necessary nor safe. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - directCodexProductRuntime, - designAgentActive, - refreshManifest, - updateProjectSupervisorResponseStream, - updateProjectSupervisorRuntime, - ]); - - useEffect(() => { - const ready = createDesignAgentEventSubscriptionReady(); - if (!canSubscribeTauriEvents() || !designAgentActive) { - resolveDesignAgentEventSubscriptionReady(); - return () => { - if (designAgentEventSubscriptionReadyRef.current === ready) { - designAgentEventSubscriptionReadyRef.current = null; - createDesignAgentEventSubscriptionReady(); - } - }; - } - let cleanup: (() => void) | null = null; - let disposed = false; - void subscribeTauriEvent('design-agent-update', (event) => { - const payload = event.payload; - const tracked = designAgentTurnRef.current; - if ( - payload.projectPath !== localProjectPathRef.current || - (tracked && payload.clientTurnId !== tracked.clientTurnId) - ) { - return; - } - if (payload.kind === 'text' && payload.text != null) { - setDesignAgentTransientReplyTarget(payload.text); - } - if (payload.reasoningText != null) { - const reasoningTurn = designAgentReasoningTurnRef.current; - if ( - reasoningTurn && - reasoningTurn.projectPath === payload.projectPath && - reasoningTurn.clientTurnId === payload.clientTurnId - ) { - reasoningTurn.text = payload.reasoningText; - setDesignAgentReasoning(payload.reasoningText); - } - } - if (payload.kind === 'tool' && payload.text) { - setDesignAgentTransientReplyTarget(payload.text); - } - if (payload.view) { - applyDesignAgentViewAfterTransient( - payload.view, - payload.projectPath, - payload.clientTurnId, - ); - } - }) - .then((unlisten) => { - resolveDesignAgentEventSubscriptionReady(); - if (disposed) { - unlisten(); - return; - } - cleanup = unlisten; - }) - .catch(() => { - resolveDesignAgentEventSubscriptionReady(); - }); - return () => { - disposed = true; - cleanup?.(); - resolveDesignAgentEventSubscriptionReady(); - if (designAgentEventSubscriptionReadyRef.current === ready) { - designAgentEventSubscriptionReadyRef.current = null; - createDesignAgentEventSubscriptionReady(); - } - }; - // applyDesignView 读的是 refs 和当前项目路径, - // 把它写进依赖会在每轮回复时重订事件。 - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [designAgentActive]); - - useEffect(() => { - if (!canSubscribeTauriEvents()) { - return; - } - let cleanup: (() => void) | null = null; - let disposed = false; - void subscribeTauriEvent( - 'game-creator-manifest-invalidated', - (event) => { - const activeProjectPath = localProjectPathRef.current; - if ( - !activeProjectPath || - !projectPathsMatchForInvalidation( - event.payload.projectPath, - activeProjectPath, - ) - ) { - return; - } - void refreshManifest(activeProjectPath); - }, - ) - .then((unlisten) => { - if (disposed) { - unlisten(); - return; - } - cleanup = unlisten; - }) - .catch(() => { - // In-process Runtime events continue to carry the same invalidation signal. - }); - return () => { - disposed = true; - cleanup?.(); - }; - }, [refreshManifest]); - - useEffect(() => { - const invoke = resolveTauriInvoke(); - const nextProjectPath = localProject?.projectPath ?? null; - const sessionId = projectSupervisorSessionId; - const trackedRunId = - projectSupervisorExpectedRunId ?? projectSupervisorRuntime?.runId ?? null; - if ( - !invoke || - directCodexProductRuntime || - designAgentActive || - !nextProjectPath || - !sessionId || - !trackedRunId || - (!projectSupervisorRuntime && !projectSupervisorExpectedRunId) || - (projectSupervisorRuntime !== null && - isAgentRuntimeTerminalState(projectSupervisorRuntime) && - !projectSupervisorExpectedRunId) - ) { - return; - } - let disposed = false; - let inFlight = false; - const pollRuntime = async () => { - if (disposed || inFlight) { - return; - } - inFlight = true; - const runtimeBeforePoll = projectSupervisorRuntimeRef.current; - const responseStreamBeforePoll = - projectSupervisorResponseStreamRef.current; - try { - const result = await invoke( - 'read_game_creator_agent_runtime', - { - projectPath: nextProjectPath, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - sessionId, - }, - ); - const currentRuntime = projectSupervisorRuntimeRef.current; - if ( - disposed || - currentRuntime !== runtimeBeforePoll || - projectSupervisorResponseStreamRef.current !== - responseStreamBeforePoll || - localProjectPathRef.current !== nextProjectPath || - projectSupervisorSessionIdRef.current !== sessionId - ) { - return; - } - const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime); - if ( - nextRuntime.sessionId !== sessionId || - nextRuntime.runId !== trackedRunId || - (currentRuntime !== null && - sameAgentRuntimeRun(nextRuntime, currentRuntime) && - nextRuntime.updatedAt < currentRuntime.updatedAt) - ) { - return; - } - updateProjectSupervisorRuntime(nextRuntime); - if (nextRuntime.runId === projectSupervisorExpectedRunIdRef.current) { - projectSupervisorExpectedRunIdRef.current = null; - setProjectSupervisorExpectedRunId(null); - } - updateProjectSupervisorResponseStream( - result.responseStream, - nextRuntime, - ); - setProjectSupervisorRuntimeError(''); - syncTerminalProjectSupervisorConversation( - invoke, - nextProjectPath, - nextRuntime, - ); - } catch (error) { - if ( - !disposed && - localProjectPathRef.current === nextProjectPath && - projectSupervisorSessionIdRef.current === sessionId - ) { - const message = - error instanceof Error ? error.message : String(error); - if (isRuntimeConfigMissingError(message)) { - requestRuntimeConfigOpen(); - } - setProjectSupervisorRuntimeError(message); - } - } finally { - inFlight = false; - } - }; - void pollRuntime(); - const timer = window.setInterval(() => { - void pollRuntime(); - }, 750); - return () => { - disposed = true; - window.clearInterval(timer); - }; - // Polling restarts only when the tracked runtime identity or phase changes. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - localProject?.projectPath, - directCodexProductRuntime, - designAgentActive, - projectSupervisorSessionId, - projectSupervisorRuntime?.phase, - projectSupervisorRuntime?.runId, - projectSupervisorRuntime?.status, - projectSupervisorExpectedRunId, - ]); - - useEffect(() => { - const invoke = resolveTauriInvoke(); - const nextProjectPath = localProject?.projectPath ?? null; - const supervisorRunId = projectSupervisorRuntime?.runId ?? null; - if ( - !projectSupervisorOnly || - directCodexProductRuntime || - designAgentActive || - !invoke || - !nextProjectPath || - !supervisorRunId - ) { - return; - } - let disposed = false; - let inFlight = false; - const pollProfessionalRuntimes = async () => { - if (disposed || inFlight) { - return; - } - inFlight = true; - try { - const runtimes = await invoke( - 'read_game_creator_agent_runtimes', - { projectPath: nextProjectPath }, - ); - if ( - disposed || - localProjectPathRef.current !== nextProjectPath || - projectSupervisorRuntimeRef.current?.runId !== supervisorRunId - ) { - return; - } - const nextRuntimes = runtimes.map((runtimeResult) => - agentRuntimeStateFromResult(runtimeResult), - ); - const authoritativeProfessionalAgentIds = new Set( - nextRuntimes - .filter( - (runtime) => - ['agent-delegate', 'agent-delegate-retry'].includes( - runtime.source, - ) && - runtime.parentAgentId === PROJECT_SUPERVISOR_AGENT_ID && - runtime.parentRunId === supervisorRunId, - ) - .map((runtime) => runtime.agentId), - ); - setAgentRuntimeById((current) => { - const reconciled = { ...current }; - for (const [key, runtime] of Object.entries(reconciled)) { - if ( - runtime && - ['agent-delegate', 'agent-delegate-retry'].includes( - runtime.source, - ) && - runtime.parentAgentId === PROJECT_SUPERVISOR_AGENT_ID && - runtime.parentRunId === supervisorRunId && - !authoritativeProfessionalAgentIds.has(runtime.agentId) - ) { - delete reconciled[key]; - } - } - return nextRuntimes.reduce( - (next, runtime) => - mergeAgentRuntimeStateIntoMap(next, runtime, true), - reconciled, - ); - }); - } catch { - // Preserve the last truthful professional Runtime snapshot on transient read failures. - } finally { - inFlight = false; - } - }; - void pollProfessionalRuntimes(); - const timer = window.setInterval(() => { - void pollProfessionalRuntimes(); - }, 1000); - return () => { - disposed = true; - window.clearInterval(timer); - }; - // Track the stable projection fields; the Runtime object identity changes on every merge. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - localProject?.projectPath, - directCodexProductRuntime, - designAgentActive, - projectSupervisorOnly, - projectSupervisorRuntime?.phase, - projectSupervisorRuntime?.runId, - projectSupervisorRuntime?.status, - ]); + }, [initialProjectPath]); useLayoutEffect(() => { - if ( - (!supervisorChatOnly && - !( - projectSupervisorOnly && - (directCodexProductRuntime || designAgentActive) - )) || - !supervisorChatShouldFollowLatestRef.current - ) { + if (!planningChatShouldFollowLatestRef.current) { return; } - const messageList = supervisorChatMessagesRef.current; + const messageList = planningChatMessagesRef.current; if (messageList) { messageList.scrollTop = messageList.scrollHeight; } - }, [ - messages, - projectSupervisorResponseStream?.sequence, - projectSupervisorRuntime?.updatedAt, - projectSupervisorRuntimeError, - directThreadChat, - directCodexProductRuntime, - designAgentActive, - projectSupervisorOnly, - supervisorChatOnly, - ]); - - useEffect(() => { - if (!supervisorChatOnly) { - return; - } - persistSupervisorChatDraft(initialProjectPath, chatInput); - }, [chatInput, initialProjectPath, supervisorChatOnly]); + }, [messages, projectChatError]); useEffect(() => { latestMessagesRef.current = messages; @@ -2127,7 +889,7 @@ export function App({ } // DirectProject history is written by Rust from raw app-server items. // The browser only renders that projection and must not append chat rows. - if (projectSupervisorOnly && directCodexProductRuntime) { + if (directProjectMode) { // `messages` is only an optimistic UI projection in Direct mode; it is // intentionally not proof of durability. Rust owns the raw response // history, so this effect must not route these rows through the generic @@ -2141,7 +903,6 @@ export function App({ return; } if ( - projectSupervisorOnly && pendingMessages.every( (message) => (message.runtimeOwned && @@ -2206,7 +967,6 @@ export function App({ } conversationWriteInFlightRef.current = true; void (async () => { - let wroteMessage = false; for (const [index, message] of pendingMessages.entries()) { if ( message.runtimeOwned && @@ -2235,7 +995,6 @@ export function App({ }, }, ); - wroteMessage = true; savedConversationCountRef.current = start + index + 1; } setWorkspaceStatus((current) => @@ -2244,9 +1003,6 @@ export function App({ ? `已打开:${nextProjectPath}` : current, ); - if (wroteMessage) { - setCommandLog((current) => [...current, 'conversation.write']); - } })() .catch((error) => { savedConversationCountRef.current = Math.min( @@ -2258,12 +1014,6 @@ export function App({ error instanceof Error ? error.message : String(error) }`, ); - setCommandLog((current) => [ - ...current, - `conversation.write.failed ${ - error instanceof Error ? error.message : String(error) - }`, - ]); }) .finally(() => { conversationWriteInFlightRef.current = false; @@ -2281,8 +1031,7 @@ export function App({ messages, conversationWriteVersion, pendingUiConfirmation, - projectSupervisorOnly, - directCodexProductRuntime, + directProjectMode, ]); function appendLocalPermissionLog( @@ -2298,39 +1047,17 @@ export function App({ if (!invoke || !projectPath) { return; } - let result: Promise; try { - result = Promise.resolve( + void Promise.resolve( invoke('append_local_permission_log', { projectPath, event, commandId, }), - ); - } catch (error) { - setCommandLog((current) => [ - ...current, - `permission.log.failed ${commandId}: ${ - error instanceof Error ? error.message : String(error) - }`, - ]); - return; + ).catch(() => undefined); + } catch { + // 权限日志只是审计旁路,写失败不能阻塞对话。 } - void result.catch((error) => { - setCommandLog((current) => [ - ...current, - `permission.log.failed ${commandId}: ${ - error instanceof Error ? error.message : String(error) - }`, - ]); - }); - } - - function resolvePermissionLogProjectPath(command: PendingCommand) { - if (command.id === 'project.create') { - return null; - } - return resolveChatProjectPath(localProject); } function resolveUiPermissionLogProjectPath( @@ -2361,49 +1088,6 @@ export function App({ return draftProjectPath; } - function queuePendingCommand(command: PendingCommand) { - setPendingCommand(command); - setCommandLog((current) => [ - ...current, - `permission.pending ${command.id}`, - ]); - appendLocalPermissionLog( - resolvePermissionLogProjectPath(command), - 'permission.pending', - command.id, - ); - } - - function requestCommandConfirmation( - commandId: GameCreationAppCommandDescriptor['id'], - detail: string, - onConfirm: () => void, - ) { - const permission = GAME_CREATION_APP_COMMANDS.find( - (command) => command.id === commandId, - )?.permission as GameCreationAppPermission | undefined; - - if (permission === 'deny' || !permission) { - setCommandLog((current) => [...current, `permission.deny ${commandId}`]); - return; - } - if (permission === 'confirm') { - pendingUiConfirmationActionRef.current = onConfirm; - setPendingUiConfirmation({ commandId, detail, projectPath: null }); - setCommandLog((current) => [ - ...current, - `permission.pending ${commandId}`, - ]); - appendLocalPermissionLog( - resolveUiPermissionLogProjectPath(commandId), - 'permission.pending', - commandId, - ); - return; - } - onConfirm(); - } - function requestProjectPolicyConfirmation( commandId: GameCreationAppCommandDescriptor['id'], projectPath: string, @@ -2412,7 +1096,6 @@ export function App({ ) { pendingUiConfirmationActionRef.current = onConfirm; setPendingUiConfirmation({ commandId, detail, projectPath }); - setCommandLog((current) => [...current, `permission.pending ${commandId}`]); appendLocalPermissionLog(projectPath, 'permission.pending', commandId); } @@ -2420,33 +1103,13 @@ export function App({ commandId: GameCreationAppCommandDescriptor['id'], message: string, ) { - if (commandId.startsWith('project.') || commandId === 'task.list') { - setProjectStatus(message); - setWorkspaceStatus(message); - } if ( - commandId.startsWith('file.') || - commandId === 'agent.trace_read' || - commandId === 'project.index' || - commandId === 'project.diff' || - commandId === 'project.export_package' || - commandId === 'project.export_list' + commandId.startsWith('project.') || + commandId === 'task.list' || + commandId.startsWith('preview.') ) { - setFileStatus(message); - } - if (commandId.startsWith('asset.') || commandId.startsWith('canvas.')) { - setAssetStatus(message); - } - if (commandId.startsWith('preview.')) { - setPreviewStatus(message); setWorkspaceStatus(message); } - if (commandId.startsWith('agent.')) { - setAgentRunStatus(message); - } - if (commandId.startsWith('memory.')) { - setMemoryStatus(message); - } } async function queueProjectPolicyConfirmationIfNeeded( @@ -2464,7 +1127,6 @@ export function App({ if (policyView.policy.deniedCommands.includes(commandId)) { const message = `项目权限策略拒绝执行:${commandId}`; markProjectPolicyDenied(commandId, message); - setCommandLog((current) => [...current, `permission.deny ${commandId}`]); setMessages((current) => [ ...current, { role: 'assistant', text: message }, @@ -2500,7 +1162,6 @@ export function App({ } const message = `项目权限策略拒绝执行:${commandId}`; markProjectPolicyDenied(commandId, message); - setCommandLog((current) => [...current, `permission.deny ${commandId}`]); setMessages((current) => [ ...current, { role: 'assistant', text: message }, @@ -2536,10 +1197,6 @@ export function App({ const action = pendingUiConfirmationActionRef.current; pendingUiConfirmationActionRef.current = null; setPendingUiConfirmation(null); - setCommandLog((current) => [ - ...current, - `permission.confirm ${pending.commandId}`, - ]); appendLocalPermissionLog( permissionProjectPath, 'permission.confirm', @@ -2568,26 +1225,13 @@ export function App({ } } if (pending.commandId === 'conversation.read') { - if (pending.detail.includes('Agent 对话')) { - setAgentConversationStatus((current) => - current === '等待确认' ? '已取消读取 Agent 对话' : current, - ); - setAgentMemoryStatus((current) => - current === '等待确认' ? '已取消读取 Agent 对话' : current, - ); - } else { + if (!pending.detail.includes('Agent 对话')) { setWorkspaceStatus((current) => current === '等待确认' ? '已取消读取项目对话' : current, ); } } if (pending.commandId === 'memory.read') { - setMemoryStatus((current) => - current === '等待确认' ? '已取消读取项目记忆' : current, - ); - setAgentMemoryStatus((current) => - current === '等待确认' ? '已取消读取 Agent 私有记忆' : current, - ); if (!pending.detail.includes('Agent 私有记忆')) { setMessages((current) => [ ...current, @@ -2595,30 +1239,6 @@ export function App({ ]); } } - if (pending.commandId === 'memory.write') { - setMemoryStatus((current) => - current === '等待确认' ? '已取消保存项目记忆' : current, - ); - setAgentMemoryStatus((current) => - current === '等待确认' ? '已取消保存 Agent 私有记忆' : current, - ); - } - if ( - pending.commandId === 'file.list' || - pending.commandId === 'file.read' || - pending.commandId === 'agent.trace_read' - ) { - setFileStatus((current) => - current === '等待确认' ? '已取消读取项目文件' : current, - ); - } - if (pending.commandId === 'agent.trace_read') { - setAgentRunStatus((current) => - current === '等待确认' || current === '等待确认读取 Agent trace' - ? '已取消读取 Agent trace' - : current, - ); - } if ( pending.commandId === 'project.status' || pending.commandId === 'task.list' @@ -2627,60 +1247,23 @@ export function App({ pending.commandId === 'task.list' ? '已取消读取任务拆分' : '已取消读取项目状态'; - setProjectStatus(status); setWorkspaceStatus(status); } if (pending.commandId === 'asset.list') { - setAssetStatus('已取消读取项目资产'); setWorkspaceStatus('已取消读取项目资产'); } - if (pending.commandId === 'asset.upload') { - setAssetStatus('已取消上传资产'); - } - if (pending.commandId === 'canvas.project_open') { - setAssetStatus('已取消打开画板'); - } - if (pending.commandId === 'canvas.project_sync') { - setAssetStatus('已取消同步画板项目'); - } - if (pending.commandId === 'canvas.asset_import') { - setAssetStatus('已取消导入画板资产'); - } - if (pending.commandId === 'canvas.asset_generate') { - setAssetStatus('已取消生成美术素材'); - } - if (pending.commandId === 'canvas.export_import') { - setAssetStatus('已取消导入画板导出包'); - } if ( pending.commandId === 'preview.start' || pending.commandId === 'preview.open' || pending.commandId === 'preview.stop' || pending.commandId === 'preview.status' ) { - setPreviewStatus('已取消预览操作'); setWorkspaceStatus('已取消预览操作'); setMessages((current) => [ ...current, { role: 'assistant', text: '已取消预览操作' }, ]); } - if ( - pending.commandId === 'agent.run_status' || - pending.commandId === 'agent.audit' - ) { - setAgentRunStatus( - pending.commandId === 'agent.audit' - ? '已取消 Agent 审计' - : '已取消读取 Agent run 状态', - ); - } - if ( - pending.commandId === 'agent.resume' && - pending.detail.includes('未完成的 Agent Runtime 任务') - ) { - setAgentRunStatus('已取消恢复 Agent Runtime 任务'); - } if ( pending.commandId === 'project.index' || pending.commandId === 'project.diff' || @@ -2692,37 +1275,13 @@ export function App({ : pending.commandId === 'project.diff' ? '已取消对比项目 checkpoint' : '已取消导出本地试玩包'; - setFileStatus(status); setWorkspaceStatus(status); } if (pending.commandId === 'project.create') { - setProjectStatus('已取消'); setWorkspaceStatus('已取消'); } - if (pending.commandId === 'file.write') { - setFileStatus('已取消保存项目文件'); - } - if (pending.commandId === 'file.delete') { - setFileStatus('已取消删除项目文件'); - } - if (pending.commandId === 'memory.write') { - setMemoryStatus('已取消写入项目记忆'); - } - if (pending.commandId === 'memory.delete') { - setMemoryStatus('已取消删除项目记忆'); - } - if (pending.commandId === 'asset.register') { - setAssetStatus('已取消登记资产'); - } - if (pending.commandId === 'command.run_limited') { - setLimitedCommandStatus('已取消运行'); - } pendingUiConfirmationActionRef.current = null; setPendingUiConfirmation(null); - setCommandLog((current) => [ - ...current, - `permission.cancel ${pending.commandId}`, - ]); appendLocalPermissionLog( pending.projectPath ?? resolveUiPermissionLogProjectPath(pending.commandId), @@ -2731,27 +1290,11 @@ export function App({ ); } - function handleRuntimeConfigOpen() { - setRuntimeConfigOpen(true); - } - - function queueRunLocalShortcut() { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: 'game.run_local' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备运行当前本地游戏。' }, - ]); - } - queueRunLocalShortcutRef.current = queueRunLocalShortcut; executeRunLocalRef.current = executeRunLocal; useEffect(() => { const nextProjectPath = localProject?.projectPath; if ( - !projectSupervisorOnly || !playRequest || !nextProjectPath || playRequest.projectPath !== nextProjectPath @@ -2765,238 +1308,31 @@ export function App({ handledPlayRequestRef.current = requestKey; onPlayRequestHandled?.(playRequest.requestId); void executeRunLocalRef.current(true); - }, [ - localProject?.projectPath, - onPlayRequestHandled, - playRequest, - projectSupervisorOnly, - ]); + }, [localProject?.projectPath, onPlayRequestHandled, playRequest]); - function queueStaticSmokeShortcut() { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ - id: 'command.run_limited', - commandId: 'game.static_smoke', - }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备运行静态入口自检。' }, - ]); + /** + * 切换项目作用域时把这条链路自己的聊天状态清干净。 + * + * 策划会话与设计 Agent 视图都属于上一个项目;项目身份一变就不能留到下一个项目里。 + */ + function resetChatState() { + setChatFilesImporting(false); + setChatFileImportNotice(''); + setProjectChatError(''); + setDesignAgentTransientReplyTarget(''); + designAgentPendingViewRef.current = null; + setDesignAgentReasoning(''); + setDesignAgentActive(planningStartMode); + designAgentActiveRef.current = planningStartMode; + designAgentLaneRef.current = planningStartMode; + designAgentTurnRef.current = null; + designAgentReasoningTurnRef.current = null; + setDesignAgentView(null); } - function queueProjectCheckpointShortcut() { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: 'project.checkpoint' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备保存当前项目快照。' }, - ]); - } - - function queueProjectExportPackageShortcut() { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: 'project.export_package' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备导出本地试玩包。' }, - ]); - } - - function queuePreviewStartShortcut() { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: 'preview.start' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备启动本地预览。' }, - ]); - } - - function queuePreviewOpenShortcut() { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: 'preview.open' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备打开当前本地预览。' }, - ]); - } - - async function handleOpenLauncherWindow() { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请回到首页的项目组切换项目。' }, - ]); - } - - async function handleRevealCurrentProjectDirectory() { - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - return; - } - const invoke = resolveTauriInvoke(); - if (!invoke) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内打开项目目录。' }, - ]); - return; - } - try { - await invoke('open_local_project_directory', { - projectPath: nextProjectPath, - }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '已打开项目目录。' }, - ]); - } catch (error) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: error instanceof Error ? error.message : String(error), - }, - ]); - } - } - - function requireChatProjectForUserAction() { - const nextProjectPath = resolveChatProjectPath(localProject); - if (nextProjectPath) { - return nextProjectPath; - } - setMemoryStatus('请先初始化本地项目'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请先用 /project 设置本地项目。' }, - ]); - return null; - } - - async function handleProjectInit(event: FormEvent) { - event.preventDefault(); - requestCommandConfirmation( - 'project.create', - `创建 ${projectPath}`, - () => void executeProjectCreate(projectPath, false), - ); - } - - async function resumeProjectSupervisorRuntimeTasksIfNeeded( - invoke: TauriInvoke, - nextProjectPath: string, - ) { - if ( - projectSupervisorRuntimeResumeProjectPathRef.current === nextProjectPath - ) { - return ''; - } - try { - await invoke( - 'resume_game_creator_agent_runtime_tasks', - { projectPath: nextProjectPath }, - ); - projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath; - agentRuntimeResumeProjectPathRef.current = nextProjectPath; - return ''; - } catch (error) { - if (isMissingAgentRuntimeResumeCommandError(error)) { - projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath; - agentRuntimeResumeProjectPathRef.current = nextProjectPath; - return ''; - } - const message = error instanceof Error ? error.message : String(error); - if ( - message.includes('项目权限策略要求用户确认:agent.resume') || - message.includes('项目权限策略拒绝执行:agent.resume') - ) { - return ''; - } - if (isRuntimeConfigMissingError(message)) { - requestRuntimeConfigOpen(); - } - const runtimeError = `项目总控 Agent 恢复失败:${message}`; - setProjectSupervisorRuntimeError(runtimeError); - return runtimeError; - } - } - - async function readProjectSupervisorActiveSession( - invoke: TauriInvoke, - nextProjectPath: string, - ) { - return readProjectSupervisorActiveSessionId(invoke, nextProjectPath); - } - - async function refreshProjectSupervisorConversation( - invoke: TauriInvoke, - nextProjectPath: string, - sessionId: string, - ) { - const loadVersion = projectSupervisorHistoryLoadVersionRef.current + 1; - projectSupervisorHistoryLoadVersionRef.current = loadVersion; - const [projectConversation, supervisorConversation] = await Promise.all([ - invoke('read_local_conversation', { - projectPath: nextProjectPath, - agentId: null, - }), - invoke('read_local_conversation', { - projectPath: nextProjectPath, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - sessionId, - }), - ]); - if ( - projectSupervisorHistoryLoadVersionRef.current !== loadVersion || - localProjectPathRef.current !== nextProjectPath || - projectSupervisorSessionIdRef.current !== sessionId - ) { - return; - } - const conversationMessages = mergeProjectSupervisorConversation( - projectConversation.messages, - supervisorConversation.messages, - ); - const transientResponse = projectSupervisorResponseStreamRef.current; - if ( - conversationContainsProjectSupervisorResponseStream( - supervisorConversation.messages, - transientResponse, - ) - ) { - projectSupervisorResponseStreamRef.current = null; - setProjectSupervisorResponseStream(null); - } - setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT); - setMessages((current) => { - // 不能整体替换:乐观插入、尚未落盘的用户消息会被冲掉(初始需求看不到就是这个原因)。 - const nextMessages = mergeLoadedConversationWithPendingRuntimeMessages( - conversationMessages, - current, - ); - savedConversationProjectPathRef.current = nextProjectPath; - savedConversationCountRef.current = nextMessages.length; - latestMessagesRef.current = nextMessages; - return nextMessages; - }); - setProjectSupervisorRuntimeError(''); - } - projectSupervisorRefreshConversationRef.current = - refreshProjectSupervisorConversation; - async function loadProjectConversation( nextProjectPath: string, skipPolicyConfirm = false, - mode: 'initial' | 'replace' = 'initial', ) { const invoke = resolveTauriInvoke(); if (!invoke) { @@ -3013,7 +1349,7 @@ export function App({ 'conversation.read', nextProjectPath, `读取 ${nextProjectPath} 的项目对话历史`, - () => void loadProjectConversation(nextProjectPath, true, mode), + () => void loadProjectConversation(nextProjectPath, true), ); setWorkspaceStatus('等待确认'); return; @@ -3022,265 +1358,61 @@ export function App({ return; } } - const loadVersion = projectSupervisorHistoryLoadVersionRef.current + 1; - projectSupervisorHistoryLoadVersionRef.current = loadVersion; try { - if (planningStartMode) { - try { - const design = await hydrateDesignAgentSession(nextProjectPath); - if (design) { - if ( - projectSupervisorHistoryLoadVersionRef.current !== loadVersion || - localProjectPathRef.current !== nextProjectPath - ) { - return; - } - setWorkspaceStatus((workspaceStatus) => - workspaceStatus === '等待确认' - ? `已打开:${nextProjectPath}` - : workspaceStatus, - ); - return; - } - designAgentActiveRef.current = true; - setDesignAgentActive(true); - setDesignAgentView(null); - projectSupervisorSessionIdRef.current = null; - setProjectSupervisorSessionId(null); - updateProjectSupervisorRuntime(null); - setMessages(createDefaultChatMessages()); - savedConversationProjectPathRef.current = nextProjectPath; - savedConversationCountRef.current = 0; - latestMessagesRef.current = []; - setWorkspaceStatus((workspaceStatus) => - workspaceStatus === '等待确认' - ? `已打开:${nextProjectPath}` - : workspaceStatus, - ); - return; - } catch (error) { - setProjectSupervisorRuntimeError( - `策划会话无法恢复:${error instanceof Error ? error.message : String(error)}`, - ); + const design = await hydrateDesignAgentSession(nextProjectPath); + if (design) { + if (localProjectPathRef.current !== nextProjectPath) { return; } - } - const resumeError = directCodexProductRuntime - ? '' - : await resumeProjectSupervisorRuntimeTasksIfNeeded( - invoke, - nextProjectPath, - ); - if (directCodexProductRuntime) { - projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath; - agentRuntimeResumeProjectPathRef.current = nextProjectPath; - } - // Direct Codex owns its app-server conversation identity. The project - // workbench only hydrates the project conversation and must not inspect - // or create a legacy Supervisor Session while opening the project. - const sessionId = directCodexProductRuntime - ? null - : await readProjectSupervisorActiveSession(invoke, nextProjectPath); - let runtimeError = ''; - let loadedDirectHistoryHasMore = false; - let loadedDirectHistoryFirstItemId: string | null = null; - // 首屏连拉到的条目先暂存,待下方 staleness 守卫通过后再并入聊天 reducer: - // 迟到的切片属于已经切走的项目,不能在守卫之前就写进全局聊天状态。 - let loadedDirectHistoryItems: DirectThreadItem[] = []; - // 首屏切片的新端边界只认订阅回执里的 `lastCompletedItemId`(含该条):比它更新的条目 - // 只能来自运行态事件。打开项目时订阅 effect 还没跑,这里就先把闸门开好,订阅侧会复用 - // 同一道闸门并 settle 它。同一道闸门只锚定一次:同一订阅下再读一次(例如重开同一个项目) - // 没有新回执可等,退回"按当前文件尾取尾屏",与 `/history` 手动重读一致。 - let directHistoryThroughItemId: string | null = null; - if (directCodexProductRuntime && mode !== 'replace') { - const anchorGate = directHistoryAnchorGateToWaitFor( - directHistoryAnchorGateRef.current, - nextProjectPath, - ); - if (anchorGate) { - directHistoryAnchorGateRef.current = anchorGate; - directHistoryThroughItemId = await anchorGate.anchor; - anchorGate.consumed = true; - } - } - const projectConversation = directCodexProductRuntime - ? (() => { - // 首屏铺的就是这份视图的基线,"新回合"比较没有意义:判据退化为"这一页得有能渲染 - // 的条目",所以基线取空,免得拿上一份对话的残留状态去比。首屏失败仍按原有语义 - // 抛出,交给调用方的 catch 处理。 - return readDirectHistoryPages({ - existingEntries: [], - beforeItemId: null, - readSlice: (beforeItemId) => - invoke( - 'read_direct_project_history_slice', - beforeItemId - ? { - projectPath: nextProjectPath, - beforeItemId, - limit: CONVERSATION_INITIAL_VISIBLE_COUNT, - } - : { - projectPath: nextProjectPath, - ...(directHistoryThroughItemId - ? { throughItemId: directHistoryThroughItemId } - : {}), - limit: CONVERSATION_INITIAL_VISIBLE_COUNT, - }, - ), - }).then((pages) => { - if (pages.error) { - throw pages.error; - } - loadedDirectHistoryHasMore = pages.hasMore; - loadedDirectHistoryFirstItemId = pages.firstItemId; - loadedDirectHistoryItems = pages.items; - return { - path: nextProjectPath, - agentId: null, - messages: [], - } satisfies LocalConversationResult; - }); - })() - : invoke('read_local_conversation', { - projectPath: nextProjectPath, - agentId: null, - }); - const resolvedProjectConversation = await projectConversation; - // 首屏历史直接进聊天 reducer;`messages` 在 DirectProject 下只剩运行期本地消息。 - let supervisorConversation: LocalConversationResult | null = null; - let runtime: AgentRuntimeState | null = null; - let runtimeResponseStream: AgentRuntimeResponseStream | null = null; - if (sessionId) { - supervisorConversation = await invoke( - 'read_local_conversation', - { - projectPath: nextProjectPath, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - sessionId, - }, - ); - } - if (sessionId) { - try { - const runtimeResult = await invoke( - 'read_game_creator_agent_runtime', - { - projectPath: nextProjectPath, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - sessionId, - }, - ); - runtime = agentRuntimeStateFromResult(runtimeResult); - runtimeResponseStream = runtimeResult.responseStream ?? null; - } catch (error) { - runtimeError = error instanceof Error ? error.message : String(error); - } - } - if ( - projectSupervisorHistoryLoadVersionRef.current !== loadVersion || - localProjectPathRef.current !== nextProjectPath - ) { - return; - } - const conversationMessages = mergeProjectSupervisorConversation( - resolvedProjectConversation.messages, - supervisorConversation?.messages ?? [], - ); - if ( - conversationContainsProjectSupervisorResponseStream( - supervisorConversation?.messages ?? [], - runtimeResponseStream, - ) - ) { - runtimeResponseStream = null; - } - projectSupervisorSessionIdRef.current = sessionId; - setProjectSupervisorSessionId(sessionId); - updateProjectSupervisorRuntime(runtime); - if (runtime) { - updateProjectSupervisorResponseStream(runtimeResponseStream, runtime); - } else { - projectSupervisorResponseStreamRef.current = null; - setProjectSupervisorResponseStream(null); - } - setProjectSupervisorRuntimeError(runtimeError || resumeError); - if (directCodexProductRuntime) { - setDirectHistoryHasMore(loadedDirectHistoryHasMore); - directHistoryOldestItemIdRef.current = loadedDirectHistoryFirstItemId; - if (loadedDirectHistoryItems.length > 0) { - const items = loadedDirectHistoryItems; - // 首屏历史进聊天 reducer;`messages` 在 DirectProject 下只剩运行期本地消息。 - setDirectThreadChat((state) => mergeDirectHistoryItems(state, items)); - } - } - setMessages((current) => { - // replace 分支同样不能丢掉尚未落盘的运行时消息(初始需求)。 - const nextConversationMessages = - mergeLoadedConversationWithPendingRuntimeMessages( - conversationMessages, - current, - ); - // 空对话(没有默认问候之后的新常态)同样应当接受回读结果。 - const isEmptyConversation = current.length === 0; - const hasOnlyDefaultGreeting = - current.length === 1 && - current[0]?.role === 'assistant' && - current[0]?.text === '想做什么游戏?'; - const hasOnlyOpenStatus = - current.length === 2 && - current[0]?.role === 'assistant' && - current[0]?.text === '想做什么游戏?' && - current[1]?.role === 'assistant' && - current[1]?.text === `已设置本地项目:${nextProjectPath}`; - if ( - mode !== 'replace' && - !isEmptyConversation && - !hasOnlyDefaultGreeting && - !hasOnlyOpenStatus - ) { - return current; - } - setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT); - savedConversationProjectPathRef.current = nextProjectPath; - savedConversationCountRef.current = nextConversationMessages.length; - latestMessagesRef.current = nextConversationMessages; - setWorkspaceStatus((workspaceStatus) => { - if (mode === 'replace') { - return `已读取项目对话历史:${conversationMessages.length} 条`; - } - return workspaceStatus === '等待确认' + setWorkspaceStatus((workspaceStatus) => + workspaceStatus === '等待确认' ? `已打开:${nextProjectPath}` - : workspaceStatus; - }); - return nextConversationMessages; - }); - } catch (error) { - if ( - projectSupervisorHistoryLoadVersionRef.current !== loadVersion || - localProjectPathRef.current !== nextProjectPath - ) { + : workspaceStatus, + ); return; } - const message = error instanceof Error ? error.message : String(error); - if (isRuntimeConfigMissingError(message)) { - requestRuntimeConfigOpen(); + } catch (error) { + if (planningStartMode) { + setProjectChatError( + `策划会话无法恢复:${error instanceof Error ? error.message : String(error)}`, + ); } - setProjectSupervisorRuntimeError(message); + } + if (planningStartMode) { + if (localProjectPathRef.current !== nextProjectPath) { + return; + } + designAgentActiveRef.current = true; + setDesignAgentActive(true); + designAgentLaneRef.current = true; + setDesignAgentView(null); + setMessages(createDefaultChatMessages()); + savedConversationProjectPathRef.current = nextProjectPath; + savedConversationCountRef.current = 0; + latestMessagesRef.current = []; setWorkspaceStatus((workspaceStatus) => workspaceStatus === '等待确认' - ? `项目对话读取失败:${message}` + ? `已打开:${nextProjectPath}` : workspaceStatus, ); - // Keep the default greeting when history is missing or blocked. + return; } + // DirectProject 的项目对话由聊天容器自己的订阅与切片读取拥有:工作台壳不读项目 + // 对话文件,也不把历史混进壳自己的 `messages`。 + setProjectChatError(''); + setWorkspaceStatus((workspaceStatus) => + workspaceStatus === '等待确认' + ? `已打开:${nextProjectPath}` + : workspaceStatus, + ); } async function openWorkspace( nextProjectPath: string, announceToChat: boolean, mode: 'create' | 'open' = 'create', - projectKind: LocalProjectKind = 'web', + // 工程类型已不再由壳层决定插件可用性(插件宿主自己判),保留入参只为调用契约。 + _projectKind: LocalProjectKind = 'web', ) { const trimmedProjectPath = nextProjectPath.trim(); if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) { @@ -3295,7 +1427,6 @@ export function App({ const invoke = resolveTauriInvoke(); if (!invoke) { setWorkspaceStatus('需要在 Tauri App 内运行'); - setProjectStatus('需要在 Tauri App 内运行'); if (announceToChat) { setMessages((current) => [ ...current, @@ -3307,26 +1438,9 @@ export function App({ const projectScopeVersion = projectScopeVersionRef.current + 1; projectScopeVersionRef.current = projectScopeVersion; - resetProjectSupervisorState(); - resetDirectThreadChat(); + resetChatState(); updateClientPreview(null); - setPreviewStatus('未启动'); setWorkspaceStatus('正在打开'); - setProjectStatus('正在初始化'); - setSelectedAgent(null); - setAgentConversationInput(''); - setAgentConversationMessages([]); - setAgentConversationRuntime(null); - setAgentConversationSessionId(null); - setAgentConversationRunSubmitMode('steer'); - setAgentConversationRuntimeError(''); - setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT); - setAgentConversationStatus('未选择 agent'); - setAgentConversationSaving(false); - setAgentMemoryStatus('未读取'); - setAgentMemoryContent(''); - agentConversationSavingRef.current = false; - agentConversationLoadVersionRef.current += 1; try { const result = mode === 'open' @@ -3354,7 +1468,6 @@ export function App({ ) { const message = '本地项目路径无效'; setWorkspaceStatus(message); - setProjectStatus(message); if (announceToChat) { setMessages((current) => [ ...current, @@ -3374,18 +1487,8 @@ export function App({ } localProjectPathRef.current = openedProject.projectPath; setProjectPath(openedProject.projectPath); - setWorkspaceProjectKind(projectKind); setLocalProject(openedProject); - if (supervisorChatOnly) { - setChatInput(readSupervisorChatDraft(openedProject.projectPath)); - } - setChatReferences([]); - setChatContent([]); setManifest(openedProject.manifest); - setProjectFiles([]); - setProjectCheckpoints([]); - setAgentRunHistory([]); - setAgentRunHistoryFiles([]); setAgentRuntimeById({}); setMessages(conversationMessages); setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT); @@ -3393,7 +1496,6 @@ export function App({ savedConversationCountRef.current = conversationMessages.length; writeRecentWorkspace(openedProject.projectPath); setWorkspaceStatus(`已打开:${openedProject.projectPath}`); - setProjectStatus(mode === 'open' ? '已打开' : '已初始化'); appendLocalPermissionLog( openedProject.projectPath, 'permission.confirm', @@ -3408,11 +1510,8 @@ export function App({ }, ]); } - // Direct Codex must not resume or inspect legacy Supervisor work. Its - // own project conversation is hydrated above; the old run trace remains - // available only in the explicit legacy diagnostic surface. void loadProjectConversation(openedProject.projectPath); - if (!directCodexProductRuntime) { + if (!directProjectMode) { void refreshAgentRunTrace(openedProject.projectPath); } } catch (error) { @@ -3421,7 +1520,6 @@ export function App({ } const message = error instanceof Error ? error.message : String(error); setWorkspaceStatus(message); - setProjectStatus(message); if (announceToChat) { setMessages((current) => [ ...current, @@ -3431,206 +1529,6 @@ export function App({ } } - async function openAgentConversation( - agent: AgentStatusCard, - skipConversationPolicyConfirm = false, - skipMemoryPolicyConfirm = false, - ) { - const loadVersion = agentConversationLoadVersionRef.current + 1; - agentConversationLoadVersionRef.current = loadVersion; - setSelectedAgent(agent); - setAgentConversationInput(''); - setAgentConversationMessages([]); - setAgentConversationRuntime(null); - setAgentConversationSessionId(null); - setAgentConversationRunSubmitMode('steer'); - setAgentConversationRuntimeError(''); - setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT); - setAgentMemoryContent(''); - const invoke = resolveTauriInvoke(); - const nextProjectPath = resolveChatProjectPath(localProject); - if (!invoke || !nextProjectPath) { - setAgentConversationStatus('请先初始化本地项目'); - setAgentMemoryStatus('请先初始化本地项目'); - return; - } - try { - const status = await invoke( - 'check_game_creator_llm_config', - ); - if (agentConversationLoadVersionRef.current === loadVersion) { - setLlmConfigStatus(status); - } - } catch { - // Agent conversation history is still useful when config status is unavailable. - } - try { - if ( - !skipConversationPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'conversation.read', - nextProjectPath, - `读取 ${agent.title} Agent 对话`, - '准备读取 Agent 对话。', - () => - void openAgentConversation(agent, true, skipMemoryPolicyConfirm), - )) - ) { - setAgentConversationStatus('等待确认'); - setAgentMemoryStatus('等待确认'); - return; - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setAgentConversationStatus(message); - setAgentMemoryStatus(message); - return; - } - setAgentConversationStatus('正在读取'); - setAgentMemoryStatus('正在读取'); - try { - const result = await invoke( - 'read_local_conversation', - { - projectPath: nextProjectPath, - agentId: agent.id, - }, - ); - if (agentConversationLoadVersionRef.current !== loadVersion) { - return; - } - setAgentConversationMessages(result.messages); - setAgentConversationSessionId(result.sessionId ?? null); - try { - const runtime = await invoke( - 'read_game_creator_agent_runtime', - { - projectPath: nextProjectPath, - agentId: agent.id, - ...(result.sessionId ? { sessionId: result.sessionId } : {}), - }, - ); - if (agentConversationLoadVersionRef.current === loadVersion) { - const nextRuntime = agentRuntimeStateFromResult(runtime); - setAgentConversationRuntime(nextRuntime); - rememberAgentRuntimeState(nextRuntime); - setAgentConversationRuntimeError(''); - } - } catch (error) { - if (agentConversationLoadVersionRef.current === loadVersion) { - setAgentConversationRuntime(null); - setAgentConversationRuntimeError( - error instanceof Error ? error.message : String(error), - ); - } - } - setAgentConversationStatus( - `已读取 ${result.messages.length} 条:${result.path}`, - ); - setCommandLog((current) => [...current, 'conversation.read']); - } catch (error) { - if (agentConversationLoadVersionRef.current !== loadVersion) { - return; - } - setAgentConversationMessages([]); - setAgentConversationStatus( - error instanceof Error ? error.message : String(error), - ); - } - try { - if ( - !skipMemoryPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'memory.read', - nextProjectPath, - `读取 ${agent.title} Agent 私有记忆`, - '准备读取 Agent 私有记忆。', - () => void openAgentConversation(agent, true, true), - )) - ) { - setAgentMemoryStatus('等待确认'); - return; - } - } catch (error) { - if (agentConversationLoadVersionRef.current !== loadVersion) { - return; - } - setAgentMemoryStatus( - error instanceof Error ? error.message : String(error), - ); - return; - } - try { - const result = await invoke( - 'read_local_agent_memory', - { - projectPath: nextProjectPath, - taskId: agent.id, - }, - ); - if (agentConversationLoadVersionRef.current !== loadVersion) { - return; - } - setAgentMemoryContent(result.content); - setAgentMemoryStatus( - result.exists ? `已读取:${result.path}` : '私有记忆为空', - ); - setCommandLog((current) => [...current, 'memory.agent.read']); - } catch (error) { - if (agentConversationLoadVersionRef.current !== loadVersion) { - return; - } - setAgentMemoryContent(''); - setAgentMemoryStatus( - error instanceof Error ? error.message : String(error), - ); - } - } - - function closeAgentConversation() { - agentConversationLoadVersionRef.current += 1; - agentConversationSavingRef.current = false; - agentConversationBackgroundBusyRef.current = false; - setSelectedAgent(null); - setAgentConversationInput(''); - setAgentConversationMessages([]); - setAgentConversationRuntime(null); - setAgentConversationSessionId(null); - setAgentConversationRunSubmitMode('steer'); - setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT); - setAgentConversationStatus('未选择 agent'); - setAgentConversationSaving(false); - setAgentConversationBackgroundBusy(false); - setAgentMemoryStatus('未读取'); - setAgentMemoryContent(''); - } - - function prepareSuggestedToolCommandDraft( - toolCall: GameCreationAgentToolCallTrace, - ) { - const commandDraft = commandDraftFromSuggestedToolCall(toolCall); - if (!commandDraft) { - return; - } - prepareChatCommandDraft(commandDraft); - closeAgentConversation(); - } - - function prepareChatCommandDraft(commandDraft: string) { - setChatInput(commandDraft); - setChatReferences([]); - setChatContent([]); - window.setTimeout(() => chatInputRef.current?.focus(), 0); - } - - function handleChatComposerChange(draft: ChatComposerDraft) { - setChatInput(draft.text); - setChatReferences(draft.references); - setChatContent(draft.content ?? []); - } - useEffect(() => { const handleResourceReferenceInsert = (event: Event) => { const detail = (event as CustomEvent) @@ -3639,2740 +1537,143 @@ export function App({ chatComposerRef.current?.insertReferences([detail.reference]); chatComposerRef.current?.focus(); }; - /** - * 画布拖拽的批量 @ 引用:与单条入口同一个消费点,只是一次把整批插进去。 - * - * 逐条派发单条事件也能跑,但每次都会重建一次草稿并重新聚焦,而且插入顺序只能靠事件顺序兜着; - * 批次为空时静默返回(不开一次空事务)。 - */ - const handleResourceReferenceInsertMany = (event: Event) => { - const detail = ( - event as CustomEvent - ).detail; - const references = detail?.references ?? []; - if (references.length === 0) return; - chatComposerRef.current?.insertReferences(references); - chatComposerRef.current?.focus(); - }; window.addEventListener( RESOURCE_REFERENCE_INSERT_EVENT, handleResourceReferenceInsert, ); - window.addEventListener( - RESOURCE_REFERENCE_INSERT_MANY_EVENT, - handleResourceReferenceInsertMany, - ); - return () => { + return () => window.removeEventListener( RESOURCE_REFERENCE_INSERT_EVENT, handleResourceReferenceInsert, ); - window.removeEventListener( - RESOURCE_REFERENCE_INSERT_MANY_EVENT, - handleResourceReferenceInsertMany, - ); - }; }, []); - function prepareProjectAssetRegisterDraft(localPath: string) { - prepareChatCommandDraft(projectFileActionDrafts(localPath).assetCommand); - } + /** + * DirectProject 聊天的首轮需求:入口 latch 里那份原文与附件,项目匹配后由聊天认领。 + * + * 认领记录与 Design / Planning 入口共用一份(按页面保存),因此同一条 + * 首轮需求不会被两个入口各发一次。 + */ + const initialDirectTurn: DirectProjectInitialTurn | null = (() => { + const latch = initialPlanningPromptLatchRef.current; + if (!latch.prompt || !latch.projectPath) return null; + return { + projectPath: latch.projectPath, + // 首轮需求按 Composer 的 canonical content 形状交给聊天:文本与附件引用内联在 + // 同一份 content 里,`@` 引用与附件的处理不需要两套入参。 + content: [ + { type: 'input_text', text: latch.prompt }, + ...directCodexAttachmentContentParts(latch.attachments), + ], + creationType: latch.creationType, + claimScope: latch.claimScope, + }; + })(); - function prepareAgentEvidenceReadDraft(localPath: string) { - if (!isSafeProjectRelativePath(localPath)) { - return; - } - prepareChatCommandDraft(projectFileActionDrafts(localPath).readCommand); - closeAgentConversation(); - } - - async function handlePickCanvasExportFileDraft() { - if (!requireChatProjectForUserAction()) { - return; - } + /** + * DirectProject 读自己项目对话的门:策略要求确认时入队确认,读取由回调继续。 + */ + async function ensureDirectHistoryReadAllowed(input: { + projectPath: string; + onConfirmed: () => void; + }) { const invoke = resolveTauriInvoke(); - if (!invoke) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内选择画板导出包。' }, - ]); - return; - } + if (!invoke) return false; try { - const selectedPath = await invoke('pick_local_file'); - if (!selectedPath) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '已取消选择画板导出包。' }, - ]); - return; - } - setCanvasExportPath(selectedPath); - prepareChatCommandDraft(`/import-canvas-export ${selectedPath} `); - setAssetStatus(`已选择画板导出包:${selectedPath}`); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `已选择画板导出包:${selectedPath}` }, - ]); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setAssetStatus(message); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - - async function saveAgentConversationMessage( - agent: AgentStatusCard, - content: string, - skipPolicyConfirm = false, - ) { - if (!agent || !content || agentConversationSavingRef.current) { - return; - } - if (agentRuntimeNeedsUserInput(agentConversationRuntime)) { - setAgentConversationStatus('请先回答 Agent 当前的澄清问题'); - return; - } - const invoke = resolveTauriInvoke(); - const nextProjectPath = resolveChatProjectPath(localProject); - if (!invoke) { - setAgentConversationStatus('需要在 Tauri App 内运行'); - return; - } - if (!nextProjectPath) { - setAgentConversationStatus('请先初始化本地项目'); - return; - } - const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent); - if (llmWarning) { - setAgentConversationStatus(llmWarning); - return; - } - const saveVersion = agentConversationLoadVersionRef.current; - try { - if ( - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'conversation.write', - nextProjectPath, - `写入 ${agent.title} Agent 对话`, - '准备保存 Agent 对话。', - () => void saveAgentConversationMessage(agent, content, true), - )) - ) { - setAgentConversationStatus('等待确认'); - return; - } - } catch (error) { - setAgentConversationStatus( - error instanceof Error ? error.message : String(error), + const policyView = await invoke( + 'read_project_permission_policy', + { projectPath: input.projectPath }, ); - return; - } - agentConversationSavingRef.current = true; - setAgentConversationSaving(true); - setAgentConversationInput(''); - setAgentConversationStatus('正在保存用户消息'); - let savedUserResult: LocalConversationResult | null = null; - let stopStreamListen: (() => void) | null = null; - let streamListenDisposed = false; - let streamListenReady = false; - try { - savedUserResult = await invoke( - 'append_local_conversation_message', - { - projectPath: nextProjectPath, - agentId: agent.id, - message: { - role: 'user', - content, - agentId: null, - }, - }, + if (!policyView.policy.confirmCommands.includes('conversation.read')) { + return true; + } + requestProjectPolicyConfirmation( + 'conversation.read', + input.projectPath, + `读取 ${input.projectPath} 的项目对话历史`, + input.onConfirmed, ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - const savedUserMessages = savedUserResult.messages; - setAgentConversationMessages(savedUserMessages); - setAgentConversationStatus('正在连接 Agent LLM'); - const streamRunId = createAgentChatRunId('agent-conversation'); - if (canSubscribeTauriEvents()) { - try { - stopStreamListen = - await subscribeTauriEvent( - 'game-creator-role-agent-chat-stream', - (event) => { - const payload = event.payload; - if ( - payload.projectPath !== nextProjectPath || - payload.agentId !== agent.id || - payload.runId !== streamRunId || - agentConversationLoadVersionRef.current !== saveVersion - ) { - return; - } - if (payload.runtimeState) { - setAgentConversationRuntime((current) => { - const nextRuntime = normalizeAgentRuntimeState( - payload.runtimeState!, - current, - ); - rememberAgentRuntimeState(nextRuntime); - return nextRuntime; - }); - setAgentConversationRuntimeError(''); - } - if (payload.status === 'started') { - setAgentConversationStatus( - payload.runtimeSummary ?? 'Agent 已连接,正在等待回复', - ); - return; - } - if (payload.status === 'delta') { - const draftText = - payload.accumulatedText || payload.deltaText; - if (draftText) { - setAgentConversationMessages([ - ...savedUserMessages, - createLocalConversationDraftMessage(draftText), - ]); - } - setAgentConversationStatus( - payload.finishReason - ? `Agent 回复结束:${payload.finishReason}` - : '正在接收 Agent 回复', - ); - return; - } - if (payload.status === 'completed') { - setAgentConversationStatus( - payload.runtimeSummary ?? 'Agent 回复完成,正在保存', - ); - return; - } - if (payload.status === 'failed') { - setAgentConversationStatus( - payload.runtimeSummary ?? - 'Agent 流式回复失败,正在记录错误', - ); - } - }, - ); - streamListenReady = true; - if (streamListenDisposed) { - stopStreamListen(); - stopStreamListen = null; - } - } catch { - setAgentConversationStatus('实时状态不可用,正在使用普通回复模式'); - } - } - const reply = streamListenReady - ? await invoke( - 'chat_with_game_creator_role_agent_stream', - { - projectPath: nextProjectPath, - agentId: agent.id, - prompt: content, - runId: streamRunId, - }, - ) - : await invoke( - 'chat_with_game_creator_role_agent', - { - projectPath: nextProjectPath, - agentId: agent.id, - prompt: content, - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - try { - const runtime = await invoke( - 'read_game_creator_agent_runtime', - { - projectPath: nextProjectPath, - agentId: agent.id, - }, - ); - if (agentConversationLoadVersionRef.current === saveVersion) { - const nextRuntime = agentRuntimeStateFromResult(runtime); - setAgentConversationRuntime(nextRuntime); - rememberAgentRuntimeState(nextRuntime); - setAgentConversationRuntimeError(''); - } - } catch (error) { - if (agentConversationLoadVersionRef.current === saveVersion) { - setAgentConversationRuntimeError( - error instanceof Error ? error.message : String(error), - ); - } - } - setAgentConversationStatus('正在保存 Agent 回复'); - setAgentConversationMessages([ - ...savedUserMessages, - createLocalConversationDraftMessage(reply.replyText), - ]); - const assistantResult = await invoke( - 'append_local_conversation_message', - { - projectPath: nextProjectPath, - agentId: agent.id, - message: { - role: 'assistant', - content: reply.replyText, - agentId: null, - }, - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationMessages(assistantResult.messages); - setAgentConversationStatus( - `已保存 ${assistantResult.messages.length} 条:${assistantResult.path}`, - ); - setCommandLog((current) => [...current, 'conversation.write']); - } catch (error) { - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - void captureAgentRuntimeError(error, agent.id); - if (savedUserResult) { - const message = `已保存用户消息;Agent 回复失败:${ - error instanceof Error ? error.message : String(error) - }`; - try { - const errorResult = await invoke( - 'append_local_conversation_message', - { - projectPath: nextProjectPath, - agentId: agent.id, - message: { - role: 'assistant', - content: message, - agentId: null, - }, - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationMessages(errorResult.messages); - } catch { - setAgentConversationMessages([ - ...savedUserResult.messages, - { - schemaVersion: 'game-creator-conversation.v1', - role: 'assistant', - content: message, - agentId: null, - updatedAt: Date.now(), - }, - ]); - } - setAgentConversationStatus(message); - } else { - setAgentConversationInput(content); - setAgentConversationStatus( - error instanceof Error ? error.message : String(error), - ); - } - } finally { - streamListenDisposed = true; - stopStreamListen?.(); - agentConversationSavingRef.current = false; - setAgentConversationSaving(false); - } - } - - async function startSelectedAgentBackgroundTask( - agent: AgentStatusCard, - content: string, - skipPolicyConfirm = false, - submitMode = agentConversationRunSubmitMode, - ) { - if (!agent || !content || agentConversationBackgroundBusyRef.current) { - return; - } - if (agentRuntimeNeedsUserInput(agentConversationRuntime)) { - setAgentConversationStatus('请先回答 Agent 当前的澄清问题'); - return; - } - const invoke = resolveTauriInvoke(); - const nextProjectPath = resolveChatProjectPath(localProject); - if (!invoke) { - setAgentConversationStatus('需要在 Tauri App 内运行'); - return; - } - if (!nextProjectPath) { - setAgentConversationStatus('请先初始化本地项目'); - return; - } - const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent); - if (llmWarning) { - setAgentConversationStatus(llmWarning); - return; - } - const steerRuntime = - submitMode === 'steer' - ? matchingAgentRuntimeForSteer( - [agentConversationRuntime], - agent.id, - agentConversationSessionId, - ) - : null; - const saveVersion = agentConversationLoadVersionRef.current; - try { - if ( - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'conversation.write', - nextProjectPath, - steerRuntime - ? `向 ${agent.title} 当前 Run 追加指令` - : `启动 ${agent.title} 后台任务`, - steerRuntime - ? '准备向当前 Agent Run 追加指令。' - : '准备启动 Agent 后台任务。', - () => - void startSelectedAgentBackgroundTask( - agent, - content, - true, - submitMode, - ), - )) - ) { - setAgentConversationStatus('等待确认'); - return; - } - } catch (error) { - setAgentConversationStatus( - error instanceof Error ? error.message : String(error), - ); - return; - } - agentConversationBackgroundBusyRef.current = true; - setAgentConversationBackgroundBusy(true); - if (steerRuntime) { - setAgentConversationStatus( - `正在向当前 Run 追加指令:${steerRuntime.runId}`, - ); - } else { - setAgentConversationInput(''); - setAgentConversationStatus('正在启动 Agent 后台任务'); - } - try { - let runtime: AgentRuntimeResult; - let successStatus: string; - if (steerRuntime) { - const steer = await invoke( - 'steer_game_creator_agent_runtime_task', - { - projectPath: nextProjectPath, - agentId: agent.id, - sessionId: steerRuntime.sessionId, - runId: steerRuntime.runId, - steerId: createAgentChatRunId('agent-steer'), - instruction: content, - }, - ); - runtime = steer.runtime; - successStatus = agentRuntimeSteerStatus(steer); - } else { - runtime = await invoke( - 'start_game_creator_agent_runtime_task', - { - projectPath: nextProjectPath, - agentId: agent.id, - task: content, - runId: createAgentChatRunId('agent-background-task'), - ...(agentConversationSessionId - ? { sessionId: agentConversationSessionId } - : {}), - }, - ); - successStatus = agentRuntimeStartStatus(runtime); - } - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - const nextRuntime = agentRuntimeStateFromResult(runtime); - setAgentConversationInput(''); - setAgentConversationRunSubmitMode('steer'); - setAgentConversationSessionId(nextRuntime.sessionId); - setAgentConversationRuntime(nextRuntime); - rememberAgentRuntimeState(nextRuntime); - setAgentConversationRuntimeError(''); - try { - const conversation = await invoke( - 'read_local_conversation', - { - projectPath: nextProjectPath, - agentId: agent.id, - sessionId: nextRuntime.sessionId, - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationMessages(conversation.messages); - setAgentConversationSessionId( - conversation.sessionId ?? nextRuntime.sessionId, - ); - } catch (error) { - if (agentConversationLoadVersionRef.current === saveVersion) { - setAgentConversationRuntimeError( - `对话刷新失败:${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - } - setAgentConversationStatus(successStatus); - setCommandLog((current) => [ - ...current, - steerRuntime ? 'agent.runtime.steer' : 'agent.runtime.background_task', - ]); - } catch (error) { - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationInput(content); - setAgentConversationStatus( - error instanceof Error ? error.message : String(error), - ); - } finally { - agentConversationBackgroundBusyRef.current = false; - setAgentConversationBackgroundBusy(false); - } - } - - async function cancelSelectedAgentRuntimeTask( - agent: AgentStatusCard, - runId: string, - ) { - if (!agent || !runId || agentConversationBackgroundBusyRef.current) { - return; - } - const invoke = resolveTauriInvoke(); - const nextProjectPath = resolveChatProjectPath(localProject); - if (!invoke) { - setAgentConversationStatus('需要在 Tauri App 内运行'); - return; - } - if (!nextProjectPath) { - setAgentConversationStatus('请先初始化本地项目'); - return; - } - const saveVersion = agentConversationLoadVersionRef.current; - agentConversationBackgroundBusyRef.current = true; - setAgentConversationBackgroundBusy(true); - setAgentConversationStatus('正在取消 Agent 后台任务'); - try { - const runtime = await invoke( - 'cancel_game_creator_agent_runtime_task', - { - projectPath: nextProjectPath, - agentId: agent.id, - runId, - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - const nextRuntime = agentRuntimeStateFromResult(runtime); - setAgentConversationRuntime(nextRuntime); - rememberAgentRuntimeState(nextRuntime); - setAgentConversationRuntimeError(''); - setAgentConversationStatus(agentRuntimeCancelStatus(nextRuntime, runId)); - setCommandLog((current) => [ - ...current, - 'agent.runtime.background_task.cancel', - ]); - } catch (error) { - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationStatus( - error instanceof Error ? error.message : String(error), - ); - } finally { - agentConversationBackgroundBusyRef.current = false; - setAgentConversationBackgroundBusy(false); - } - } - - async function retrySelectedAgentRuntimeTask( - agent: AgentStatusCard, - runId: string, - ) { - if (!agent || !runId || agentConversationBackgroundBusyRef.current) { - return; - } - const invoke = resolveTauriInvoke(); - const nextProjectPath = resolveChatProjectPath(localProject); - if (!invoke) { - setAgentConversationStatus('需要在 Tauri App 内运行'); - return; - } - if (!nextProjectPath) { - setAgentConversationStatus('请先初始化本地项目'); - return; - } - const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent); - if (llmWarning) { - setAgentConversationStatus(llmWarning); - return; - } - const saveVersion = agentConversationLoadVersionRef.current; - agentConversationBackgroundBusyRef.current = true; - setAgentConversationBackgroundBusy(true); - setAgentConversationStatus('正在重试 Agent 后台任务'); - try { - const runtime = await invoke( - 'retry_game_creator_agent_runtime_task', - { - projectPath: nextProjectPath, - agentId: agent.id, - runId, - nextRunId: createAgentChatRunId('agent-background-retry'), - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - const nextRuntime = agentRuntimeStateFromResult(runtime); - setAgentConversationRuntime(nextRuntime); - rememberAgentRuntimeState(nextRuntime); - setAgentConversationRuntimeError(''); - const conversation = await invoke( - 'read_local_conversation', - { - projectPath: nextProjectPath, - agentId: agent.id, - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationMessages(conversation.messages); - setAgentConversationStatus(agentRuntimeStartStatus(runtime)); - setCommandLog((current) => [ - ...current, - 'agent.runtime.background_task.retry', - ]); - } catch (error) { - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationStatus( - error instanceof Error ? error.message : String(error), - ); - } finally { - agentConversationBackgroundBusyRef.current = false; - setAgentConversationBackgroundBusy(false); - } - } - - async function confirmSelectedAgentRuntimeTask( - agent: AgentStatusCard, - runId: string, - actionId: string, - ) { - if ( - !agent || - !runId || - !actionId || - agentConversationBackgroundBusyRef.current - ) { - return; - } - const invoke = resolveTauriInvoke(); - const nextProjectPath = resolveChatProjectPath(localProject); - if (!invoke) { - setAgentConversationStatus('需要在 Tauri App 内运行'); - return; - } - if (!nextProjectPath) { - setAgentConversationStatus('请先初始化本地项目'); - return; - } - const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent); - if (llmWarning) { - setAgentConversationStatus(llmWarning); - return; - } - const saveVersion = agentConversationLoadVersionRef.current; - agentConversationBackgroundBusyRef.current = true; - setAgentConversationBackgroundBusy(true); - setAgentConversationStatus('正在确认并继续 Agent 后台任务'); - try { - const runtime = await invoke( - 'confirm_game_creator_agent_runtime_task', - { - projectPath: nextProjectPath, - agentId: agent.id, - runId, - actionId, - note: '开发者已确认待执行工具动作', - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - const nextRuntime = agentRuntimeStateFromResult(runtime); - setAgentConversationRuntime(nextRuntime); - rememberAgentRuntimeState(nextRuntime); - setAgentConversationRuntimeError(''); - const conversation = await invoke( - 'read_local_conversation', - { - projectPath: nextProjectPath, - agentId: agent.id, - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationMessages(conversation.messages); - setAgentConversationStatus(agentRuntimeStartStatus(runtime)); - setCommandLog((current) => [ - ...current, - 'agent.runtime.tool_confirmation.approved', - ]); - } catch (error) { - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationStatus( - error instanceof Error ? error.message : String(error), - ); - } finally { - agentConversationBackgroundBusyRef.current = false; - setAgentConversationBackgroundBusy(false); - } - } - - async function rejectSelectedAgentRuntimeTask( - agent: AgentStatusCard, - runId: string, - actionId: string, - ) { - if ( - !agent || - !runId || - !actionId || - agentConversationBackgroundBusyRef.current - ) { - return; - } - const invoke = resolveTauriInvoke(); - const nextProjectPath = resolveChatProjectPath(localProject); - if (!invoke) { - setAgentConversationStatus('需要在 Tauri App 内运行'); - return; - } - if (!nextProjectPath) { - setAgentConversationStatus('请先初始化本地项目'); - return; - } - const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent); - if (llmWarning) { - setAgentConversationStatus(llmWarning); - return; - } - const saveVersion = agentConversationLoadVersionRef.current; - agentConversationBackgroundBusyRef.current = true; - setAgentConversationBackgroundBusy(true); - setAgentConversationStatus('正在拒绝工具动作并继续 Agent 后台任务'); - try { - const runtime = await invoke( - 'reject_game_creator_agent_runtime_task', - { - projectPath: nextProjectPath, - agentId: agent.id, - runId, - actionId, - note: '开发者拒绝待执行工具动作', - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - const nextRuntime = agentRuntimeStateFromResult(runtime); - setAgentConversationRuntime(nextRuntime); - rememberAgentRuntimeState(nextRuntime); - setAgentConversationRuntimeError(''); - const conversation = await invoke( - 'read_local_conversation', - { - projectPath: nextProjectPath, - agentId: agent.id, - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationMessages(conversation.messages); - setAgentConversationStatus(agentRuntimeStartStatus(runtime)); - setCommandLog((current) => [ - ...current, - 'agent.runtime.tool_confirmation.rejected', - ]); - } catch (error) { - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationStatus( - error instanceof Error ? error.message : String(error), - ); - } finally { - agentConversationBackgroundBusyRef.current = false; - setAgentConversationBackgroundBusy(false); - } - } - - async function answerSelectedAgentRuntimeUserInput( - agent: AgentStatusCard, - request: AgentRuntimeUserInputRequest, - responseId: string, - answers: Record, - ) { - const invoke = resolveTauriInvoke(); - const nextProjectPath = resolveChatProjectPath(localProject); - const currentRequest = agentConversationRuntime?.userInputRequest; - if ( - !invoke || - !nextProjectPath || - !responseId || - agentConversationBackgroundBusyRef.current - ) { - return; - } - if ( - request.agentId !== agent.id || - request.sessionId !== agentConversationSessionId || - request.runId !== agentConversationRuntime?.runId || - currentRequest?.requestId !== request.requestId || - currentRequest?.actionId !== request.actionId - ) { - setAgentConversationRuntimeError('待回答请求已变更,请刷新 Runtime 状态'); - return; - } - const saveVersion = agentConversationLoadVersionRef.current; - agentConversationBackgroundBusyRef.current = true; - setAgentConversationBackgroundBusy(true); - setAgentConversationRuntimeError(''); - setAgentConversationStatus('正在提交回答并继续当前 Run'); - try { - const result = await invoke( - 'answer_game_creator_agent_runtime_user_input', - { - projectPath: nextProjectPath, - agentId: agent.id, - runId: request.runId, - actionId: request.actionId, - requestId: request.requestId, - responseId, - answers, - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - const runtimeState = agentRuntimeStateFromResult( - result, - agentConversationRuntime, - ); - if ( - runtimeState.agentId !== agent.id || - runtimeState.sessionId !== request.sessionId || - runtimeState.runId !== request.runId - ) { - throw new Error('Agent 回答后 Runtime 身份不匹配'); - } - setAgentConversationRuntime(runtimeState); - rememberAgentRuntimeState(runtimeState); - setAgentConversationSessionId(runtimeState.sessionId); - const conversation = await invoke( - 'read_local_conversation', - { - projectPath: nextProjectPath, - agentId: agent.id, - sessionId: runtimeState.sessionId, - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationMessages(conversation.messages); - setAgentConversationSessionId( - conversation.sessionId ?? runtimeState.sessionId, - ); - setAgentConversationRuntimeError(''); - setAgentConversationStatus(agentRuntimeConversationStatus(runtimeState)); - setCommandLog((current) => [ - ...current, - 'agent.runtime.user_input.answered', - ]); - } catch (error) { - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - const message = error instanceof Error ? error.message : String(error); - setAgentConversationRuntimeError(message); - setAgentConversationStatus(`回答提交失败:${message}`); - } finally { - agentConversationBackgroundBusyRef.current = false; - setAgentConversationBackgroundBusy(false); - } - } - - async function saveSelectedAgentPrivateMemory( - agent: AgentStatusCard, - content: string, - skipPolicyConfirm = false, - ) { - if (!agent || !content || agentConversationSavingRef.current) { - return; - } - const invoke = resolveTauriInvoke(); - const nextProjectPath = resolveChatProjectPath(localProject); - if (!invoke) { - setAgentMemoryStatus('需要在 Tauri App 内运行'); - return; - } - if (!nextProjectPath) { - setAgentMemoryStatus('请先初始化本地项目'); - return; - } - const saveVersion = agentConversationLoadVersionRef.current; - try { - if (!skipPolicyConfirm) { - const policyView = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - if (policyView.policy.deniedCommands.includes('memory.write')) { - const message = '项目权限策略拒绝执行:memory.write'; - markProjectPolicyDenied('memory.write', message); - setAgentMemoryStatus(message); - setCommandLog((current) => [ - ...current, - 'permission.deny memory.write', - ]); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - return; - } - if (policyView.policy.confirmCommands.includes('memory.write')) { - requestProjectPolicyConfirmation( - 'memory.write', - nextProjectPath, - `写入 ${agent.title} Agent 私有记忆`, - () => void saveSelectedAgentPrivateMemory(agent, content, true), - ); - setAgentMemoryStatus('等待确认'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备保存 Agent 私有记忆。' }, - ]); - return; - } - } - } catch (error) { - setAgentMemoryStatus( - error instanceof Error ? error.message : String(error), - ); - return; - } - agentConversationSavingRef.current = true; - setAgentConversationSaving(true); - setAgentConversationInput(''); - setAgentMemoryStatus('正在保存'); - try { - const result = await invoke( - 'write_local_agent_memory', - { - projectPath: nextProjectPath, - taskId: agent.id, - content: appendMemoryContent(agentMemoryContent, content), - }, - ); - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentMemoryContent(result.content); - setAgentMemoryStatus(`已追加私有记忆:${result.path}`); - setAgentConversationStatus(`已写入 ${agent.title} 私有记忆。`); - setCommandLog((current) => [...current, 'memory.agent.write']); - } catch (error) { - if (agentConversationLoadVersionRef.current !== saveVersion) { - return; - } - setAgentConversationInput(content); - setAgentMemoryStatus( - error instanceof Error ? error.message : String(error), - ); - } finally { - agentConversationSavingRef.current = false; - setAgentConversationSaving(false); - } - } - - function handleAgentConversationSubmit(event: FormEvent) { - event.preventDefault(); - const agent = selectedAgent; - const content = agentConversationInput.trim(); - if (agentRuntimeNeedsUserInput(agentConversationRuntime)) { - setAgentConversationStatus('请先回答 Agent 当前的澄清问题'); - return; - } - if (!agent || !content || agentConversationSavingRef.current) { - return; - } - void saveAgentConversationMessage(agent, content); - } - - function handleAgentPrivateMemorySubmit() { - const agent = selectedAgent; - const content = agentConversationInput.trim(); - if (!agent || !content || agentConversationSavingRef.current) { - return; - } - void saveSelectedAgentPrivateMemory(agent, content); - } - - function handleAgentBackgroundTaskSubmit() { - const agent = selectedAgent; - const content = agentConversationInput.trim(); - if (agentRuntimeNeedsUserInput(agentConversationRuntime)) { - setAgentConversationStatus('请先回答 Agent 当前的澄清问题'); - return; - } - if (!agent || !content || agentConversationBackgroundBusyRef.current) { - return; - } - void startSelectedAgentBackgroundTask(agent, content); - } - - function showChatHelp() { - setCommandLog((current) => [...current, 'help.show']); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `可用命令:\n${chatCommandHelp.join('\n')}`, - }, - ]); - } - - async function handleChatSubmit(event: FormEvent) { - event.preventDefault(); - const prompt = chatInput.trim(); - const references = chatReferences; - if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) { - setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题'); - return; - } - if ((!prompt && references.length === 0) || chatAgentBusy) { - return; - } - - setChatInput(''); - setChatReferences([]); - setChatContent([]); - setMessages((current) => [ - ...current, - { - role: 'user', - text: prompt, - runtimeOwned: !prompt.startsWith('/'), - }, - ]); - if (prompt === '/help') { - showChatHelp(); - return; - } - - if (prompt === '/config') { - handleRuntimeConfigOpen(); - return; - } - - if (prompt === '/capabilities' || prompt === '/能力') { - void executeAgentCapabilitiesChat(); - return; - } - - if (prompt === '/audit' || prompt === '/审计') { - void executeAgentAuditChat(); - return; - } - - const missingArgumentMessage = missingChatCommandArgumentMessage(prompt); - if (missingArgumentMessage) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: missingArgumentMessage }, - ]); - return; - } - - if (prompt.startsWith('/project ')) { - const nextProjectPath = prompt.slice('/project '.length).trim(); - if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请提供本地项目绝对路径。' }, - ]); - return; - } - if (projectPathHasControlCharacter(nextProjectPath)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '本地项目路径不能包含控制字符。' }, - ]); - return; - } - queuePendingCommand({ - id: 'project.create', - projectPath: nextProjectPath, - }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `准备初始化本地项目:${nextProjectPath}` }, - ]); - return; - } - - if (prompt.startsWith('/generate ') || prompt.startsWith('/draft ')) { - if (!requireChatProjectForUserAction()) { - return; - } - const generationPrompt = prompt - .slice( - prompt.startsWith('/generate ') - ? '/generate '.length - : '/draft '.length, - ) - .trim(); - if (!generationPrompt) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '格式:/generate 创作想法' }, - ]); - return; - } - queuePendingCommand({ - id: 'game.generate_draft', - prompt: generationPrompt, - }); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `准备生成本地游戏草案:${generationPrompt}`, - }, - ]); - return; - } - - if (prompt === '/status') { - void executeProjectStatus(true); - return; - } - - if ( - handleProjectSummaryChatCommand({ - agentRunHistory, - agentRunTrace, - agentStatusCards, - manifest, - prompt, - requireChatProjectForUserAction, - setMessages, - }) - ) { - return; - } - - if (prompt === '/index') { - void queueOrExecuteProjectIndex(); - return; - } - - if (prompt === '/checkpoint') { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: 'project.checkpoint' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备保存当前项目快照。' }, - ]); - return; - } - - if (prompt === '/checkpoints') { - void executeProjectCheckpoints(true); - return; - } - - if (prompt.startsWith('/diff ')) { - const checkpointId = prompt.slice('/diff '.length).trim(); - if (!checkpointId) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '格式:/diff checkpoint-id' }, - ]); - return; - } - if (!isSafeCheckpointId(checkpointId)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: 'checkpoint id 非法。' }, - ]); - return; - } - void executeProjectDiff(checkpointId, true); - return; - } - - if (prompt.startsWith('/restore ')) { - if (!requireChatProjectForUserAction()) { - return; - } - const checkpointId = prompt.slice('/restore '.length).trim(); - if (!checkpointId) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '格式:/restore checkpoint-id' }, - ]); - return; - } - if (!isSafeCheckpointId(checkpointId)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: 'checkpoint id 非法。' }, - ]); - return; - } - queuePendingCommand({ id: 'project.restore', checkpointId }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `准备回滚到 checkpoint:${checkpointId}` }, - ]); - return; - } - - if (prompt === '/policy') { - void executeProjectPolicyRead(true); - return; - } - - if ( - prompt.startsWith('/policy-deny ') || - prompt.startsWith('/policy-allow ') || - prompt.startsWith('/policy-confirm ') || - prompt.startsWith('/policy-auto ') - ) { - if (!requireChatProjectForUserAction()) { - return; - } - const mode = prompt.startsWith('/policy-deny ') - ? 'deny' - : prompt.startsWith('/policy-allow ') - ? 'allow' - : prompt.startsWith('/policy-confirm ') - ? 'confirm' - : 'auto'; - const commandPrefix = - mode === 'deny' - ? '/policy-deny ' - : mode === 'allow' - ? '/policy-allow ' - : mode === 'confirm' - ? '/policy-confirm ' - : '/policy-auto '; - const commandId = prompt.slice(commandPrefix.length).trim(); - if (!commandId) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: - mode === 'deny' || mode === 'allow' - ? '格式:/policy-deny file.write' - : '格式:/policy-confirm project.index', - }, - ]); - return; - } - if (!isRegisteredGameCreationCommandId(commandId)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: `未知内置命令:${commandId}` }, - ]); - return; - } - if ( - (mode === 'confirm' || mode === 'auto') && - !isProjectPolicyConfirmableCommandId(commandId) - ) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: '当前仅支持确认 project.index、project.status、project.bootstrap、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。', - }, - ]); - return; - } - void queueProjectPolicyMutation(commandId, mode); - return; - } - - if ( - prompt.startsWith('/agent-policy-deny ') || - prompt.startsWith('/agent-policy-allow ') || - prompt.startsWith('/agent-policy-confirm ') || - prompt.startsWith('/agent-policy-auto ') - ) { - if (!requireChatProjectForUserAction()) { - return; - } - const mode = prompt.startsWith('/agent-policy-deny ') - ? 'deny' - : prompt.startsWith('/agent-policy-allow ') - ? 'allow' - : prompt.startsWith('/agent-policy-confirm ') - ? 'confirm' - : 'auto'; - const commandPrefix = - mode === 'deny' - ? '/agent-policy-deny ' - : mode === 'allow' - ? '/agent-policy-allow ' - : mode === 'confirm' - ? '/agent-policy-confirm ' - : '/agent-policy-auto '; - const args = prompt.slice(commandPrefix.length).trim(); - const [agentId = '', commandId = ''] = args.split(/\s+/, 2); - if (!agentId || !commandId) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: - mode === 'deny' || mode === 'allow' - ? '格式:/agent-policy-deny design-director file.read' - : '格式:/agent-policy-confirm design-director memory.write', - }, - ]); - return; - } - if (!isRegisteredGameCreationCommandId(commandId)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: `未知内置命令:${commandId}` }, - ]); - return; - } - if ( - (mode === 'confirm' || mode === 'auto') && - !isProjectPolicyConfirmableCommandId(commandId) - ) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: '当前仅支持确认 project.index、project.status、project.bootstrap、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。', - }, - ]); - return; - } - void queueAgentPolicyMutation(agentId, commandId, mode); - return; - } - - if (prompt === '/llm-status') { - void executeLlmConfigStatus(); - return; - } - - if (prompt === '/llm-routes') { - void executeLlmRouteSummary(); - return; - } - - if (prompt === '/open-project' || prompt === '/show-project') { - if (!requireChatProjectForUserAction()) { - return; - } - void handleRevealCurrentProjectDirectory(); - return; - } - - if (prompt === '/switch-project') { - void handleOpenLauncherWindow(); - return; - } - - if (prompt === '/files') { - void executeProjectFiles(true); - return; - } - - if (prompt === '/assets') { - void executeProjectAssets(true); - return; - } - - if (prompt === '/credits') { - if (!requireChatProjectForUserAction()) { - return; - } - const summary = summarizeProjectAssetCredits(manifest); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summary.text, - draftCommand: summary.draftCommand, - draftCommandLabel: summary.draftCommandLabel, - }, - ]); - return; - } - - if (prompt === '/art') { - if (!requireChatProjectForUserAction()) { - return; - } - const summary = summarizeProjectVisualAssets(manifest); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summary.text, - draftCommand: summary.draftCommand, - draftCommandLabel: summary.draftCommandLabel, - }, - ]); - return; - } - - if (prompt === '/audio') { - if (!requireChatProjectForUserAction()) { - return; - } - const summary = summarizeProjectAudioAssets(manifest); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summary.text, - draftCommand: summary.draftCommand, - draftCommandLabel: summary.draftCommandLabel, - }, - ]); - return; - } - - if (prompt === '/artifacts') { - if (!requireChatProjectForUserAction()) { - return; - } - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeCommonProjectArtifactReadDrafts(), - draftCommand: `/read ${commonProjectArtifactReadDrafts[0].path}`, - draftCommandLabel: commonProjectArtifactReadDrafts[0].label, - }, - ]); - return; - } - - if (prompt === '/run-artifacts') { - if (!requireChatProjectForUserAction()) { - return; - } - const artifacts = agentRunTrace - ? readableArtifactsFromAgentRunTrace(agentRunTrace) - : []; - const firstArtifact = artifacts[0] ?? null; - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeRunArtifactReadDrafts(agentRunTrace), - draftCommand: firstArtifact - ? `/read ${firstArtifact.path}` - : undefined, - draftCommandLabel: firstArtifact ? '读取首个 Run 产物' : undefined, - }, - ]); - return; - } - - if (prompt === '/passes') { - if (!requireChatProjectForUserAction()) { - return; - } - const artifacts = agentRunTrace - ? readablePassArtifactsFromAgentRunTrace(agentRunTrace) - : []; - const firstArtifact = artifacts[0] ?? null; - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeAgentPassArtifactReadDrafts(agentRunTrace), - draftCommand: firstArtifact - ? `/read ${firstArtifact.path}` - : undefined, - draftCommandLabel: firstArtifact ? '读取首个轮次产物' : undefined, - }, - ]); - return; - } - - if (prompt === '/runs') { - if (!requireChatProjectForUserAction()) { - return; - } - const firstHistory = agentRunHistory[0] ?? null; - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeAgentRunHistoryReadDrafts( - agentRunTrace, - agentRunHistory, - ), - draftCommand: firstHistory - ? `/read ${firstHistory.path}` - : agentRunTrace - ? '/trace' - : undefined, - draftCommandLabel: firstHistory - ? '读取首个历史 Run' - : agentRunTrace - ? '查看最近 Run' - : undefined, - }, - ]); - return; - } - - if (prompt === '/run-files') { - if (!requireChatProjectForUserAction()) { - return; - } - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeAgentRunSupportFileReadDrafts(), - draftCommand: `/read ${commonAgentRunSupportReadDrafts[0].path}`, - draftCommandLabel: commonAgentRunSupportReadDrafts[0].label, - }, - ]); - return; - } - - if (prompt === '/internals') { - if (!requireChatProjectForUserAction()) { - return; - } - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeProjectInternalReadDrafts(), - draftCommand: `/read ${commonProjectInternalReadDrafts[0].path}`, - draftCommandLabel: commonProjectInternalReadDrafts[0].label, - }, - ]); - return; - } - - if (prompt === '/logs') { - if (!requireChatProjectForUserAction()) { - return; - } - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeCommonProjectLogReadDrafts(), - draftCommand: `/read ${commonProjectLogReadDrafts[0].path}`, - draftCommandLabel: commonProjectLogReadDrafts[0].label, - }, - ]); - return; - } - - if (prompt.startsWith('/asset-register ')) { - if (!requireChatProjectForUserAction()) { - return; - } - const [ - localPath, - kind = 'unknown', - mediaType = 'application/octet-stream', - ] = prompt.slice('/asset-register '.length).trim().split(/\s+/); - if (!localPath) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: '格式:/asset-register assets/hero.png [kind] [mediaType]', - }, - ]); - return; - } - if (!isSafeProjectRelativePath(localPath)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '资产路径必须是项目内相对路径。' }, - ]); - return; - } - queuePendingCommand({ - id: 'asset.register', - localPath, - kind: parseGameCreationAppAssetKind(kind, 'chat.asset-register.kind'), - mediaType, - }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `准备登记项目资产:${localPath}` }, - ]); - return; - } - - if (prompt.startsWith('/read ')) { - const relativePath = prompt.slice('/read '.length).trim(); - if (!relativePath) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '格式:/read game/index.html' }, - ]); - return; - } - if (!isSafeProjectRelativePath(relativePath)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '文件路径必须是项目内相对路径。' }, - ]); - return; - } - void executeProjectFileReadChat(relativePath); - return; - } - - if (prompt === '/tasks') { - void executeProjectTasks(true); - return; - } - - if (prompt === '/agents') { - if (!requireChatProjectForUserAction()) { - return; - } - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeAgentStatusCardsForChat( - agentStatusCards, - llmConfigStatus, - ), - }, - ]); - return; - } - - if (prompt === '/agent-conversations') { - if (!requireChatProjectForUserAction()) { - return; - } - const drafts = agentConversationReadDraftsFromManifest(manifest); - const firstDraft = drafts[0] ?? null; - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeAgentConversationReadDrafts(manifest), - draftCommand: firstDraft ? `/read ${firstDraft.path}` : undefined, - draftCommandLabel: firstDraft - ? `读取${firstDraft.task.title}对话` - : undefined, - }, - ]); - return; - } - - if (prompt === '/agent-memories') { - if (!requireChatProjectForUserAction()) { - return; - } - const drafts = agentMemoryReadDraftsFromManifest(manifest); - const firstDraft = drafts[0] ?? null; - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeAgentMemoryReadDrafts(manifest), - draftCommand: firstDraft ? `/read ${firstDraft.path}` : undefined, - draftCommandLabel: firstDraft - ? `读取${firstDraft.task.title}记忆` - : undefined, - }, - ]); - return; - } - - if (prompt === '/trace' || prompt === '/loop') { - void executeAgentTraceChat(); - return; - } - - if (prompt === '/agent-status') { - void executeAgentRunControl('status', undefined, true); - return; - } - - if (prompt === '/agent-kill') { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: 'agent.kill' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备标记最近 run 为 killed。' }, - ]); - return; - } - - if (prompt === '/agent-retry') { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: 'agent.retry' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备重新运行最近 run 的目标。' }, - ]); - return; - } - - if (prompt === '/agent-resume' || prompt.startsWith('/agent-resume ')) { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ - id: 'agent.resume', - detail: prompt.slice('/agent-resume'.length).trim(), - }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备恢复最近 run。' }, - ]); - return; - } - - if (prompt === '/history') { - const nextProjectPath = requireChatProjectForUserAction(); - if (!nextProjectPath) { - return; - } - void loadProjectConversation(nextProjectPath, false, 'replace'); - return; - } - - if (prompt === '/commands' || prompt === '/limited-commands') { - void executeLimitedCommandList(); - return; - } - - if (prompt === '/smoke') { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ - id: 'command.run_limited', - commandId: 'game.static_smoke', - }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备运行静态入口自检。' }, - ]); - return; - } - - if (prompt === '/run') { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: 'game.run_local' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备运行当前本地游戏。' }, - ]); - return; - } - - if (prompt === '/export') { - queueProjectExportPackageShortcut(); - return; - } - - if (prompt === '/exports') { - void executeProjectExportPackages(true); - return; - } - - if (prompt === '/preview') { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: 'preview.start' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备启动本地预览。' }, - ]); - return; - } - - if (prompt === '/open-preview') { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: 'preview.open' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备打开当前本地预览。' }, - ]); - return; - } - - if (prompt === '/preview-status') { - void executePreviewStatus(true); - return; - } - - if (prompt === '/preview-stop') { - if (!requireChatProjectForUserAction()) { - return; - } - void executePreviewStop(true); - return; - } - - if (prompt === '/memory' || prompt.startsWith('/memory ')) { - if (!requireChatProjectForUserAction()) { - return; - } - const scope = parseOptionalMemoryScope(prompt.slice('/memory'.length)); - if (!scope) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: '格式:/memory [short|long|blackboard]', - }, - ]); - return; - } - void executeMemoryReadChat(scope); - return; - } - - if (prompt.startsWith('/remember ')) { - if (!requireChatProjectForUserAction()) { - return; - } - const { scope, content } = parseRememberInput( - prompt.slice('/remember '.length), - ); - if (!content) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请提供要追加的记忆内容。' }, - ]); - return; - } - queuePendingCommand({ - id: 'memory.write', - scope, - content, - mode: 'append', - }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `准备追加${memoryScopeLabel(scope)}记忆。` }, - ]); - return; - } - - if (prompt.startsWith('/memory-set ')) { - if (!requireChatProjectForUserAction()) { - return; - } - const { scope, content } = parseRememberInput( - prompt.slice('/memory-set '.length), - ); - if (!content) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请提供要保存的记忆内容。' }, - ]); - return; - } - queuePendingCommand({ - id: 'memory.write', - scope, - content, - mode: 'replace', - }); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `准备覆盖保存${memoryScopeLabel(scope)}记忆。`, - }, - ]); - return; - } - - if (prompt === '/forget-memory' || prompt.startsWith('/forget-memory ')) { - if (!requireChatProjectForUserAction()) { - return; - } - const scope = parseOptionalMemoryScope( - prompt.slice('/forget-memory'.length), - ); - if (!scope) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: '格式:/forget-memory [short|long|blackboard]', - }, - ]); - return; - } - queuePendingCommand({ id: 'memory.delete', scope }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `准备删除${memoryScopeLabel(scope)}记忆。` }, - ]); - return; - } - - if (prompt.startsWith('/canvas ')) { - const canvasProjectId = prompt.slice('/canvas '.length).trim(); - if (!canvasProjectId) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请提供画板项目 ID。' }, - ]); - return; - } - if (!isSafeCanvasProjectId(canvasProjectId)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '画板项目 ID 不能包含控制字符。' }, - ]); - return; - } - queuePendingCommand({ id: 'canvas.project_open', canvasProjectId }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `准备打开画板项目:${canvasProjectId}` }, - ]); - return; - } - - if (prompt.startsWith('/sync-canvas-project ')) { - if (!requireChatProjectForUserAction()) { - return; - } - const canvasProjectId = prompt - .slice('/sync-canvas-project '.length) - .trim(); - if (!canvasProjectId) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请提供画板项目 ID。' }, - ]); - return; - } - if (!isSafeCanvasProjectId(canvasProjectId)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '画板项目 ID 不能包含控制字符。' }, - ]); - return; - } - queuePendingCommand({ id: 'canvas.project_sync', canvasProjectId }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `准备同步画板项目资源:${canvasProjectId}` }, - ]); - return; - } - - if (prompt.startsWith('/generate-art ')) { - if (!requireChatProjectForUserAction()) { - return; - } - const generationPrompt = prompt.slice('/generate-art '.length).trim(); - if (!generationPrompt) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请提供美术生成提示词。' }, - ]); - return; - } - queuePendingCommand({ - id: 'canvas.asset_generate', - prompt: generationPrompt, - }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备生成首版美术素材。' }, - ]); - return; - } - - if (prompt.startsWith('/import-canvas-asset ')) { - if (!requireChatProjectForUserAction()) { - return; - } - const [localPath, canvasProjectId, canvasAssetId, kind, mediaType] = - prompt.slice('/import-canvas-asset '.length).trim().split(/\s+/); - if (!localPath || !canvasProjectId || !canvasAssetId) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: '格式:/import-canvas-asset assets/hero.png 画板项目ID 资源ID|object:资产对象ID', - }, - ]); - return; - } - if (!isSafeCanvasProjectId(canvasProjectId)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '画板项目 ID 不能包含控制字符。' }, - ]); - return; - } - if (!isSafeProjectRelativePath(localPath)) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: '画板资产路径必须是项目内相对路径。', - }, - ]); - return; - } - const canvasAssetObjectId = canvasAssetId.startsWith('object:') - ? canvasAssetId.slice('object:'.length).trim() - : ''; - if (canvasAssetId.startsWith('object:') && !canvasAssetObjectId) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: '请提供 object: 后面的资产对象 ID。', - }, - ]); - return; - } - queuePendingCommand({ - id: 'canvas.asset_import', - localPath, - canvasProjectId, - canvasAssetId: canvasAssetObjectId ? '' : canvasAssetId, - canvasAssetObjectId, - kind: parseGameCreationAppAssetKind( - kind ?? 'unknown', - 'chat.canvas-asset-import.kind', - ), - mediaType: mediaType ?? 'application/octet-stream', - }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `准备导入画板资产:${localPath}` }, - ]); - return; - } - - if (prompt.startsWith('/import-canvas-export ')) { - if (!requireChatProjectForUserAction()) { - return; - } - const [exportPath, canvasProjectId] = prompt - .slice('/import-canvas-export '.length) - .trim() - .split(/\s+/); - if (!exportPath || !canvasProjectId) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: '格式:/import-canvas-export /绝对/画板素材.zip 画板项目ID', - }, - ]); - return; - } - if (!isSafeCanvasProjectId(canvasProjectId)) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '画板项目 ID 不能包含控制字符。' }, - ]); - return; - } - if ( - !isAbsoluteProjectPath(exportPath) || - projectPathHasControlCharacter(exportPath) - ) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: '画板导出 ZIP 路径必须是绝对路径。', - }, - ]); - return; - } - queuePendingCommand({ - id: 'canvas.export_import', - exportPath, - canvasProjectId, - }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `准备导入画板导出包:${exportPath}` }, - ]); - return; - } - - if (prompt.startsWith('/')) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `未知命令:${prompt}。输入 /help 查看可用命令。`, - }, - ]); - return; - } - - const clientTurnId = createDirectCodexConversationTurnId(); - const userItem = chatComposerDraftToDirectCodexUserItem( - { text: prompt, references, content: chatContent }, - directCodexConversationMessageId(clientTurnId, 'user'), - ); - void executeChatAgentReply({ prompt, references, userItem, clientTurnId }); - } - - async function executeLlmConfigStatus() { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setLlmConfigStatus(null); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - - try { - const status = await invoke( - 'check_game_creator_llm_config', - ); - setLlmConfigStatus(status); - setCommandLog((current) => [...current, 'llm.config_check']); - const agentLines = (status.agents ?? []).map(formatLlmAgentStatusLine); - const summary = status.configured - ? `${formatLlmRouteEndpoint(status)}。` - : `官方智能服务未就绪;请登录或重新登录后重试。${formatCodexRuntimeCapabilities(status)},账号状态 ${ - status.accountCredentialState === 'login_required' - ? '需要登录' - : '暂不可用' - }。`; - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: [summary, ...agentLines].join('\n'), - }, - ]); - } catch (error) { - setLlmConfigStatus(null); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: error instanceof Error ? error.message : String(error), - }, - ]); - } - } - - async function executeLlmRouteSummary() { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setLlmConfigStatus(null); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - - try { - const status = await invoke( - 'check_game_creator_llm_config', - ); - setLlmConfigStatus(status); - setCommandLog((current) => [...current, 'llm.route_summary']); - const summary = summarizeAgentLlmRoutes(status); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summary.text, - draftCommand: summary.draftCommand, - draftCommandLabel: summary.draftCommandLabel, - }, - ]); - } catch (error) { - setLlmConfigStatus(null); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: error instanceof Error ? error.message : String(error), - }, - ]); - } - } - - async function executeAgentCapabilitiesChat() { - const invoke = resolveTauriInvoke(); - let capabilities: readonly GameCreationAgentCapabilityDescriptor[] = - GAME_CREATION_AGENT_CAPABILITIES; - if (invoke) { - try { - const nativeCapabilities = await invoke< - GameCreationAgentCapabilityDescriptor[] - >('get_game_creation_agent_capabilities'); - if (nativeCapabilities.length > 0) { - capabilities = nativeCapabilities; - } - } catch { - capabilities = GAME_CREATION_AGENT_CAPABILITIES; - } - } - setCommandLog((current) => [...current, 'agent.capabilities']); - setMessages((current) => [ - ...current, - { role: 'assistant', text: summarizeAgentCapabilities(capabilities) }, - ]); - } - - async function resolveProjectSupervisorSessionForSubmission( - invoke: TauriInvoke, - nextProjectPath: string, - ) { - const currentSessionId = projectSupervisorSessionIdRef.current; - if (currentSessionId) { - return currentSessionId; - } - const sessionId = await ensureProjectSupervisorActiveSessionId( - invoke, - nextProjectPath, - ); - if (!sessionId) { - throw new Error('项目总控 Agent active Session 不可用'); - } - if (localProjectPathRef.current !== nextProjectPath) { - return null; - } - projectSupervisorSessionIdRef.current = sessionId; - setProjectSupervisorSessionId(sessionId); - return sessionId; - } - - async function refreshDirectProjectManifest(nextProjectPath: string) { - try { - await refreshManifest(nextProjectPath); + setWorkspaceStatus('等待确认'); + return false; } catch { - // The direct turn result is still authoritative for the chat. A missing - // or legacy manifest must not hide the Codex reply. + return false; } } + /** + * DirectProject 写自己项目对话的门:策略要求确认时入队确认,确认后重跑同一轮。 + */ + async function ensureDirectTurnWriteAllowed(input: { + projectPath: string; + onConfirmed: () => void; + }) { + const invoke = resolveTauriInvoke(); + if (!invoke) return false; + if (projectConversationWriteConfirmedRef.current === input.projectPath) { + return true; + } + try { + const paused = await queueProjectPolicyConfirmationIfNeeded( + invoke, + 'conversation.write', + input.projectPath, + '写入 DirectProject 对话历史', + 'DirectProject 对话写入需要确认。', + () => { + projectConversationWriteConfirmedRef.current = input.projectPath; + input.onConfirmed(); + }, + ); + if (paused) return false; + projectConversationWriteConfirmedRef.current = input.projectPath; + return true; + } catch (error) { + if (localProjectPathRef.current === input.projectPath) { + setProjectChatError( + `DirectProject 对话权限检查失败:${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + return false; + } + } + + /** + * 工作台壳要把一句结果说给用户在项目对话里听。 + * + * DirectProject 的会话由聊天容器持有,壳只把这句话交给聊天的本地消息流; + * 立项策划路径仍写壳自己的 `messages`。 + */ + function announceProjectChatMessage(text: string) { + if (directProjectMode) { + directProjectChatRef.current?.announce(text); + return; + } + setMessages((current) => [...current, { role: 'assistant', text }]); + } + async function executeChatAgentReply({ prompt, clientTurnId: directConversationTurnId, - creationType, - attachments, - directPolicyChecked = false, - references, - userItem, }: ExecuteChatAgentReplyInput) { - if (designAgentActiveRef.current) { - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setProjectSupervisorRuntimeError('请先初始化本地项目'); - return; - } - if (designAgentActiveRef.current) { - await executeDesignAgentTurn( - nextProjectPath, - { type: 'message', text: prompt }, - directConversationTurnId ?? createAgentChatRunId('design-agent-turn'), - ); - return; - } - return; - } - // Product default: send the conversation directly to Codex app-server. - // The legacy Supervisor/harness path remains below for rollback and tests. - if (directCodexProductRuntime) { - const directInvoke = resolveTauriInvoke(); - // Capture the project snapshot before any asynchronous policy/session work. - // `resolveChatProjectPath` only returns a path and TypeScript cannot infer - // that the source project is still non-null after an await; keeping the - // immutable snapshot also prevents a project switch from changing the - // projectId used by this turn halfway through submission. - const directProject = localProject; - const directProjectPath = resolveChatProjectPath(directProject); - const directProjectId = directProject?.manifest.projectId; - if (directProjectPath && directProjectId && directInvoke) { - const clientTurnId = - directConversationTurnId ?? createDirectCodexConversationTurnId(); - const effectiveUserItem = - userItem ?? - chatComposerDraftToDirectCodexUserItem( - { text: prompt, references: references ?? [], content: [] }, - directCodexConversationMessageId(clientTurnId, 'user'), - ); - if ( - !directPolicyChecked && - projectConversationWriteConfirmedRef.current !== directProjectPath - ) { - // 首轮的入参整体留一份给「确认后重跑」用,不在回调里重列字段。 - const policyRetryInput = directCodexPolicyRetryInput({ - prompt, - clientTurnId, - creationType, - attachments, - references, - userItem: effectiveUserItem, - }); - try { - const policyPaused = await queueProjectPolicyConfirmationIfNeeded( - directInvoke, - 'conversation.write', - directProjectPath, - '写入 DirectProject 对话历史', - 'DirectProject 对话写入需要确认。', - () => { - projectConversationWriteConfirmedRef.current = - directProjectPath; - void executeChatAgentReply(policyRetryInput); - }, - ); - if (policyPaused) { - return; - } - projectConversationWriteConfirmedRef.current = directProjectPath; - } catch (error) { - if (localProjectPathRef.current === directProjectPath) { - setProjectSupervisorRuntimeError( - `DirectProject 对话权限检查失败:${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - return; - } - } - const directUserMessageId = directCodexConversationMessageId( - clientTurnId, - 'user', - ); - const directAssistantMessageId = directCodexConversationMessageId( - clientTurnId, - 'assistant', - ); - const appendDirectUserMessageIfMissing = ( - current: ChatMessage[], - ): ChatMessage[] => { - if ( - current.some((message) => message.messageId === directUserMessageId) - ) { - return current; - } - let optimisticIndex = -1; - for (let index = current.length - 1; index >= 0; index -= 1) { - const message = current[index]; - if ( - message?.role === 'user' && - message.text === prompt && - !message.messageId - ) { - optimisticIndex = index; - break; - } - } - if (optimisticIndex >= 0) { - return current.map((message, index) => - index === optimisticIndex - ? { - ...message, - runtimeOwned: true, - messageId: directUserMessageId, - updatedAt: Date.now(), - } - : message, - ); - } - return [ - ...current, - { - role: 'user' as const, - text: prompt, - runtimeOwned: true, - messageId: directUserMessageId, - updatedAt: Date.now(), - }, - ]; - }; - const appendDirectAssistantMessage = ( - current: ChatMessage[], - text: string, - failed = false, - ): ChatMessage[] => { - const messageId = failed - ? `direct-codex:${clientTurnId}:failure` - : directAssistantMessageId; - const nextMessage: ChatMessage = { - role: 'assistant', - text, - runtimeOwned: true, - messageId, - updatedAt: Date.now(), - }; - const withUser = appendDirectUserMessageIfMissing(current); - const existingIndex = withUser.findIndex( - (message) => message.messageId === messageId, - ); - if (existingIndex < 0) { - return [...withUser, nextMessage]; - } - return withUser.map((message, index) => - index === existingIndex ? nextMessage : message, - ); - }; - setChatAgentBusy(true); - setProjectSupervisorRuntimeError(''); - try { - const directTurnInput: { - projectPath: string; - prompt: string; - clientTurnId: string; - creationType?: HomeCreationType; - attachments?: DirectCodexTurnAttachment[]; - userItem: ReturnType; - } = { - projectPath: directProjectPath, - prompt, - clientTurnId, - userItem: effectiveUserItem, - }; - if (creationType) { - directTurnInput.creationType = creationType; - } - if (attachments?.length) { - directTurnInput.attachments = attachments; - } - directTurnInput.userItem = effectiveUserItem; - // 回复正文按条目身份从线程事件 / 历史切片进聊天,本地不再补一条, - // 因此这里只等回合跑完,不接返回值。 - await withDirectCodexSessionRefresh(() => { - return directInvoke( - 'chat_with_game_creator_direct_codex', - directTurnInput, - ); - }); - // Rust already persisted the complete raw response items. Invalidate - // any history snapshot captured before the turn completed. - if (localProjectPathRef.current === directProjectPath) { - projectSupervisorHistoryLoadVersionRef.current += 1; - } - if (localProjectPathRef.current === directProjectPath) { - await refreshDirectProjectManifest(directProjectPath); - } - } catch (error) { - if ( - isDirectCodexTurnAlreadyRunningError(error) || - isDirectCodexAnotherTurnRunningError(error) - ) { - if (localProjectPathRef.current === directProjectPath) { - setProjectSupervisorRuntimeError( - '陶泥儿仍在处理上一条消息,可在输入盒点「终止」结束它,或等它结束后再发送。', - ); - } - return; - } - if (localProjectPathRef.current !== directProjectPath) { - return; - } - if (isDirectCodexTurnInterruptedError(error)) { - // 用户主动终止:不是失败,不写运行错误与诊断,只把回合标记成已终止。 - setProjectSupervisorRuntimeError(''); - setChatComposerNotice('已终止本次回合'); - setMessages((current) => - appendDirectAssistantMessage(current, '已终止本次回合。', true), - ); - return; - } - void captureAgentRuntimeError(error, PROJECT_SUPERVISOR_AGENT_ID); - const message = - error instanceof Error ? error.message : String(error); - let persistedDetail = ''; - const detailRef = message.match( - /详情:(\.agent\/runtime\/errors\/[^\s;]+)/, - )?.[1]; - if (detailRef && directInvoke) { - try { - persistedDetail = await directInvoke( - 'read_agent_runtime_error_detail', - { - projectPath: directProjectPath, - detailRef, - }, - ); - } catch { - persistedDetail = ''; - } - } - const visibleMessage = projectRuntimeVisibleError( - persistedDetail ? `${message}\n\n${persistedDetail}` : message, - '陶泥儿智能创作', - true, - ); - projectSupervisorHistoryLoadVersionRef.current += 1; - if (localProjectPathRef.current === directProjectPath) { - setProjectSupervisorRuntimeError(visibleMessage); - setMessages((current) => - appendDirectAssistantMessage(current, visibleMessage, true), - ); - } - } finally { - try { - if (localProjectPathRef.current === directProjectPath) { - await refreshManifest(directProjectPath); - } - } finally { - if (localProjectPathRef.current === directProjectPath) { - setChatAgentBusy(false); - setDirectCodexTurnCancelling(false); - chatAgentBusyRef.current = false; - // 本地 invoke 正常收尾仍可直接推进队列;恢复场景则由 turn.completed effect 推进。 - dispatchNextQueuedChatTurn(); - } - } - } - return; - } - const message = directInvoke - ? '当前项目尚未准备好,无法启动智能创作。' - : '需要在 Tauri App 内运行,无法启动智能创作。'; - setProjectSupervisorRuntimeError(message); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: message, - runtimeOwned: true, - ...(directConversationTurnId - ? { - messageId: directCodexConversationMessageId( - directConversationTurnId, - 'assistant', - ), - } - : {}), - updatedAt: Date.now(), - }, - ]); - return; - } - if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) { - setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题'); - return; - } - const nextProjectPath = requireChatProjectForUserAction(); + const nextProjectPath = resolveChatProjectPath(localProject); if (!nextProjectPath) { + setProjectChatError('请先初始化本地项目'); return; } - const invoke = resolveTauriInvoke(); - if (!invoke) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - - setChatAgentBusy(true); - setProjectSupervisorRuntimeError(''); - try { - const sessionId = await resolveProjectSupervisorSessionForSubmission( - invoke, - nextProjectPath, - ); - if (!sessionId || localProjectPathRef.current !== nextProjectPath) { - return; - } - const runtimeAtSubmission = projectSupervisorRuntimeRef.current; - const submissionRoute = resolveProjectSupervisorRuntimeSubmission({ - workspaceProjectKind, - orchestrationMode, - supervisorChatOnly, - }); - const submission = await submitProjectSupervisorRuntimeTask({ - invoke, - projectPath: nextProjectPath, - sessionId, - prompt, - runtime: runtimeAtSubmission, - runProfile: submissionRoute.runProfile, - source: submissionRoute.source, - }); - const runtimeResult = submission.runtimeResult; - const acceptedRunId = submission.acceptedRunId.trim(); - if (!acceptedRunId) { - throw new Error('项目总控 Agent 请求未进入后台队列'); - } - setCommandLog((current) => [ - ...current, - `agent.runtime.${submission.mode} project-supervisor`, - ]); - const runtime = agentRuntimeStateFromResult( - runtimeResult, - projectSupervisorRuntimeRef.current, - ); - if ( - localProjectPathRef.current !== nextProjectPath || - projectSupervisorSessionIdRef.current !== sessionId - ) { - return; - } - if ( - runtime.agentId !== PROJECT_SUPERVISOR_AGENT_ID || - runtime.sessionId !== sessionId - ) { - throw new Error('项目总控 Agent Runtime Session 身份不匹配'); - } - const acceptedRuntimeReady = runtime.runId === acceptedRunId; - if (!acceptedRuntimeReady) { - projectSupervisorExpectedRunIdRef.current = acceptedRunId; - setProjectSupervisorExpectedRunId(acceptedRunId); - } - if (acceptedRuntimeReady) { - projectSupervisorExpectedRunIdRef.current = null; - setProjectSupervisorExpectedRunId(null); - updateProjectSupervisorRuntime(runtime); - updateProjectSupervisorResponseStream( - runtimeResult.responseStream, - runtime, - ); - } - setProjectSupervisorRuntimeError(''); - const refreshConversation = - projectSupervisorRefreshConversationRef.current; - if (refreshConversation) { - void refreshConversation(invoke, nextProjectPath, sessionId).catch( - (error) => { - if ( - localProjectPathRef.current === nextProjectPath && - projectSupervisorSessionIdRef.current === sessionId - ) { - setProjectSupervisorRuntimeError( - `项目总控 Agent 对话刷新失败:${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - }, - ); - } - if (acceptedRuntimeReady) { - syncTerminalProjectSupervisorConversation( - invoke, - nextProjectPath, - runtime, - ); - } - } catch (error) { - if (localProjectPathRef.current !== nextProjectPath) { - return; - } - void captureAgentRuntimeError(error, PROJECT_SUPERVISOR_AGENT_ID); - const message = error instanceof Error ? error.message : String(error); - if (isRuntimeConfigMissingError(message)) { - requestRuntimeConfigOpen(); - } - setProjectSupervisorRuntimeError(message); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: message, - runtimeOwned: true, - updatedAt: Date.now(), - }, - ]); - } finally { - setChatAgentBusy(false); - } + await executeDesignAgentTurn( + nextProjectPath, + { type: 'message', text: prompt }, + directConversationTurnId ?? createAgentChatRunId('design-agent-turn'), + ); } executeChatAgentReplyRef.current = executeChatAgentReply; useEffect(() => { - const latch = initialSupervisorMessageLatchRef.current; - if ( - (!directCodexProductRuntime && !planningStartMode) || - !latch.prompt || - !localProject - ) { + const latch = initialPlanningPromptLatchRef.current; + if (!planningStartMode || !latch.prompt || !localProject) { return; } if (localProject.projectPath !== latch.projectPath) { @@ -6383,898 +1684,36 @@ export function App({ } if ( chatAgentBusy || - !claimInitialSupervisorMessageForPage(latch.projectPath, latch.claimScope) + !claimInitialTurnForPage(latch.projectPath, latch.claimScope) ) { return; } - supervisorChatShouldFollowLatestRef.current = true; - const directConversationTurnId = directCodexProductRuntime - ? createDirectCodexConversationTurnId() - : undefined; + planningChatShouldFollowLatestRef.current = true; setMessages((current) => [ ...current, { role: 'user', text: latch.prompt, runtimeOwned: true, - ...(directConversationTurnId - ? { - messageId: directCodexConversationMessageId( - directConversationTurnId, - 'user', - ), - } - : {}), updatedAt: Date.now(), }, ]); - void executeChatAgentReplyRef.current({ - prompt: latch.prompt, - clientTurnId: directConversationTurnId, - creationType: latch.creationType, - attachments: latch.attachments, - }); + // DirectProject 的首轮需求由聊天容器自己认领并发送(见 `DirectProjectChat`)。 + void executeChatAgentReplyRef.current({ prompt: latch.prompt }); + // 初始消息 latch 只由入口状态驱动;controller 内部函数保持稳定语义。 + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ chatAgentBusy, - directCodexProductRuntime, initialCreationType, - initialSupervisorMessage, + initialPlanningPrompt, localProject, planningStartMode, ]); - async function handleProjectSupervisorToolAction( - decision: 'confirm' | 'reject', - ) { - const nextProjectPath = resolveChatProjectPath(localProject); - const runtime = projectSupervisorRuntimeRef.current; - const pendingAction = runtime?.pendingToolAction; - const sessionId = projectSupervisorSessionIdRef.current; - const invoke = resolveTauriInvoke(); - if ( - !invoke || - !nextProjectPath || - !runtime || - !pendingAction?.actionId || - !sessionId || - chatAgentBusy - ) { - return; - } - setChatAgentBusy(true); - setProjectSupervisorRuntimeError(''); - try { - const command = - decision === 'confirm' - ? 'confirm_game_creator_agent_runtime_task' - : 'reject_game_creator_agent_runtime_task'; - const result = await invoke(command, { - projectPath: nextProjectPath, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - runId: runtime.runId, - actionId: pendingAction.actionId, - note: - decision === 'confirm' - ? '用户已确认待执行工具动作' - : '用户已拒绝待执行工具动作', - }); - const nextRuntime = agentRuntimeStateFromResult( - result, - projectSupervisorRuntimeRef.current, - ); - if ( - localProjectPathRef.current !== nextProjectPath || - projectSupervisorSessionIdRef.current !== sessionId || - nextRuntime.sessionId !== sessionId || - nextRuntime.runId !== runtime.runId - ) { - return; - } - updateProjectSupervisorRuntime(nextRuntime); - updateProjectSupervisorResponseStream(result.responseStream, nextRuntime); - setCommandLog((current) => [ - ...current, - `agent.runtime.${decision} project-supervisor`, - ]); - syncTerminalProjectSupervisorConversation( - invoke, - nextProjectPath, - nextRuntime, - ); - } catch (error) { - if ( - localProjectPathRef.current !== nextProjectPath || - projectSupervisorSessionIdRef.current !== sessionId - ) { - return; - } - const message = error instanceof Error ? error.message : String(error); - if (isRuntimeConfigMissingError(message)) { - requestRuntimeConfigOpen(); - } - setProjectSupervisorRuntimeError(message); - } finally { - setChatAgentBusy(false); - } - } - - async function handleProjectProfessionalAgentToolAction( - runtime: AgentRuntimeState, - decision: 'confirm' | 'reject', - ) { - const nextProjectPath = resolveChatProjectPath(localProject); - const currentRuntime = agentRuntimeById[runtime.agentId]; - const pendingAction = currentRuntime?.pendingToolAction; - const supervisorRunId = projectSupervisorRuntimeRef.current?.runId; - const invoke = resolveTauriInvoke(); - if ( - !invoke || - !nextProjectPath || - !currentRuntime || - !pendingAction?.actionId || - chatAgentBusy - ) { - throw new Error('专业 Agent 待确认动作已变化,请等待状态刷新'); - } - if ( - currentRuntime.runId !== runtime.runId || - pendingAction.actionId !== runtime.pendingToolAction?.actionId || - currentRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID || - currentRuntime.parentRunId !== supervisorRunId - ) { - throw new Error('专业 Agent Runtime 身份已变化,请等待状态刷新'); - } - setChatAgentBusy(true); - try { - const command = - decision === 'confirm' - ? 'confirm_game_creator_agent_runtime_task' - : 'reject_game_creator_agent_runtime_task'; - const result = await invoke(command, { - projectPath: nextProjectPath, - agentId: currentRuntime.agentId, - runId: currentRuntime.runId, - actionId: pendingAction.actionId, - note: - decision === 'confirm' - ? '用户已确认专业 Agent 待执行工具动作' - : '用户已拒绝专业 Agent 待执行工具动作', - }); - if ( - localProjectPathRef.current !== nextProjectPath || - projectSupervisorRuntimeRef.current?.runId !== supervisorRunId - ) { - return; - } - const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime); - if ( - nextRuntime.agentId !== currentRuntime.agentId || - nextRuntime.runId !== currentRuntime.runId - ) { - throw new Error('专业 Agent 操作后 Runtime 身份不匹配'); - } - rememberAgentRuntimeState(nextRuntime); - setCommandLog((current) => [ - ...current, - `agent.runtime.${decision} ${currentRuntime.agentId}`, - ]); - } catch (error) { - throw new Error( - `专业 Agent 操作失败:${ - error instanceof Error ? error.message : String(error) - }`, - ); - } finally { - setChatAgentBusy(false); - } - } - - async function handleProjectProfessionalAgentRetry( - runtime: AgentRuntimeState, - ): Promise { - const nextProjectPath = resolveChatProjectPath(localProject); - const currentRuntime = agentRuntimeById[runtime.agentId]; - const supervisorRunId = projectSupervisorRuntimeRef.current?.runId; - const invoke = resolveTauriInvoke(); - if (!invoke || !nextProjectPath || !currentRuntime || chatAgentBusy) { - throw new Error('专业 Agent 状态已变化,请等待刷新后重试'); - } - const supervisorRuntime = projectSupervisorRuntimeRef.current; - if (!supervisorRuntime || isAgentRuntimeTerminalState(supervisorRuntime)) { - throw new Error('项目总控已停止,请先重试项目总控'); - } - if ( - currentRuntime.runId !== runtime.runId || - currentRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID || - currentRuntime.parentRunId !== supervisorRunId || - !( - currentRuntime.status === 'failed' || currentRuntime.phase === 'failed' - ) || - currentRuntime.pendingToolAction - ) { - throw new Error('专业 Agent 当前状态不允许直接重试'); - } - if ( - projectSupervisorPendingRepairMatchesProfessional( - projectSupervisorRuntimeRef.current?.pendingToolAction, - currentRuntime, - ) - ) { - await handleProjectSupervisorToolAction('confirm'); - return `已确认项目总控安排${projectProfessionalAgentLabel( - currentRuntime.agentId, - )}返工,正在同步新一轮状态`; - } - setChatAgentBusy(true); - try { - const result = await invoke( - 'confirm_retry_game_creator_agent_runtime_task', - { - projectPath: nextProjectPath, - agentId: currentRuntime.agentId, - runId: currentRuntime.runId, - nextRunId: createAgentChatRunId('project-professional-retry'), - }, - ); - if ( - localProjectPathRef.current !== nextProjectPath || - projectSupervisorRuntimeRef.current?.runId !== supervisorRunId - ) { - return '项目已切换,未把旧项目的重试状态合并到当前界面'; - } - const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime); - if ( - nextRuntime.agentId !== currentRuntime.agentId || - nextRuntime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID || - nextRuntime.parentRunId !== supervisorRunId - ) { - throw new Error('专业 Agent 重试后的 Runtime 身份不匹配'); - } - rememberAgentRuntimeState(nextRuntime); - setCommandLog((current) => [ - ...current, - `agent.runtime.retry ${currentRuntime.agentId}`, - ]); - return '重试请求已受理,正在同步新一轮状态'; - } catch (error) { - throw new Error( - `专业 Agent 重试失败:${ - error instanceof Error ? error.message : String(error) - }`, - ); - } finally { - setChatAgentBusy(false); - } - } - - async function handleProjectSupervisorRetry( - runtime: AgentRuntimeState, - ): Promise { - const nextProjectPath = resolveChatProjectPath(localProject); - const currentRuntime = projectSupervisorRuntimeRef.current; - const invoke = resolveTauriInvoke(); - if (!invoke || !nextProjectPath || !currentRuntime || chatAgentBusy) { - throw new Error('项目总控状态已变化,请等待刷新后重试'); - } - const needsReconciliation = - currentRuntime.status === 'needs-reconciliation' || - currentRuntime.phase === 'needs-reconciliation'; - if (needsReconciliation) { - if ( - currentRuntime.runId !== runtime.runId || - currentRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID - ) { - throw new Error('项目总控待核对任务已变化,请等待刷新'); - } - const previousRunId = currentRuntime.runId; - setChatAgentBusy(true); - setProjectSupervisorRuntimeError(''); - try { - const result = await invoke( - 'cancel_game_creator_agent_runtime_task', - { - projectPath: nextProjectPath, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - runId: previousRunId, - }, - ); - if ( - localProjectPathRef.current !== nextProjectPath || - projectSupervisorRuntimeRef.current?.runId !== previousRunId - ) { - return '项目已切换,未把旧项目的取消状态合并到当前界面'; - } - const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime); - if ( - nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID || - nextRuntime.runId !== previousRunId || - nextRuntime.sessionId !== currentRuntime.sessionId - ) { - throw new Error('项目总控取消后的 Runtime 身份不匹配'); - } - updateProjectSupervisorRuntime(nextRuntime); - updateProjectSupervisorResponseStream( - result.responseStream, - nextRuntime, - ); - setCommandLog((current) => [ - ...current, - 'agent.runtime.cancel project-supervisor reconciliation', - ]); - const queuePending = nextRuntime.taskQueue?.pending ?? 0; - if ( - nextRuntime.status === 'cancelled' || - nextRuntime.phase === 'cancelled' - ) { - return queuePending > 0 - ? `旧任务已结束,队列中还有 ${queuePending} 个待处理任务,队列将继续处理` - : '旧任务已结束,当前队列为空,可重新启动项目总控'; - } - return '已提交结束旧任务请求,正在同步取消状态'; - } catch (error) { - throw new Error( - `项目总控旧任务结束失败:${ - error instanceof Error ? error.message : String(error) - }`, - ); - } finally { - setChatAgentBusy(false); - } - } - const cancelledWithEmptyQueue = - (currentRuntime.status === 'cancelled' || - currentRuntime.phase === 'cancelled') && - (currentRuntime.taskQueue?.pending ?? 0) === 0; - if ( - currentRuntime.runId !== runtime.runId || - currentRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID || - !( - currentRuntime.status === 'failed' || - currentRuntime.phase === 'failed' || - cancelledWithEmptyQueue - ) || - currentRuntime.pendingToolAction - ) { - throw new Error('项目总控当前状态不允许重试'); - } - const previousRunId = currentRuntime.runId; - const nextRunId = createAgentChatRunId('project-supervisor-retry'); - setChatAgentBusy(true); - setProjectSupervisorRuntimeError(''); - try { - const result = await invoke( - 'confirm_retry_game_creator_agent_runtime_task', - { - projectPath: nextProjectPath, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - runId: previousRunId, - nextRunId, - }, - ); - if ( - localProjectPathRef.current !== nextProjectPath || - projectSupervisorRuntimeRef.current?.runId !== previousRunId - ) { - return '项目已切换,未把旧项目的重试状态合并到当前界面'; - } - const inferredRunId = agentRuntimeStartedRunId(result, nextRunId); - const acceptedRunId = - result.acceptedRunId?.trim() || - (inferredRunId === nextRunId ? nextRunId : ''); - if (!acceptedRunId) { - throw new Error('项目总控重试请求未进入后台队列'); - } - projectSupervisorExpectedRunIdRef.current = acceptedRunId; - setProjectSupervisorExpectedRunId(acceptedRunId); - const nextRuntime = agentRuntimeStateFromResult(result, currentRuntime); - if (nextRuntime.runId === acceptedRunId) { - if ( - nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID || - nextRuntime.sessionId !== currentRuntime.sessionId - ) { - throw new Error('项目总控重试后的 Runtime 身份不匹配'); - } - updateProjectSupervisorRuntime(nextRuntime); - updateProjectSupervisorResponseStream( - result.responseStream, - nextRuntime, - ); - projectSupervisorExpectedRunIdRef.current = null; - setProjectSupervisorExpectedRunId(null); - } - setCommandLog((current) => [ - ...current, - 'agent.runtime.retry project-supervisor', - ]); - return nextRuntime.runId === acceptedRunId - ? '已在当前项目重新启动项目总控' - : '重试已受理,正在同步新一轮项目总控状态'; - } catch (error) { - throw new Error( - `项目总控重试失败:${ - error instanceof Error ? error.message : String(error) - }`, - ); - } finally { - setChatAgentBusy(false); - } - } - - async function handleProjectSupervisorUserInput( - request: AgentRuntimeUserInputRequest, - responseId: string, - answers: Record, - ) { - const nextProjectPath = resolveChatProjectPath(localProject); - const runtime = projectSupervisorRuntimeRef.current; - const sessionId = projectSupervisorSessionIdRef.current; - const currentRequest = runtime?.userInputRequest; - const invoke = resolveTauriInvoke(); - if ( - !invoke || - !nextProjectPath || - !runtime || - !sessionId || - !responseId || - chatAgentBusy - ) { - return; - } - if ( - request.agentId !== PROJECT_SUPERVISOR_AGENT_ID || - request.sessionId !== sessionId || - request.runId !== runtime.runId || - currentRequest?.requestId !== request.requestId || - currentRequest?.actionId !== request.actionId - ) { - setProjectSupervisorRuntimeError('待回答请求已变更,请刷新 Runtime 状态'); - return; - } - setChatAgentBusy(true); - setProjectSupervisorRuntimeError(''); - try { - const result = await invoke( - 'answer_game_creator_agent_runtime_user_input', - { - projectPath: nextProjectPath, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - runId: request.runId, - actionId: request.actionId, - requestId: request.requestId, - responseId, - answers, - }, - ); - const nextRuntime = agentRuntimeStateFromResult( - result, - projectSupervisorRuntimeRef.current, - ); - if ( - localProjectPathRef.current !== nextProjectPath || - projectSupervisorSessionIdRef.current !== sessionId - ) { - return; - } - if ( - nextRuntime.agentId !== PROJECT_SUPERVISOR_AGENT_ID || - nextRuntime.sessionId !== sessionId || - nextRuntime.runId !== request.runId - ) { - throw new Error('项目总控 Agent 回答后 Runtime 身份不匹配'); - } - updateProjectSupervisorRuntime(nextRuntime); - updateProjectSupervisorResponseStream(result.responseStream, nextRuntime); - const refreshConversation = - projectSupervisorRefreshConversationRef.current; - if (refreshConversation) { - await refreshConversation(invoke, nextProjectPath, sessionId); - } - setCommandLog((current) => [ - ...current, - 'agent.runtime.user_input.answered project-supervisor', - ]); - setProjectSupervisorRuntimeError(''); - syncTerminalProjectSupervisorConversation( - invoke, - nextProjectPath, - nextRuntime, - ); - } catch (error) { - if ( - localProjectPathRef.current !== nextProjectPath || - projectSupervisorSessionIdRef.current !== sessionId - ) { - return; - } - const message = error instanceof Error ? error.message : String(error); - if (isRuntimeConfigMissingError(message)) { - requestRuntimeConfigOpen(); - } - setProjectSupervisorRuntimeError(`回答提交失败:${message}`); - } finally { - setChatAgentBusy(false); - } - } - - async function executeGameDraft(prompt: string) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - const nextProjectPath = requireChatProjectForUserAction(); - if (!nextProjectPath) { - return; - } - - try { - setCommandLog((current) => [...current, 'game.generate_draft']); - setProjectStatus('正在调用 LLM'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: gameDraftStartedMessage() }, - ]); - const result = await invoke( - 'generate_local_game_draft', - { projectPath: nextProjectPath, prompt }, - ); - const generatedProjectPath = result.projectPath.trim(); - if ( - !generatedProjectPath || - !isAbsoluteProjectPath(generatedProjectPath) || - projectPathHasControlCharacter(generatedProjectPath) - ) { - const message = '生成结果项目路径无效'; - setProjectStatus(message); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - return; - } - setLocalProject({ - projectPath: generatedProjectPath, - manifestPath: `${generatedProjectPath}/.agent/manifest.json`, - manifest: result.manifest, - }); - setManifest(result.manifest); - setProjectStatus('已生成可试玩原型'); - setCommandLog((current) => [ - ...current, - 'memory.write', - 'file.write game/index.html', - ]); - const completionSummary = - await refreshAgentRunTrace(generatedProjectPath); - - try { - const previewResult = await invoke( - 'start_local_game_preview', - { projectPath: generatedProjectPath }, - ); - updateClientPreview(previewResult); - setPreviewStatus(`运行中:127.0.0.1:${previewResult.port}`); - setCommandLog((current) => [...current, 'preview.start']); - void refreshManifest(generatedProjectPath); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: [ - `已保存并在客户端运行视图启动预览:${previewResult.url}`, - completionSummary?.text, - ] - .filter(Boolean) - .join('\n\n'), - draftCommand: completionSummary?.draftCommand, - }, - ]); - } catch (previewError) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: [ - `已保存草案:${result.designPath}。预览启动失败:${ - previewError instanceof Error - ? previewError.message - : String(previewError) - }`, - completionSummary?.text, - ] - .filter(Boolean) - .join('\n\n'), - draftCommand: completionSummary?.draftCommand, - }, - ]); - } - } catch (error) { - void refreshAgentRunTrace(nextProjectPath); - const message = error instanceof Error ? error.message : String(error); - if (isRuntimeConfigMissingError(message)) { - requestRuntimeConfigOpen(); - } - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: message, - }, - ]); - } - } - - function markPendingCommandCanceled(command: PendingCommand) { - if (command.id === 'project.create') { - setWorkspaceStatus('已取消'); - setProjectStatus('已取消'); - return; - } - if (command.id === 'game.generate_draft') { - setProjectStatus('已取消生成游戏草案'); - return; - } - if ( - command.id === 'game.run_local' || - command.id === 'command.run_limited' - ) { - setLimitedCommandStatus('已取消运行'); - return; - } - if (command.id === 'project.checkpoint') { - setFileStatus('已取消保存 checkpoint'); - return; - } - if (command.id === 'project.export_package') { - setFileStatus('已取消导出本地试玩包'); - return; - } - if (command.id === 'project.index') { - setFileStatus('已取消索引项目'); - setWorkspaceStatus('已取消索引项目'); - return; - } - if (command.id === 'project.restore') { - setFileStatus('已取消回滚项目'); - return; - } - if (command.id === 'project.policy_write') { - setProjectStatus('已取消修改权限策略'); - return; - } - if (command.id === 'preview.start' || command.id === 'preview.open') { - setPreviewStatus('已取消预览操作'); - setWorkspaceStatus('已取消预览操作'); - return; - } - if ( - command.id === 'agent.kill' || - command.id === 'agent.retry' || - command.id === 'agent.resume' - ) { - setAgentRunStatus('已取消 Agent run 操作'); - return; - } - if (command.id === 'memory.write') { - setMemoryStatus('已取消写入项目记忆'); - return; - } - if (command.id === 'memory.delete') { - setMemoryStatus('已取消删除项目记忆'); - return; - } - if (command.id === 'asset.upload') { - setAssetStatus('已取消上传资产'); - return; - } - if (command.id === 'asset.register') { - setAssetStatus('已取消登记资产'); - return; - } - if (command.id === 'canvas.project_open') { - setAssetStatus('已取消打开画板'); - return; - } - if (command.id === 'canvas.project_sync') { - setAssetStatus('已取消同步画板项目'); - return; - } - if (command.id === 'canvas.asset_import') { - setAssetStatus('已取消导入画板资产'); - return; - } - if (command.id === 'canvas.asset_generate') { - setAssetStatus('已取消生成美术素材'); - return; - } - if (command.id === 'canvas.export_import') { - setAssetStatus('已取消导入画板导出包'); - } - } - - function handlePendingCommandCancel() { - const command = pendingCommand; - setPendingCommand(null); - if (command) { - markPendingCommandCanceled(command); - appendLocalPermissionLog( - resolvePermissionLogProjectPath(command), - 'permission.cancel', - command.id, - ); - setCommandLog((current) => [ - ...current, - `permission.cancel ${command.id}`, - ]); - } - setMessages((current) => [ - ...current, - { role: 'assistant', text: '已取消。' }, - ]); - } - - async function handlePendingCommandConfirm() { - const command = pendingCommand; - if (!command) { - return; - } - if ( - needsInitializedChatProject(command.id) && - !resolveChatProjectPath(localProject) - ) { - setPendingCommand(null); - appendLocalPermissionLog( - resolvePermissionLogProjectPath(command), - 'permission.cancel', - command.id, - ); - setCommandLog((current) => [ - ...current, - `permission.cancel ${command.id} missing-project`, - ]); - requireChatProjectForUserAction(); - return; - } - if ( - await denyPendingCommandIfNeeded( - command.id, - resolvePermissionLogProjectPath(command), - ) - ) { - setPendingCommand(null); - return; - } - setPendingCommand(null); - appendLocalPermissionLog( - resolvePermissionLogProjectPath(command), - 'permission.confirm', - command.id, - ); - setCommandLog((current) => [ - ...current, - `permission.confirm ${command.id}`, - ]); - if (command.id === 'asset.upload') { - void executeAssetUpload(command.file); - } else if (command.id === 'asset.register') { - void executeAssetRegister( - resolveChatProjectPath(localProject) ?? '', - command.localPath, - command.kind, - command.mediaType, - 'generated', - '', - '', - '', - true, - ); - } else if (command.id === 'game.run_local') { - void executeRunLocal(true); - } else if (command.id === 'command.run_limited') { - void executeLimitedCommand(command.commandId, true); - } else if (command.id === 'project.create') { - void executeProjectCreate(command.projectPath, true); - } else if (command.id === 'project.checkpoint') { - void executeProjectCheckpoint(true); - } else if (command.id === 'project.export_package') { - void executeProjectExportPackage(true); - } else if (command.id === 'project.index') { - void executeProjectIndex(true); - } else if (command.id === 'project.restore') { - void executeProjectRestore(command.checkpointId, true); - } else if (command.id === 'project.policy_write') { - void executeProjectPolicyWrite(command.policy, true); - } else if (command.id === 'preview.start') { - void executePreviewStart(true); - } else if (command.id === 'preview.open') { - void executePreviewOpen(true); - } else if ( - command.id === 'agent.kill' || - command.id === 'agent.retry' || - command.id === 'agent.resume' - ) { - void executeAgentRunControl( - command.id.slice('agent.'.length), - command.detail, - true, - ); - } else if (command.id === 'memory.write') { - void executeMemoryWrite( - command.scope, - command.content, - command.mode ?? 'append', - ); - } else if (command.id === 'memory.delete') { - void executeMemoryDeleteChat(command.scope); - } else if (command.id === 'canvas.project_open') { - void executeCanvasProjectOpen(command.canvasProjectId, true); - } else if (command.id === 'canvas.project_sync') { - void executeCanvasProjectSync(command.canvasProjectId, true); - } else if (command.id === 'canvas.asset_import') { - void executeCanvasAssetImport( - null, - command.localPath, - command.kind, - command.mediaType, - command.canvasProjectId, - command.canvasAssetId, - command.canvasAssetObjectId ?? '', - true, - ); - } else if (command.id === 'canvas.asset_generate') { - void executeCanvasAssetGenerate(command.prompt, true); - } else if (command.id === 'canvas.export_import') { - void executeCanvasExportImport( - command.exportPath, - command.canvasProjectId, - true, - ); - } else if (command.id === 'game.generate_draft') { - void executeGameDraft(command.prompt); - } - } - - async function executeProjectCreate( - nextProjectPath: string, - announceToChat: boolean, - ) { - const trimmedProjectPath = nextProjectPath.trim(); - const invoke = resolveTauriInvoke(); - if ( - invoke && - trimmedProjectPath && - isAbsoluteProjectPath(trimmedProjectPath) && - !projectPathHasControlCharacter(trimmedProjectPath) - ) { - try { - const nonEmpty = await invoke( - 'is_local_project_directory_non_empty', - { projectPath: trimmedProjectPath }, - ); - if (nonEmpty) { - setPendingNonEmptyProjectCreate({ - projectPath: trimmedProjectPath, - announceToChat, - }); - setWorkspaceStatus('目标文件夹不是空的'); - setProjectStatus('等待确认'); - return; - } - } catch { - // ponytail: stale test doubles and older shells may miss this helper; init still validates. - } - } - await openWorkspace(nextProjectPath, announceToChat); - } - function cancelProjectCreateInNonEmptyFolder() { const pendingCreate = pendingNonEmptyProjectCreate; setPendingNonEmptyProjectCreate(null); setWorkspaceStatus('已取消'); - setProjectStatus('已取消'); if (pendingCreate?.announceToChat) { setMessages((current) => [ ...current, @@ -7286,3311 +1725,100 @@ export function App({ } } - function confirmProjectCreateInNonEmptyFolder() { - const pendingCreate = pendingNonEmptyProjectCreate; - if (!pendingCreate) { - return; - } - setPendingNonEmptyProjectCreate(null); - void openWorkspace(pendingCreate.projectPath, pendingCreate.announceToChat); - } - - async function executeProjectStatus( - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setProjectStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setProjectStatus('请先初始化本地项目'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请先用 /project 设置本地项目。' }, - ]); - } - return; - } - + /** + * 预览已经在跑时先切视图,不重启预览服务。 + * + * `activate_local_game_preview` 是 Rust 侧的「这条预览还活着、且属于这个项目」闸门: + * 它只核对内存 registry 里的状态并回传可用的 loopback 地址,前端据此进客户端运行视图。 + * 同一个动作过去由工作台壳的 `/preview open` 聊天命令承担,那条命令链随 Supervisor + * 前端链路一起退役,行为改由「运行」入口承接,结果经 DirectProject 聊天的 `announce` + * 交给聊天自己的消息流。返回 `null` 表示没有可复用的活体预览,调用方照旧重启预览。 + */ + async function activateRunningPreview( + invoke: TauriInvoke, + nextProjectPath: string, + ): Promise { try { - if ( - announceToChat && - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'project.status', - nextProjectPath, - `读取 ${nextProjectPath} 的项目状态`, - '准备读取项目状态。', - () => void executeProjectStatus(true, true), - )) - ) { - return; - } - const nextManifest = await invoke( - 'get_local_game_manifest', - { projectPath: nextProjectPath, commandId: 'project.status' }, - ); - setManifest(nextManifest); - setProjectStatus('已读取状态'); - setCommandLog((current) => [...current, 'project.status']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeProjectStatus(nextManifest, nextProjectPath), - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setProjectStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeProjectIndex(announceToChat: boolean) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setLimitedCommandStatus('请先初始化本地项目'); - return; - } - - try { - const result = await invoke( - 'build_local_project_index', - { projectPath: nextProjectPath }, - ); - setFileStatus(`已索引 ${result.fileCount} 个文件`); - setCommandLog((current) => [...current, 'project.index']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: summarizeProjectIndex(result) }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setFileStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function queueOrExecuteProjectIndex() { - const invoke = resolveTauriInvoke(); - if (!invoke) { - void executeProjectIndex(true); - return; - } - const nextProjectPath = requireChatProjectForUserAction(); - if (!nextProjectPath) { - return; - } - - try { - const result = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - if (result.policy.confirmCommands.includes('project.index')) { - queuePendingCommand({ id: 'project.index' }); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '准备刷新本地项目索引。' }, - ]); - return; - } - void executeProjectIndex(true); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - - async function executeProjectCheckpoint(announceToChat: boolean) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : projectPath; - if (!nextProjectPath) { - return; - } - - try { - const result = await invoke( - 'create_local_project_checkpoint', - { projectPath: nextProjectPath }, - ); - setFileStatus(`已保存 checkpoint:${result.checkpointId}`); - setCommandLog((current) => [...current, 'project.checkpoint']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: summarizeProjectCheckpoint(result) }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setFileStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeProjectExportPackage(announceToChat: boolean) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : projectPath; - if (!nextProjectPath) { - return; - } - - try { - const result = await invoke( - 'export_local_project_package', - { projectPath: nextProjectPath }, - ); - setFileStatus(`已导出本地试玩包:${result.packageRelativePath}`); - setCommandLog((current) => [...current, 'project.export_package']); - void refreshManifest(nextProjectPath); - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeProjectExportPackage(result), - draftCommand: '/open-project', - draftCommandLabel: '显示目录', - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setFileStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeProjectExportPackages( - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : resolveChatProjectPath(localProject); - if (!nextProjectPath) { - return; - } - - try { - if ( - announceToChat && - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'project.export_list', - nextProjectPath, - `列出 ${nextProjectPath} 的本地试玩包`, - '准备列出本地试玩包。', - () => void executeProjectExportPackages(true, true), - )) - ) { - return; - } - const result = await invoke( - 'list_local_project_export_packages', - { projectPath: nextProjectPath }, - ); - setFileStatus(`已列出 ${result.packages.length} 个本地试玩包`); - setCommandLog((current) => [...current, 'project.export_list']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeProjectExportPackages(result), - draftCommand: - result.packages.length > 0 ? '/open-project' : '/export', - draftCommandLabel: - result.packages.length > 0 ? '显示目录' : '导出试玩包', - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setFileStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeProjectCheckpoints( - announceToChat: boolean, - skipListPolicyConfirm = false, - skipReadPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : resolveChatProjectPath(localProject); - if (!nextProjectPath) { - return; - } - - try { - if ( - announceToChat && - !skipListPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'file.list', - nextProjectPath, - `列出 ${nextProjectPath} 的 checkpoint`, - '准备列出项目 checkpoint。', - () => void executeProjectCheckpoints(true, true, false), - )) - ) { - return; - } - const result = await invoke( - 'list_local_project_files', - { projectPath: nextProjectPath }, - ); - const manifestFiles = sortCheckpointManifestFiles(result.files); - const visibleFiles = manifestFiles.slice(0, 5); - if ( - announceToChat && - visibleFiles.length > 0 && - !skipReadPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'file.read', - nextProjectPath, - `读取 ${nextProjectPath} 的 checkpoint manifest`, - '准备读取 checkpoint manifest。', - () => void executeProjectCheckpoints(true, true, true), - )) - ) { - return; - } - const checkpoints: LocalProjectCheckpointSummary[] = []; - for (const file of visibleFiles) { - const manifest = await invoke( - 'read_local_project_file', - { - projectPath: nextProjectPath, - relativePath: file.path, - commandId: 'file.read', - }, - ); - checkpoints.push(checkpointSummaryFromManifest(file, manifest.content)); - } - setProjectFiles(result.files); - setProjectCheckpoints(checkpoints); - setFileStatus(`已列出 ${manifestFiles.length} 个 checkpoint`); - setCommandLog((current) => [ - ...current, - 'file.list', - ...(visibleFiles.length > 0 ? ['file.read checkpoint manifests'] : []), - ]); - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeProjectCheckpoints( - checkpoints, - manifestFiles.length - visibleFiles.length, - ), - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setFileStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeProjectDiff( - checkpointId: string, - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : projectPath; - if (!nextProjectPath) { - return; - } - - try { - if ( - announceToChat && - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'project.diff', - nextProjectPath, - `对比 ${nextProjectPath} 的 checkpoint:${checkpointId}`, - '准备对比项目 checkpoint。', - () => void executeProjectDiff(checkpointId, true, true), - )) - ) { - return; - } - const result = await invoke( - 'diff_local_project_checkpoint', - { projectPath: nextProjectPath, checkpointId }, - ); - setFileStatus(`已对比 checkpoint:${result.checkpointId}`); - setCommandLog((current) => [...current, 'project.diff']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: summarizeProjectDiff(result) }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setFileStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeProjectRestore( - checkpointId: string, - announceToChat: boolean, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : projectPath; - if (!nextProjectPath) { - return; - } - - try { - const result = await invoke( - 'restore_local_project_checkpoint', - { projectPath: nextProjectPath, checkpointId }, - ); - setFileStatus( - `已回滚 ${result.restoredCount} 个文件,删除 ${result.deletedCount} 个新增文件`, - ); - setCommandLog((current) => [...current, 'project.restore']); - void refreshManifest(nextProjectPath); - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `已回滚 ${result.restoredCount} 个文件到 ${nextProjectPath},删除 ${result.deletedCount} 个新增文件:${result.checkpointId}`, - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setFileStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeProjectPolicyRead(announceToChat: boolean) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setProjectStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : projectPath; - if (!nextProjectPath) { - return; - } - - try { - const result = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - setProjectStatus('已读取权限策略'); - setCommandLog((current) => [...current, 'project.policy_read']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: summarizeProjectPolicy(result) }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setProjectStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function queueProjectPolicyMutation( - commandId: string, - mode: 'deny' | 'allow' | 'confirm' | 'auto', - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - const nextProjectPath = requireChatProjectForUserAction(); - if (!nextProjectPath) { - return; - } - - try { - const result = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - const alreadyDenied = result.policy.deniedCommands.includes(commandId); - const alreadyConfirmed = - result.policy.confirmCommands.includes(commandId); - if (mode === 'deny' && alreadyDenied) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: `命令已在拒绝列表中:${commandId}` }, - ]); - return; - } - if (mode === 'allow' && !alreadyDenied) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: `命令不在拒绝列表中:${commandId}` }, - ]); - return; - } - if (mode === 'confirm' && alreadyConfirmed) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: `命令已在确认列表中:${commandId}` }, - ]); - return; - } - if (mode === 'auto' && !alreadyConfirmed) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: `命令不在确认列表中:${commandId}` }, - ]); - return; - } - const deniedCommands = - mode === 'deny' - ? [...new Set([...result.policy.deniedCommands, commandId])] - : mode === 'allow' - ? result.policy.deniedCommands.filter( - (value) => value !== commandId, - ) - : mode === 'confirm' - ? result.policy.deniedCommands.filter( - (value) => value !== commandId, - ) - : result.policy.deniedCommands; - const confirmCommands = - mode === 'confirm' - ? [...new Set([...result.policy.confirmCommands, commandId])] - : mode === 'deny' - ? result.policy.confirmCommands.filter( - (value) => value !== commandId, - ) - : mode === 'auto' - ? result.policy.confirmCommands.filter( - (value) => value !== commandId, - ) - : result.policy.confirmCommands; - queuePendingCommand({ - id: 'project.policy_write', - policy: { ...result.policy, deniedCommands, confirmCommands }, - }); - const action = - mode === 'deny' - ? '拒绝' - : mode === 'allow' - ? '允许' - : mode === 'confirm' - ? '确认' - : '自动执行'; - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `准备${action}命令:${commandId}`, - }, - ]); - } catch (error) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: error instanceof Error ? error.message : String(error), - }, - ]); - } - } - - async function queueAgentPolicyMutation( - agentId: string, - commandId: string, - mode: 'deny' | 'allow' | 'confirm' | 'auto', - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - const nextProjectPath = requireChatProjectForUserAction(); - if (!nextProjectPath) { - return; - } - - try { - const result = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - const agentPolicies = { ...(result.policy.agentPolicies ?? {}) }; - const agentPolicy = agentPolicies[agentId] ?? { - deniedCommands: [], - confirmCommands: [], - }; - const alreadyDenied = agentPolicy.deniedCommands.includes(commandId); - const alreadyConfirmed = agentPolicy.confirmCommands.includes(commandId); - if (mode === 'deny' && alreadyDenied) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `Agent ${agentId} 的命令已在拒绝列表中:${commandId}`, - }, - ]); - return; - } - if (mode === 'allow' && !alreadyDenied) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `Agent ${agentId} 的命令不在拒绝列表中:${commandId}`, - }, - ]); - return; - } - if (mode === 'confirm' && alreadyConfirmed) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `Agent ${agentId} 的命令已在确认列表中:${commandId}`, - }, - ]); - return; - } - if (mode === 'auto' && !alreadyConfirmed) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `Agent ${agentId} 的命令不在确认列表中:${commandId}`, - }, - ]); - return; - } - const deniedCommands = - mode === 'deny' - ? [...new Set([...agentPolicy.deniedCommands, commandId])] - : mode === 'allow' - ? agentPolicy.deniedCommands.filter((value) => value !== commandId) - : mode === 'confirm' - ? agentPolicy.deniedCommands.filter( - (value) => value !== commandId, - ) - : agentPolicy.deniedCommands; - const confirmCommands = - mode === 'confirm' - ? [...new Set([...agentPolicy.confirmCommands, commandId])] - : mode === 'deny' - ? agentPolicy.confirmCommands.filter((value) => value !== commandId) - : mode === 'auto' - ? agentPolicy.confirmCommands.filter( - (value) => value !== commandId, - ) - : agentPolicy.confirmCommands; - agentPolicies[agentId] = { - deniedCommands, - confirmCommands, - }; - queuePendingCommand({ - id: 'project.policy_write', - policy: { - ...result.policy, - agentPolicies, - }, - }); - const action = - mode === 'deny' - ? '拒绝' - : mode === 'allow' - ? '允许' - : mode === 'confirm' - ? '确认' - : '自动执行'; - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `准备${action} Agent ${agentId} 命令:${commandId}`, - }, - ]); - } catch (error) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: error instanceof Error ? error.message : String(error), - }, - ]); - } - } - - async function executeProjectPolicyWrite( - policy: ProjectPermissionPolicy, - announceToChat: boolean, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setProjectStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : projectPath; - if (!nextProjectPath) { - return; - } - - try { - const result = await invoke( - 'write_project_permission_policy', - { projectPath: nextProjectPath, policy }, - ); - setProjectStatus('已写入权限策略'); - setCommandLog((current) => [...current, 'project.policy_write']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: summarizeProjectPolicy(result) }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setProjectStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeProjectFiles( - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setFileStatus('请先初始化本地项目'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请先用 /project 设置本地项目。' }, - ]); - } - return; - } - - try { - if ( - announceToChat && - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'file.list', - nextProjectPath, - `列出 ${nextProjectPath} 的项目文件`, - '准备列出项目文件。', - () => void executeProjectFiles(true, true), - )) - ) { - return; - } - const result = await invoke( - 'list_local_project_files', - { projectPath: nextProjectPath }, - ); - setProjectFiles(result.files); - setFileStatus(`已列出 ${result.files.length} 项`); - setCommandLog((current) => [...current, 'file.list']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: summarizeProjectFiles(result.files) }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setFileStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeProjectAssets( - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAssetStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setAssetStatus('请先初始化本地项目'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请先用 /project 设置本地项目。' }, - ]); - } - return; - } - - try { - if ( - announceToChat && - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'asset.list', - nextProjectPath, - `列出 ${nextProjectPath} 的项目资产`, - '准备列出项目资产。', - () => void executeProjectAssets(true, true), - )) - ) { - return; - } - const nextManifest = await invoke( - 'get_local_game_manifest', - { projectPath: nextProjectPath, commandId: 'asset.list' }, - ); - setManifest(nextManifest); - setAssetStatus(`已列出 ${nextManifest.assets.length} 个资产`); - setCommandLog((current) => [...current, 'asset.list']); - if (announceToChat) { - const firstReadableAsset = firstReadableProjectAssetPath(nextManifest); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeProjectAssets(nextManifest), - draftCommand: firstReadableAsset - ? `/read ${firstReadableAsset}` - : undefined, - draftCommandLabel: firstReadableAsset ? '读取首个资产' : undefined, - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setAssetStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeProjectFileReadChat( - relativePath: string, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setFileStatus('请先初始化本地项目'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请先用 /project 设置本地项目。' }, - ]); - return; - } - - try { - const commandId = isAgentTraceFilePath(relativePath) - ? 'agent.trace_read' - : 'file.read'; - if ( - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - commandId, - nextProjectPath, - `读取 ${nextProjectPath} 的 ${relativePath}`, - '准备读取项目文件。', - () => void executeProjectFileReadChat(relativePath, true), - )) - ) { - return; - } - const result = await invoke( - 'read_local_project_file', - { projectPath: nextProjectPath, relativePath, commandId }, - ); - setFilePath(result.path); - setFileDraft(result.content); - setFileStatus(`已读取:${result.path}`); - setCommandLog((current) => [...current, 'file.read']); - setMessages((current) => [ - ...current, - { role: 'assistant', text: summarizeProjectFileContent(result) }, - ]); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setFileStatus(message); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - - async function executeProjectTasks( - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setProjectStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setProjectStatus('请先初始化本地项目'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请先用 /project 设置本地项目。' }, - ]); - } - return; - } - - try { - if ( - announceToChat && - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'task.list', - nextProjectPath, - `读取 ${nextProjectPath} 的任务拆分`, - '准备读取任务拆分。', - () => void executeProjectTasks(true, true), - )) - ) { - return; - } - const nextManifest = await invoke( - 'get_local_game_manifest', - { projectPath: nextProjectPath, commandId: 'task.list' }, - ); - setManifest(nextManifest); - setProjectStatus('已读取任务拆分'); - setCommandLog((current) => [...current, 'task.list']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: summarizeProjectTasks(nextManifest) }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setProjectStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeAgentTraceChat(skipPolicyConfirm = false) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAgentRunStatus('需要在 Tauri App 内运行'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setAgentRunStatus('请先初始化本地项目'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请先用 /project 设置本地项目。' }, - ]); - return; - } - - try { - if ( - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'agent.trace_read', - nextProjectPath, - `读取 ${nextProjectPath} 的最近 Agent run trace`, - '准备读取 Agent run trace。', - () => void executeAgentTraceChat(true), - )) - ) { - return; - } - const result = await invoke( - 'read_local_project_file', - { - projectPath: nextProjectPath, - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }, - ); - const trace = parseAgentRunTrace(result.content); - setAgentRunTrace(trace); - setAgentRunStatus(formatAgentRunStatus(trace)); - setCommandLog((current) => [ - ...current, - 'agent.trace_read', - 'file.read .agent/run.latest.json', - ]); - const traceSummary = summarizeAgentRunCompletionForChat(trace); - setMessages((current) => [ - ...current, - { role: 'assistant', ...traceSummary }, - ]); - } catch (error) { - const rawMessage = error instanceof Error ? error.message : String(error); - const message = isMissingAgentRunTraceError(rawMessage) - ? '暂无最近 Agent trace。先生成一次游戏草案后再查看。' - : rawMessage; - setAgentRunStatus(message); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - - async function executeAgentRunControl( - action: string, - detail: string | undefined, - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAgentRunStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = requireChatProjectForUserAction(); - if (!nextProjectPath) { - return; - } - const commandId = - action === 'status' - ? 'agent.run_status' - : (`agent.${action}` as GameCreationAppCommandDescriptor['id']); - - try { - if ( - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - commandId, - nextProjectPath, - action === 'status' - ? `查看 ${nextProjectPath} 的最近 Agent run 状态` - : `执行 ${nextProjectPath} 的 ${commandId}`, - action === 'status' - ? '准备查看 Agent run 状态。' - : '准备执行 Agent run 操作。', - () => - void executeAgentRunControl(action, detail, announceToChat, true), - )) - ) { - return; - } - const result = await invoke('control_agent_run', { - projectPath: nextProjectPath, - action, - detail, - }); - setAgentRunStatus( - `${result.status} · ${result.lifecycleStatus} · ${result.nextStep}`, - ); - setCommandLog((current) => [ - ...current, - commandId, - 'file.write .agent/activity.jsonl', - 'file.write .agent/output.jsonl', - 'file.write .agent/context.bundle.json', - ]); - if (announceToChat && action === 'status') { - appendLocalPermissionLog( - nextProjectPath, - 'command.auto', - 'agent.run_status', - ); - } - let traceReadQueued = false; - if (action === 'status') { - const policyView = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - if (policyView.policy.confirmCommands.includes('agent.trace_read')) { - requestProjectPolicyConfirmation( - 'agent.trace_read', - nextProjectPath, - `读取 ${nextProjectPath} 的最近 Agent run trace`, - () => void refreshAgentRunTrace(nextProjectPath), - ); - traceReadQueued = true; - } - } - if (!traceReadQueued) { - await refreshAgentRunTrace(nextProjectPath); - } - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: [ - result.message, - `run:${result.runId}`, - `状态:${result.status} / ${result.lifecycleStatus}`, - `下一步:${result.nextStep}`, - `事件:${result.activityPath}`, - `输出:${result.outputPath}`, - `上下文包:${result.contextBundlePath}`, - ].join('\n'), - draftCommand: '/read .agent/output.jsonl', - draftCommandLabel: '读取 Run 输出', - }, - ]); - } - } catch (error) { - const rawMessage = error instanceof Error ? error.message : String(error); - const message = formatAgentRunControlError(action, rawMessage); - setAgentRunStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - function queueAgentRunControlFromPanel(action: 'kill' | 'retry' | 'resume') { - if (!requireChatProjectForUserAction()) { - return; - } - queuePendingCommand({ id: `agent.${action}` }); - setAgentRunStatus('等待确认'); - } - - async function executeAgentAuditChat( - confirmedPolicyCommands: GameCreationAppCommandDescriptor['id'][] = [], - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAgentRunStatus('需要在 Tauri App 内运行'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setAgentRunStatus('请先初始化本地项目'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请先用 /project 设置本地项目。' }, - ]); - return; - } - - try { - if ( - !confirmedPolicyCommands.includes('agent.audit') && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'agent.audit', - nextProjectPath, - `审计 ${nextProjectPath} 的 Agent 能力证据`, - '准备审计 Agent 能力证据。', - () => void executeAgentAuditChat(['agent.audit']), - )) - ) { - return; - } - const policyView = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - const auditReadCommands: Array = [ - 'project.status', - 'file.list', - 'file.read', - 'agent.trace_read', - ]; - const confirmCommandId = auditReadCommands.find( - (commandId) => - policyView.policy.confirmCommands.includes(commandId) && - !confirmedPolicyCommands.includes(commandId), - ); - if (confirmCommandId) { - requestProjectPolicyConfirmation( - confirmCommandId, - nextProjectPath, - `审计 ${nextProjectPath} 需要读取 ${confirmCommandId}`, - () => - void executeAgentAuditChat([ - ...confirmedPolicyCommands, - confirmCommandId, - ]), - ); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `准备确认审计读取:${confirmCommandId}` }, - ]); - return; - } - const nextManifest = await invoke( - 'get_local_game_manifest', - { projectPath: nextProjectPath, commandId: 'agent.audit' }, - ); - const fileResult = await invoke( - 'list_local_project_files', - { projectPath: nextProjectPath }, - ); - let commandLogContent = ''; - try { - const commandLogResult = await invoke( - 'read_local_project_file', - { - projectPath: nextProjectPath, - relativePath: '.agent/logs/command.log', - commandId: 'file.read', - }, - ); - commandLogContent = commandLogResult.content; - } catch { - commandLogContent = ''; - } - let trace: GameCreationAgentRunTrace | null = null; - try { - const traceResult = await invoke( - 'read_local_project_file', - { - projectPath: nextProjectPath, - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }, - ); - trace = parseAgentRunTrace(traceResult.content); - } catch { - trace = null; - } - - setManifest(nextManifest); - setProjectFiles(fileResult.files); - setAgentRunTrace(trace); - setAgentRunStatus( - trace ? formatAgentRunStatus(trace) : '还没有最近一次 Agent run', - ); - setCommandLog((current) => [ - ...current, - 'agent.audit', - 'project.status', - 'file.list', - ...(commandLogContent ? ['file.read .agent/logs/command.log'] : []), - ...(trace ? ['agent.trace_read'] : []), - ]); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: summarizeAgentAudit( - nextManifest, - nextProjectPath, - fileResult.files, - trace, - commandLogContent, - ), - }, - ]); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setAgentRunStatus(message); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - - async function handlePreviewStart() { - const nextProjectPath = resolveChatProjectPath(localProject) ?? projectPath; - requestCommandConfirmation( - 'preview.start', - `启动 ${nextProjectPath}/game/ 并载入客户端运行视图`, - () => void executePreviewStart(false), - ); - } - - async function executePreviewStart( - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setPreviewStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setPreviewStatus('请先初始化本地项目'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请先用 /project 设置本地项目。' }, - ]); - } - return; - } - - setPreviewStatus('正在启动'); - try { - if ( - announceToChat && - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'preview.start', - nextProjectPath, - `启动 ${nextProjectPath}/game/ 并载入客户端运行视图`, - '准备启动本地预览。', - () => void executePreviewStart(true, true), - )) - ) { - return; - } - const result = await invoke( - 'start_local_game_preview', - { projectPath: nextProjectPath }, - ); - updateClientPreview(result); - setPreviewStatus(`运行中:127.0.0.1:${result.port}`); - void refreshManifest(nextProjectPath); - setCommandLog((current) => [...current, 'preview.start']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `预览已在客户端运行视图启动:${result.url}`, - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setPreviewStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executePreviewOpen( - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setPreviewStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : resolveChatProjectPath(localProject); - if (announceToChat && !nextProjectPath) { - return; - } - - try { - if ( - announceToChat && - nextProjectPath && - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'preview.open', - nextProjectPath, - `打开 ${nextProjectPath} 的本地预览`, - '准备打开当前本地预览。', - () => void executePreviewOpen(true, true), - )) - ) { - return; - } - const result = await invoke( + const status = await invoke( 'activate_local_game_preview', - nextProjectPath ? { projectPath: nextProjectPath } : undefined, + { projectPath: nextProjectPath }, ); if ( - result.status === 'running' && - result.url && - result.port && - result.root + status.status !== 'running' || + !status.url || + !status.port || + !status.root ) { - updateClientPreview({ - url: result.url, - port: result.port, - root: result.root, - }); - setPreviewStatus(`运行中:127.0.0.1:${result.port}`); + return null; } - setCommandLog((current) => [...current, 'preview.open']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: result.url - ? `已切换到客户端运行视图:${result.url}` - : '已切换到客户端运行视图。', - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setPreviewStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function handlePreviewStatus() { - void executePreviewStatus(false); - } - - async function executePreviewStatus( - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setPreviewStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : resolveChatProjectPath(localProject); - if (announceToChat && !nextProjectPath) { - return; - } - - try { - if ( - announceToChat && - nextProjectPath && - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'preview.status', - nextProjectPath, - `查看 ${nextProjectPath} 的预览状态`, - '准备查看预览状态。', - () => void executePreviewStatus(true, true), - )) - ) { - return; - } - const result = await invoke( - 'get_local_game_preview_status', - nextProjectPath ? { projectPath: nextProjectPath } : undefined, - ); - if ( - result.status === 'running' && - result.url && - result.port && - result.root - ) { - updateClientPreview({ - url: result.url, - port: result.port, - root: result.root, - }); - setPreviewStatus(`运行中:127.0.0.1:${result.port}`); - } else { - updateClientPreview(null); - setPreviewStatus('未启动'); - } - setCommandLog((current) => [...current, 'preview.status']); - if (announceToChat && nextProjectPath) { - appendLocalPermissionLog( - nextProjectPath, - 'command.auto', - 'preview.status', - ); - } - if (announceToChat) { - const previewRunning = result.status === 'running' && result.url; - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: previewRunning ? `预览运行中:${result.url}` : '预览未启动。', - draftCommand: previewRunning ? '/open-preview' : '/preview', - draftCommandLabel: previewRunning - ? '填入打开预览命令' - : '填入启动预览命令', - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setPreviewStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function handlePreviewStop() { - void executePreviewStop(false); - } - - async function executePreviewStop( - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setPreviewStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setPreviewStatus('请先初始化本地项目'); - return; - } - - try { - if ( - announceToChat && - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'preview.stop', - nextProjectPath, - `停止 ${nextProjectPath} 的本地预览`, - '准备停止本地预览。', - () => void executePreviewStop(true, true), - )) - ) { - return; - } - await invoke('stop_local_game_preview', { - projectPath: nextProjectPath, - }); - updateClientPreview(null); - setPreviewStatus('已停止'); - setCommandLog((current) => [...current, 'preview.stop']); - if (announceToChat) { - appendLocalPermissionLog( - nextProjectPath, - 'command.auto', - 'preview.stop', - ); - } - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '预览已停止。' }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setPreviewStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function handleMemoryRead(skipPolicyConfirm = false) { - const nextProjectPath = validateMemoryPanelProjectPath(); - if (!nextProjectPath) { - return; - } - const invoke = resolveTauriInvoke(); - if (!invoke) { - setMemoryStatus('需要在 Tauri App 内运行'); - return; - } - - setMemoryStatus('正在读取'); - try { - if (!skipPolicyConfirm) { - const policyView = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - if (policyView.policy.confirmCommands.includes('memory.read')) { - requestProjectPolicyConfirmation( - 'memory.read', - nextProjectPath, - `读取 ${nextProjectPath}/${memoryScopePath(memoryScope)}`, - () => void handleMemoryRead(true), - ); - setMemoryStatus('等待确认'); - return; - } - } - const result = await invoke( - 'read_local_game_memory', - { projectPath: nextProjectPath, scope: memoryScope }, - ); - setMemoryDraft(result.content); - setMemoryStatus( - result.exists ? `已读取:${result.path}` : '记忆文件不存在', - ); - setCommandLog((current) => [...current, 'memory.read']); - } catch (error) { - setMemoryStatus(error instanceof Error ? error.message : String(error)); - } - } - - function validateMemoryPanelProjectPath() { - const nextProjectPath = projectPath.trim(); - if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) { - setMemoryStatus('请提供本地项目绝对路径。'); + return { url: status.url, port: status.port, root: status.root }; + } catch { + // 没在跑、不属于这个项目,或权限位要求确认:都不是错误,回落到重新启动预览。 return null; } - if (projectPathHasControlCharacter(nextProjectPath)) { - setMemoryStatus('本地项目路径不能包含控制字符。'); - return null; - } - return nextProjectPath; - } - - async function handleMemoryWrite() { - const nextProjectPath = validateMemoryPanelProjectPath(); - if (!nextProjectPath) { - return; - } - requestCommandConfirmation( - 'memory.write', - `保存 ${nextProjectPath}/${memoryScopePath(memoryScope)}`, - () => void executeMemoryWritePanel(nextProjectPath), - ); - } - - async function executeMemoryWritePanel(nextProjectPath: string) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setMemoryStatus('需要在 Tauri App 内运行'); - return; - } - - setMemoryStatus('正在保存'); - try { - const result = await invoke( - 'write_local_game_memory', - { - projectPath: nextProjectPath, - scope: memoryScope, - content: memoryDraft, - }, - ); - setMemoryStatus(`已保存:${result.path}`); - setCommandLog((current) => [...current, 'memory.write']); - } catch (error) { - setMemoryStatus(error instanceof Error ? error.message : String(error)); - } - } - - async function handleMemoryDelete() { - const nextProjectPath = validateMemoryPanelProjectPath(); - if (!nextProjectPath) { - return; - } - requestCommandConfirmation( - 'memory.delete', - `删除 ${nextProjectPath}/${memoryScopePath(memoryScope)}`, - () => void executeMemoryDeletePanel(nextProjectPath), - ); - } - - async function executeMemoryDeletePanel(nextProjectPath: string) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setMemoryStatus('需要在 Tauri App 内运行'); - return; - } - - setMemoryStatus('正在删除'); - try { - const result = await invoke( - 'delete_local_game_memory', - { projectPath: nextProjectPath, scope: memoryScope }, - ); - setMemoryDraft(''); - setMemoryStatus(`已删除:${result.path}`); - setCommandLog((current) => [...current, 'memory.delete']); - } catch (error) { - setMemoryStatus(error instanceof Error ? error.message : String(error)); - } - } - - async function executeMemoryReadChat( - scope: MemoryScope, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setMemoryStatus('需要在 Tauri App 内运行'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - const nextProjectPath = requireChatProjectForUserAction(); - if (!nextProjectPath) { - return; - } - - try { - if ( - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'memory.read', - nextProjectPath, - `读取 ${nextProjectPath}/${memoryScopePath(scope)}`, - '准备读取项目记忆。', - () => void executeMemoryReadChat(scope, true), - )) - ) { - return; - } - const result = await invoke( - 'read_local_game_memory', - { projectPath: nextProjectPath, scope }, - ); - setMemoryScope(scope); - setMemoryDraft(result.content); - setMemoryStatus( - result.exists ? `已读取:${result.path}` : '记忆文件不存在', - ); - setCommandLog((current) => [...current, 'memory.read']); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: result.exists - ? `${memoryScopeLabel(scope)}记忆:\n${result.content}` - : `${memoryScopeLabel(scope)}记忆为空。`, - }, - ]); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setMemoryStatus(message); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - - async function executeMemoryWrite( - scope: MemoryScope, - content: string, - mode: MemoryWriteMode, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setMemoryStatus('需要在 Tauri App 内运行'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - const nextProjectPath = requireChatProjectForUserAction(); - if (!nextProjectPath) { - return; - } - - try { - const currentMemory = - mode === 'append' - ? await invoke('read_local_game_memory', { - projectPath: nextProjectPath, - scope, - }) - : null; - const result = await invoke( - 'write_local_game_memory', - { - projectPath: nextProjectPath, - scope, - content: - mode === 'append' - ? appendMemoryContent(currentMemory?.content ?? '', content) - : content, - }, - ); - setMemoryScope(scope); - setMemoryDraft(result.content); - setMemoryStatus(`已保存:${result.path}`); - setCommandLog((current) => [ - ...current, - ...(mode === 'append' ? ['memory.read'] : []), - 'memory.write', - ]); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: - mode === 'append' - ? `已追加${memoryScopeLabel(scope)}记忆。` - : `已保存${memoryScopeLabel(scope)}记忆。`, - }, - ]); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setMemoryStatus(message); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - - async function executeMemoryDeleteChat(scope: MemoryScope) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setMemoryStatus('需要在 Tauri App 内运行'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - const nextProjectPath = requireChatProjectForUserAction(); - if (!nextProjectPath) { - return; - } - - try { - const result = await invoke( - 'delete_local_game_memory', - { projectPath: nextProjectPath, scope }, - ); - setMemoryScope(scope); - setMemoryDraft(''); - setMemoryStatus(`已删除:${result.path}`); - setCommandLog((current) => [...current, 'memory.delete']); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `已删除${memoryScopeLabel(scope)}记忆。` }, - ]); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setMemoryStatus(message); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - - async function handleLimitedCommandRun( - command: GameCreationAppLimitedRunCommandDescriptor, - ) { - const nextProjectPath = resolveChatProjectPath(localProject) ?? projectPath; - requestCommandConfirmation( - 'command.run_limited', - `运行 ${command.title} 于 ${nextProjectPath}`, - () => void executeLimitedCommand(command.id, false), - ); - } - - async function refreshLimitedLocalCommands() { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setLimitedCommandStatus('需要在 Tauri App 内运行'); - return; - } - setLimitedCommandStatus('正在读取内置命令'); - try { - const commands = await invoke< - GameCreationAppLimitedRunCommandDescriptor[] - >('get_limited_local_commands'); - setLimitedLocalCommands( - commands.length > 0 - ? commands - : [...GAME_CREATION_APP_LIMITED_RUN_COMMANDS], - ); - setLimitedCommandStatus(`已读取 ${commands.length} 个内置命令`); - setCommandLog((current) => [...current, 'command.list_limited']); - } catch (error) { - setLimitedCommandStatus( - error instanceof Error ? error.message : String(error), - ); - } - } - - async function handleProjectLogRead( - relativePath: string, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setProjectLogStatus('需要在 Tauri App 内运行'); - return; - } - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setProjectLogStatus('请先初始化本地项目'); - return; - } - setProjectLogStatus(`正在读取 ${relativePath}`); - try { - if ( - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'file.read', - nextProjectPath, - `读取 ${nextProjectPath}/${relativePath}`, - '准备读取项目日志。', - () => void handleProjectLogRead(relativePath, true), - )) - ) { - setProjectLogStatus('等待确认'); - return; - } - const result = await invoke( - 'read_local_project_file', - { - projectPath: nextProjectPath, - relativePath, - commandId: 'file.read', - }, - ); - setProjectLogContent(result.content); - setProjectLogStatus(`已读取:${result.path}`); - setCommandLog((current) => [...current, `file.read ${result.path}`]); - } catch (error) { - setProjectLogContent(''); - setProjectLogStatus( - error instanceof Error ? error.message : String(error), - ); - } - } - - async function executeLimitedCommandList() { - const invoke = resolveTauriInvoke(); - let commands: GameCreationAppLimitedRunCommandDescriptor[] = [ - ...GAME_CREATION_APP_LIMITED_RUN_COMMANDS, - ]; - if (invoke) { - try { - const nativeCommands = await invoke< - GameCreationAppLimitedRunCommandDescriptor[] - >('get_limited_local_commands'); - if (nativeCommands.length > 0) { - commands = nativeCommands; - } - } catch { - commands = [...GAME_CREATION_APP_LIMITED_RUN_COMMANDS]; - } - } - setLimitedLocalCommands(commands); - setLimitedCommandStatus(`已读取 ${commands.length} 个内置命令`); - setCommandLog((current) => [...current, 'command.list_limited']); - setMessages((current) => [ - ...current, - { role: 'assistant', text: summarizeLimitedLocalCommands(commands) }, - ]); } async function executeRunLocal(announceToChat: boolean) { const invoke = resolveTauriInvoke(); if (!invoke) { - setLimitedCommandStatus('需要在 Tauri App 内运行'); if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); + announceProjectChatMessage('需要在 Tauri App 内运行。'); } return; } const nextProjectPath = resolveChatProjectPath(localProject); if (!nextProjectPath) { - setLimitedCommandStatus('请先初始化本地项目'); if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '请先用 /project 设置本地项目。' }, - ]); + announceProjectChatMessage('请先用 /project 设置本地项目。'); } return; } - setLimitedCommandStatus('正在运行'); try { - const smoke = directCodexProductRuntime - ? null - : await invoke('run_limited_local_command', { - projectPath: nextProjectPath, - commandId: 'game.static_smoke', - }); + const activePreview = await activateRunningPreview( + invoke, + nextProjectPath, + ); + if (activePreview) { + updateClientPreview(activePreview); + if (announceToChat) { + announceProjectChatMessage( + `已切换到客户端运行视图:${activePreview.url}`, + ); + } + return; + } const previewResult = await invoke( 'start_local_game_preview', { projectPath: nextProjectPath }, ); updateClientPreview(previewResult); - setPreviewStatus(`运行中:127.0.0.1:${previewResult.port}`); - setLimitedCommandStatus(smoke?.output ?? '本地预览已启动'); - setCommandLog((current) => [ - ...current, - 'game.run_local', - ...(smoke ? [`command.run_limited ${smoke.commandId}`] : []), - 'preview.start', - ]); void refreshManifest(nextProjectPath); - if (!directCodexProductRuntime) { + if (!directProjectMode) { void refreshAgentRunTrace(nextProjectPath); } if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `运行通过,已载入客户端运行视图:${previewResult.url}`, - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setLimitedCommandStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function executeLimitedCommand( - commandId: string, - announceToChat: boolean, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setLimitedCommandStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setLimitedCommandStatus('请先初始化本地项目'); - return; - } - - setLimitedCommandStatus('正在运行'); - try { - const result = await invoke( - 'run_limited_local_command', - { projectPath: nextProjectPath, commandId }, - ); - const summary = `${result.output}\n日志:${result.logPath}`; - setLimitedCommandStatus(result.output); - setCommandLog((current) => [ - ...current, - `command.run_limited ${result.commandId}`, - ]); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: summary }, - ]); - } - void refreshManifest(nextProjectPath); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setLimitedCommandStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function handleFileList(skipPolicyConfirm = false) { - const nextProjectPath = validateFilePanelProjectPath(); - if (!nextProjectPath) { - return; - } - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - return; - } - - setFileStatus('正在列出'); - try { - if (!skipPolicyConfirm) { - const policyView = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, + announceProjectChatMessage( + `运行通过,已载入客户端运行视图:${previewResult.url}`, ); - if (policyView.policy.confirmCommands.includes('file.list')) { - requestProjectPolicyConfirmation( - 'file.list', - nextProjectPath, - `列出 ${nextProjectPath} 的项目文件`, - () => void handleFileList(true), - ); - setFileStatus('等待确认'); - return; - } - } - const result = await invoke( - 'list_local_project_files', - { projectPath: nextProjectPath }, - ); - setProjectFiles(result.files); - setFileStatus(`已列出 ${result.files.length} 项`); - setCommandLog((current) => [...current, 'file.list']); - } catch (error) { - setFileStatus(error instanceof Error ? error.message : String(error)); - } - } - - function validateFilePanelProjectPath() { - const nextProjectPath = projectPath.trim(); - if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) { - setFileStatus('请提供本地项目绝对路径。'); - return null; - } - if (projectPathHasControlCharacter(nextProjectPath)) { - setFileStatus('本地项目路径不能包含控制字符。'); - return null; - } - return nextProjectPath; - } - - function validateFilePanelPath() { - if (!isSafeProjectRelativePath(filePath)) { - setFileStatus('文件路径必须是项目内相对路径。'); - return null; - } - return filePath.trim(); - } - - async function handleFileRead(skipPolicyConfirm = false) { - const nextProjectPath = validateFilePanelProjectPath(); - if (!nextProjectPath) { - return; - } - const relativePath = validateFilePanelPath(); - if (!relativePath) { - return; - } - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - return; - } - - setFileStatus('正在读取'); - try { - const commandId = isAgentTraceFilePath(relativePath) - ? 'agent.trace_read' - : 'file.read'; - if (!skipPolicyConfirm) { - const policyView = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - if (policyView.policy.confirmCommands.includes(commandId)) { - requestProjectPolicyConfirmation( - commandId, - nextProjectPath, - `读取 ${nextProjectPath} 的 ${relativePath}`, - () => void handleFileRead(true), - ); - setFileStatus('等待确认'); - return; - } - } - const result = await invoke( - 'read_local_project_file', - { - projectPath: nextProjectPath, - relativePath, - commandId, - }, - ); - setFilePath(result.path); - setFileDraft(result.content); - setFileStatus(`已读取:${result.path}`); - setCommandLog((current) => [...current, 'file.read']); - } catch (error) { - setFileStatus(error instanceof Error ? error.message : String(error)); - } - } - - async function handleFileWrite() { - const nextProjectPath = validateFilePanelProjectPath(); - if (!nextProjectPath) { - return; - } - const relativePath = validateFilePanelPath(); - if (!relativePath) { - return; - } - requestCommandConfirmation( - 'file.write', - `保存 ${nextProjectPath}/${relativePath}`, - () => void executeFileWrite(nextProjectPath, relativePath), - ); - } - - async function executeFileWrite( - nextProjectPath: string, - relativePath: string, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - return; - } - - setFileStatus('正在保存'); - try { - const result = await invoke( - 'write_local_project_file', - { projectPath: nextProjectPath, relativePath, content: fileDraft }, - ); - setFileStatus(`已保存:${result.path}`); - setCommandLog((current) => [...current, 'file.write']); - } catch (error) { - setFileStatus(error instanceof Error ? error.message : String(error)); - } - } - - async function handleFileDelete() { - const nextProjectPath = validateFilePanelProjectPath(); - if (!nextProjectPath) { - return; - } - const relativePath = validateFilePanelPath(); - if (!relativePath) { - return; - } - requestCommandConfirmation( - 'file.delete', - `删除 ${nextProjectPath}/${relativePath}`, - () => void executeFileDelete(nextProjectPath, relativePath), - ); - } - - async function executeFileDelete( - nextProjectPath: string, - relativePath: string, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setFileStatus('需要在 Tauri App 内运行'); - return; - } - - setFileStatus('正在删除'); - try { - const result = await invoke( - 'delete_local_project_file', - { projectPath: nextProjectPath, relativePath }, - ); - setFileDraft(''); - setFileStatus(result.deleted ? `已删除:${result.path}` : '文件不存在'); - setCommandLog((current) => [...current, 'file.delete']); - } catch (error) { - setFileStatus(error instanceof Error ? error.message : String(error)); - } - } - - async function handleAssetRegister() { - const nextProjectPath = projectPath.trim(); - const localPath = assetLocalPath.trim(); - const canvasProjectId = assetCanvasProjectId.trim(); - const resourceId = assetResourceId.trim(); - const nextAssetObjectId = assetObjectId.trim(); - if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) { - setAssetStatus('请提供本地项目绝对路径。'); - return; - } - if (projectPathHasControlCharacter(nextProjectPath)) { - setAssetStatus('本地项目路径不能包含控制字符。'); - return; - } - if (!isSafeProjectRelativePath(localPath)) { - setAssetStatus('资产路径必须是项目内相对路径。'); - return; - } - if (assetSourceKind === 'canvas') { - if (!canvasProjectId) { - setAssetStatus('请提供画板项目 ID。'); - return; - } - if (!isSafeCanvasProjectId(canvasProjectId)) { - setAssetStatus('画板项目 ID 不能包含控制字符。'); - return; - } - if (!resourceId && !nextAssetObjectId) { - setAssetStatus('请提供资源 ID 或资产对象 ID。'); - return; - } - } - requestCommandConfirmation( - 'asset.register', - `登记 ${nextProjectPath}/${localPath}`, - () => - void executeAssetRegister( - nextProjectPath, - localPath, - parseGameCreationAppAssetKind(assetKind, 'ui.asset-register.kind'), - assetMediaType, - assetSourceKind, - canvasProjectId, - resourceId, - nextAssetObjectId, - ), - ); - } - - async function executeAssetRegister( - nextProjectPath: string, - localPath: string, - kind: GameCreationAppAssetKind, - mediaType: string, - sourceKind: string, - canvasProjectId: string, - resourceId: string, - nextAssetObjectId: string, - announceToChat = false, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAssetStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - - setAssetStatus('正在登记'); - try { - if ( - announceToChat && - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'asset.register', - nextProjectPath, - `登记 ${nextProjectPath}/${localPath}`, - '准备登记项目资产。', - () => - void executeAssetRegister( - nextProjectPath, - localPath, - kind, - mediaType, - sourceKind, - canvasProjectId, - resourceId, - nextAssetObjectId, - true, - true, - ), - )) - ) { - return; - } - const result = await invoke( - 'register_local_asset', - { - projectPath: nextProjectPath, - localPath, - kind, - mediaType, - sourceKind, - canvasProjectId, - resourceId, - assetObjectId: nextAssetObjectId, - taskId: '', - prompt: '', - model: '', - }, - ); - setUploadedAssets((current) => { - const filtered = current.filter((asset) => asset.id !== result.id); - return [...filtered, result]; - }); - setAssetStatus(`已登记:${result.localPath}`); - setCommandLog((current) => [...current, 'asset.register']); - void refreshManifest(nextProjectPath); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: `已登记资产:${result.localPath}` }, - ]); } } catch (error) { const message = error instanceof Error ? error.message : String(error); - setAssetStatus(message); if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); + announceProjectChatMessage(message); } } } - async function handleCanvasAssetImport() { - const nextProjectPath = projectPath.trim(); - const localPath = assetLocalPath.trim(); - const canvasProjectId = assetCanvasProjectId.trim(); - const resourceId = assetResourceId.trim(); - const nextAssetObjectId = assetObjectId.trim(); - if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) { - setAssetStatus('请提供本地项目绝对路径。'); - return; - } - if (projectPathHasControlCharacter(nextProjectPath)) { - setAssetStatus('本地项目路径不能包含控制字符。'); - return; - } - if (!isSafeProjectRelativePath(localPath)) { - setAssetStatus('画板资产路径必须是项目内相对路径。'); - return; - } - if (!canvasProjectId) { - setAssetStatus('请提供画板项目 ID。'); - return; - } - if (!isSafeCanvasProjectId(canvasProjectId)) { - setAssetStatus('画板项目 ID 不能包含控制字符。'); - return; - } - if (!resourceId && !nextAssetObjectId) { - setAssetStatus('请提供资源 ID 或资产对象 ID。'); - return; - } - requestCommandConfirmation( - 'canvas.asset_import', - `导入 ${nextProjectPath}/${localPath}`, - () => - void executeCanvasAssetImport( - nextProjectPath, - localPath, - parseGameCreationAppAssetKind( - assetKind, - 'ui.canvas-asset-import.kind', - ), - assetMediaType, - canvasProjectId, - resourceId, - nextAssetObjectId, - false, - ), - ); - } - - async function executeCanvasAssetImport( - nextProjectPath: string | null, - localPath: string, - kind: GameCreationAppAssetKind, - mediaType: string, - canvasProjectId: string, - resourceId: string, - assetObjectId: string, - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAssetStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const targetProjectPath = announceToChat - ? requireChatProjectForUserAction() - : nextProjectPath; - if (!targetProjectPath) { - return; - } - - try { - if ( - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'canvas.asset_import', - targetProjectPath, - `导入 ${targetProjectPath}/${localPath} 的画板来源资产`, - '准备导入画板资产。', - () => - void executeCanvasAssetImport( - nextProjectPath, - localPath, - kind, - mediaType, - canvasProjectId, - resourceId, - assetObjectId, - announceToChat, - true, - ), - )) - ) { - setAssetStatus('等待确认导入画板资产'); - return; - } - setAssetStatus('正在导入画板资产'); - const result = await invoke( - 'import_canvas_asset', - { - projectPath: targetProjectPath, - localPath, - kind, - mediaType, - canvasProjectId, - resourceId, - assetObjectId, - taskId: '', - prompt: '', - model: '', - }, - ); - setUploadedAssets((current) => { - const filtered = current.filter((asset) => asset.id !== result.id); - return [...filtered, result]; - }); - const canvasSource = formatCanvasAssetSource({ - canvasProjectId, - canvasAssetId: resourceId, - canvasAssetObjectId: assetObjectId, - }); - setAssetStatus(`已导入画板资产 ${canvasSource}:${result.localPath}`); - setCommandLog((current) => [...current, 'canvas.asset_import']); - void refreshManifest(targetProjectPath); - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `已导入画板资产 ${canvasSource}:${result.localPath}`, - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setAssetStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - function handleCanvasAssetGenerate() { - const nextProjectPath = projectPath.trim(); - const prompt = assetGenerationPrompt.trim(); - if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) { - setAssetStatus('请提供本地项目绝对路径。'); - return; - } - if (projectPathHasControlCharacter(nextProjectPath)) { - setAssetStatus('本地项目路径不能包含控制字符。'); - return; - } - if (!prompt) { - setAssetStatus('请提供美术生成提示词。'); - return; - } - requestCommandConfirmation( - 'canvas.asset_generate', - `生成 ${nextProjectPath} 的首版美术素材`, - () => void executeCanvasAssetGenerate(prompt, false), - ); - } - - async function executeCanvasAssetGenerate( - prompt: string, - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAssetStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : projectPath.trim(); - if (!nextProjectPath) { - return; - } - - try { - if ( - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'canvas.asset_generate', - nextProjectPath, - `生成 ${nextProjectPath} 的首版美术素材`, - '准备生成首版美术素材。', - () => void executeCanvasAssetGenerate(prompt, announceToChat, true), - )) - ) { - setAssetStatus('等待确认生成美术素材'); - return; - } - setAssetStatus('正在生成美术素材'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '正在生成首版美术素材。' }, - ]); - } - const result = await invoke( - 'generate_platform_art_asset', - { - projectPath: nextProjectPath, - prompt, - }, - ); - setUploadedAssets((current) => { - const filtered = current.filter((asset) => asset.id !== result.id); - return [...filtered, result]; - }); - setAssetStatus(`已生成美术素材:${result.localPath}`); - setCommandLog((current) => [...current, 'canvas.asset_generate']); - void refreshManifest(nextProjectPath); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: `已生成美术素材:${result.localPath}` }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setAssetStatus(message); - if (isRuntimeConfigMissingError(message)) { - requestRuntimeConfigOpen(); - } - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - function handleCanvasProjectSync() { - const nextProjectPath = projectPath.trim(); - const canvasProjectId = assetCanvasProjectId.trim(); - if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) { - setAssetStatus('请提供本地项目绝对路径。'); - return; - } - if (projectPathHasControlCharacter(nextProjectPath)) { - setAssetStatus('本地项目路径不能包含控制字符。'); - return; - } - if (!canvasProjectId) { - setAssetStatus('请提供画板项目 ID。'); - return; - } - if (!isSafeCanvasProjectId(canvasProjectId)) { - setAssetStatus('画板项目 ID 不能包含控制字符。'); - return; - } - requestCommandConfirmation( - 'canvas.project_sync', - `同步画板项目资源:${canvasProjectId}`, - () => void executeCanvasProjectSync(canvasProjectId, false), - ); - } - - async function executeCanvasProjectSync( - canvasProjectId: string, - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAssetStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : projectPath; - if (!nextProjectPath) { - return; - } - - try { - if ( - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'canvas.project_sync', - nextProjectPath, - `同步 ${nextProjectPath} 的画板项目资源:${canvasProjectId}`, - '准备同步画板项目资源。', - () => - void executeCanvasProjectSync( - canvasProjectId, - announceToChat, - true, - ), - )) - ) { - setAssetStatus('等待确认同步画板项目'); - return; - } - setAssetStatus('正在同步画板项目资源'); - const result = await invoke( - 'sync_canvas_project_assets', - { - projectPath: nextProjectPath, - canvasProjectId, - }, - ); - setUploadedAssets((current) => { - const byId = new Map(current.map((asset) => [asset.id, asset])); - for (const asset of result.assets) { - byId.set(asset.id, asset); - } - return [...byId.values()]; - }); - setAssetStatus( - `已同步画板项目:${result.importedCount} 个资产,${result.importRoot}`, - ); - setCommandLog((current) => [...current, 'canvas.project_sync']); - void refreshManifest(nextProjectPath); - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `已同步 ${result.importedCount} 个画板资产自 ${result.canvasProjectId}:${result.importRoot}`, - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setAssetStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - function handleCanvasExportImport() { - const nextProjectPath = projectPath.trim(); - const exportPath = canvasExportPath.trim(); - const canvasProjectId = assetCanvasProjectId.trim(); - if (!nextProjectPath || !isAbsoluteProjectPath(nextProjectPath)) { - setAssetStatus('请提供本地项目绝对路径。'); - return; - } - if (projectPathHasControlCharacter(nextProjectPath)) { - setAssetStatus('本地项目路径不能包含控制字符。'); - return; - } - if (!canvasProjectId) { - setAssetStatus('请提供画板项目 ID。'); - return; - } - if (!isSafeCanvasProjectId(canvasProjectId)) { - setAssetStatus('画板项目 ID 不能包含控制字符。'); - return; - } - if ( - !isAbsoluteProjectPath(exportPath) || - projectPathHasControlCharacter(exportPath) - ) { - setAssetStatus('画板导出 ZIP 路径必须是绝对路径。'); - return; - } - requestCommandConfirmation( - 'canvas.export_import', - `导入画板导出包:${exportPath}`, - () => void executeCanvasExportImport(exportPath, canvasProjectId, false), - ); - } - - async function executeCanvasExportImport( - exportPath: string, - canvasProjectId: string, - announceToChat: boolean, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAssetStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - const nextProjectPath = announceToChat - ? requireChatProjectForUserAction() - : projectPath; - if (!nextProjectPath) { - return; - } - - try { - if ( - !skipPolicyConfirm && - (await queueProjectPolicyConfirmationIfNeeded( - invoke, - 'canvas.export_import', - nextProjectPath, - `导入 ${nextProjectPath} 的画板导出包:${exportPath}`, - '准备导入画板导出包。', - () => - void executeCanvasExportImport( - exportPath, - canvasProjectId, - announceToChat, - true, - ), - )) - ) { - setAssetStatus('等待确认导入画板导出包'); - return; - } - setAssetStatus('正在导入画板导出包'); - const result = await invoke( - 'import_canvas_export', - { - projectPath: nextProjectPath, - exportPath, - canvasProjectId, - }, - ); - setUploadedAssets((current) => { - const byId = new Map(current.map((asset) => [asset.id, asset])); - for (const asset of result.assets) { - byId.set(asset.id, asset); - } - return [...byId.values()]; - }); - setAssetStatus( - `已导入画板导出包:${result.importedCount} 个资产,${result.importRoot}`, - ); - setCommandLog((current) => [...current, 'canvas.export_import']); - void refreshManifest(nextProjectPath); - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `已导入 ${result.importedCount} 个画板资产自 ${canvasProjectId}:${result.importRoot}`, - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setAssetStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function handleCanvasProjectOpen() { - const canvasProjectId = assetCanvasProjectId.trim(); - if (!canvasProjectId) { - setAssetStatus('请提供画板项目 ID。'); - return; - } - if (!isSafeCanvasProjectId(canvasProjectId)) { - setAssetStatus('画板项目 ID 不能包含控制字符。'); - return; - } - requestCommandConfirmation( - 'canvas.project_open', - `打开本机画板项目 ${canvasProjectId}`, - () => void executeCanvasProjectOpen(canvasProjectId, false), - ); - } - - async function executeCanvasProjectOpen( - canvasProjectId: string, - announceToChat: boolean, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAssetStatus('需要在 Tauri App 内运行'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - } - return; - } - - setAssetStatus('正在打开画板'); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: `正在打开画板:${canvasProjectId}` }, - ]); - } - try { - const result = await invoke( - 'open_canvas_project', - { - canvasProjectId, - editorBaseUrl, - }, - ); - setAssetStatus(`已打开画板:${result.url}`); - setCommandLog((current) => [...current, 'canvas.project_open']); - if (announceToChat) { - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: `已打开画板:${result.url}`, - draftCommand: `/sync-canvas-project ${canvasProjectId}`, - draftCommandLabel: '同步此画板', - }, - ]); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setAssetStatus(message); - if (announceToChat) { - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - } - - async function handleAssetUpload(event: ChangeEvent) { - const file = event.currentTarget.files?.[0]; - event.currentTarget.value = ''; - if (!file) { - return; - } - if (!requireChatProjectForUserAction()) { - return; - } - - queuePendingCommand({ id: 'asset.upload', file }); - setMessages((current) => [ - ...current, - { role: 'user', text: `上传文件:${file.name}` }, - { role: 'assistant', text: `准备保存文件到本地项目:${file.name}` }, - ]); - } - - async function executeAssetUpload(file: File) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAssetStatus('需要在 Tauri App 内运行'); - setMessages((current) => [ - ...current, - { role: 'assistant', text: '需要在 Tauri App 内运行。' }, - ]); - return; - } - const nextProjectPath = requireChatProjectForUserAction(); - if (!nextProjectPath) { - return; - } - - setAssetStatus('正在上传'); - try { - const bytes = Array.from(new Uint8Array(await file.arrayBuffer())); - const result = await invoke( - 'upload_local_asset', - { - projectPath: nextProjectPath, - fileName: file.name, - mediaType: file.type || 'application/octet-stream', - bytes, - }, - ); - setLocalProject({ - projectPath: nextProjectPath, - manifestPath: result.manifestPath, - manifest, - }); - setUploadedAssets((current) => [...current, result]); - setAssetStatus(`已上传:${result.localPath}`); - setCommandLog((current) => [ - ...current, - 'asset.upload', - 'asset.register', - ]); - void refreshManifest(nextProjectPath); - setMessages((current) => [ - ...current, - { role: 'assistant', text: `已保存文件:${result.localPath}` }, - ]); - } catch (error) { - setAssetStatus(error instanceof Error ? error.message : String(error)); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: error instanceof Error ? error.message : String(error), - }, - ]); - } - } - async function loadAgentRunTraceFile( relativePath: string, nextProjectPath = resolveChatProjectPath(localProject) ?? '', ) { const invoke = resolveTauriInvoke(); if (!invoke) { - setAgentRunStatus('需要在 Tauri App 内运行'); return null; } if (!nextProjectPath) { - setAgentRunStatus('请先初始化本地项目'); return null; } @@ -10606,142 +1834,9 @@ export function App({ }, ); const trace = parseAgentRunTrace(result.content); - setAgentRunTrace(trace); - setAgentRunStatus(formatAgentRunStatus(trace)); - setCommandLog((current) => [...current, `file.read ${relativePath}`]); return summarizeAgentRunCompletionForChat(trace); - } catch (error) { - setAgentRunTrace(null); - setAgentRunStatus( - relativePath === '.agent/run.latest.json' && - isMissingProjectFileError(error) - ? '还没有最近一次 Agent run' - : error instanceof Error - ? error.message - : String(error), - ); - return null; - } - } - - async function handleAgentRunTraceFileOpen( - relativePath: string, - skipPolicyConfirm = false, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAgentRunStatus('需要在 Tauri App 内运行'); - return; - } - const nextProjectPath = resolveChatProjectPath(localProject) ?? ''; - if (!nextProjectPath) { - setAgentRunStatus('请先初始化本地项目'); - return; - } - - if (!skipPolicyConfirm) { - const policyView = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - if (policyView.policy.confirmCommands.includes('agent.trace_read')) { - requestProjectPolicyConfirmation( - 'agent.trace_read', - nextProjectPath, - `读取 ${nextProjectPath} 的 ${relativePath}`, - () => void handleAgentRunTraceFileOpen(relativePath, true), - ); - setAgentRunStatus('等待确认读取 Agent trace'); - return; - } - } - - await loadAgentRunTraceFile(relativePath, nextProjectPath); - } - - async function readAgentRunHistoryItems( - invoke: TauriInvoke, - runFiles: LocalProjectFileEntry[], - nextProjectPath: string, - ) { - const history = await Promise.all( - runFiles.map(async (file) => { - try { - const traceResult = await invoke( - 'read_local_project_file', - { - projectPath: nextProjectPath, - relativePath: file.path, - commandId: 'agent.trace_read', - }, - ); - return { - path: file.path, - size: file.size, - trace: parseAgentRunTrace(traceResult.content), - } satisfies AgentRunHistoryItem; - } catch { - return null; - } - }), - ); - return history - .filter((item): item is AgentRunHistoryItem => item !== null) - .sort((left, right) => right.trace.updatedAt - left.trace.updatedAt); - } - - async function refreshAgentRunHistory( - nextProjectPath = resolveChatProjectPath(localProject) ?? '', - ) { - const invoke = resolveTauriInvoke(); - if (!invoke || !nextProjectPath) { - setAgentRunHistory([]); - setAgentRunHistoryFiles([]); - setAgentRunHistoryOverflowCount(0); - setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT); - setAgentRunHistoryLoadingMore(false); - agentRunHistoryLoadingMoreRef.current = false; - return; - } - - try { - const result = await invoke( - 'list_local_project_files', - { projectPath: nextProjectPath }, - ); - const runFiles = result.files - .filter( - (file) => - file.kind === 'file' && - file.path.startsWith('.agent/runs/') && - file.path.endsWith('.json'), - ) - .sort( - (left, right) => - (right.modifiedAt ?? 0) - (left.modifiedAt ?? 0) || - right.path.localeCompare(left.path), - ); - const runFilesToLoad = runFiles.slice(0, AGENT_RUN_HISTORY_MAX_COUNT); - const firstPage = await readAgentRunHistoryItems( - invoke, - runFilesToLoad.slice(0, AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT), - nextProjectPath, - ); - setAgentRunHistory(firstPage); - setAgentRunHistoryFiles(runFilesToLoad); - setAgentRunHistoryOverflowCount( - Math.max(0, runFiles.length - runFilesToLoad.length), - ); - setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT); - setAgentRunHistoryLoadingMore(false); - agentRunHistoryLoadingMoreRef.current = false; } catch { - setAgentRunHistory([]); - setAgentRunHistoryFiles([]); - setAgentRunHistoryOverflowCount(0); - setAgentRunHistoryVisibleCount(AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT); - setAgentRunHistoryLoadingMore(false); - agentRunHistoryLoadingMoreRef.current = false; + return null; } } @@ -10767,7 +1862,7 @@ export function App({ agentRuntimeResumeProjectPathRef.current = null; return; } - if (directCodexProductRuntime) { + if (directProjectMode) { agentRuntimeResumeProjectPathRef.current = nextProjectPath; setAgentRuntimeById({}); return; @@ -10819,30 +1914,20 @@ export function App({ agentRuntimeStateFromResult(runtimeResult), ); } - setAgentRunStatus('已确认恢复 Agent Runtime 任务'); }) - .catch((resumeError) => { + .catch(() => { if (localProjectPathRef.current !== nextProjectPath) { return; } agentRuntimeResumeProjectPathRef.current = null; - setAgentRunStatus( - `Agent Runtime 恢复失败:${ - resumeError instanceof Error - ? resumeError.message - : String(resumeError) - }`, - ); }); }, ); - setAgentRunStatus('等待确认恢复 Agent Runtime 任务'); } else if (message.includes('项目权限策略拒绝执行:agent.resume')) { agentRuntimeResumeProjectPathRef.current = nextProjectPath; markProjectPolicyDenied('agent.resume', message); } else { agentRuntimeResumeProjectPathRef.current = null; - setAgentRunStatus(`Agent Runtime 恢复失败:${message}`); } } } @@ -10857,32 +1942,6 @@ export function App({ for (const runtimeResult of runtimes) { nextRuntimes.push(agentRuntimeStateFromResult(runtimeResult)); } - const supervisorRuntimeIndex = nextRuntimes.findIndex( - (runtime) => runtime.agentId === PROJECT_SUPERVISOR_AGENT_ID, - ); - const persistedSupervisorRuntime = - supervisorRuntimeIndex >= 0 - ? nextRuntimes[supervisorRuntimeIndex]! - : null; - if (projectSupervisorOnly && persistedSupervisorRuntime) { - const currentRuntime = projectSupervisorRuntimeRef.current; - if ( - !currentRuntime || - persistedSupervisorRuntime.updatedAt >= currentRuntime.updatedAt - ) { - if (persistedSupervisorRuntime.sessionId) { - projectSupervisorSessionIdRef.current = - persistedSupervisorRuntime.sessionId; - setProjectSupervisorSessionId(persistedSupervisorRuntime.sessionId); - } - updateProjectSupervisorRuntime(persistedSupervisorRuntime); - updateProjectSupervisorResponseStream( - runtimes[supervisorRuntimeIndex]?.responseStream, - persistedSupervisorRuntime, - ); - setProjectSupervisorRuntimeError(''); - } - } setAgentRuntimeById((current) => nextRuntimes.reduce( (next, runtime) => mergeAgentRuntimeStateIntoMap(next, runtime, true), @@ -10900,9 +1959,7 @@ export function App({ nextProjectPath = resolveChatProjectPath(localProject) ?? '', ) { if (!nextProjectPath) { - setAgentRunStatus('请先初始化本地项目'); await refreshAgentRuntimes(nextProjectPath); - await refreshAgentRunHistory(nextProjectPath); return null; } const summary = await loadAgentRunTraceFile( @@ -10910,108 +1967,9 @@ export function App({ nextProjectPath, ); await refreshAgentRuntimes(nextProjectPath); - await refreshAgentRunHistory(nextProjectPath); return summary; } - async function handleAgentRunTraceRefresh(skipPolicyConfirm = false) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAgentRunStatus('需要在 Tauri App 内运行'); - return; - } - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setAgentRunStatus('请先初始化本地项目'); - return; - } - try { - if (!skipPolicyConfirm) { - const policyView = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - if (policyView.policy.confirmCommands.includes('agent.trace_read')) { - requestProjectPolicyConfirmation( - 'agent.trace_read', - nextProjectPath, - `读取 ${nextProjectPath} 的最近 Agent run trace`, - () => void handleAgentRunTraceRefresh(true), - ); - setAgentRunStatus('等待确认'); - return; - } - } - await refreshAgentRunTrace(nextProjectPath); - } catch (error) { - setAgentRunStatus(error instanceof Error ? error.message : String(error)); - } - } - - async function scheduleReadyAgentTasks(skipPolicyConfirm = false) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - setAgentRunStatus('需要在 Tauri App 内运行'); - return; - } - const nextProjectPath = resolveChatProjectPath(localProject); - if (!nextProjectPath) { - setAgentRunStatus('请先初始化本地项目'); - return; - } - try { - if (!skipPolicyConfirm) { - const policyView = await invoke( - 'read_project_permission_policy', - { projectPath: nextProjectPath }, - ); - if ( - policyView.policy.confirmCommands.includes('agent.schedule_ready') - ) { - requestProjectPolicyConfirmation( - 'agent.schedule_ready', - nextProjectPath, - `调度 ${nextProjectPath} 中已 Ready 的 Agent 任务`, - () => void scheduleReadyAgentTasks(true), - ); - setAgentRunStatus('等待确认调度 Ready 任务'); - return; - } - } - setAgentRunStatus('正在调度 Ready 任务'); - const scheduledRuntimes = await invoke( - 'schedule_game_creator_agent_ready_tasks', - { projectPath: nextProjectPath, limit: 16 }, - ); - for (const runtimeResult of scheduledRuntimes) { - rememberAgentRuntimeState(agentRuntimeStateFromResult(runtimeResult)); - } - await refreshManifest(nextProjectPath); - await refreshAgentRuntimes(nextProjectPath); - const message = - scheduledRuntimes.length > 0 - ? `已调度 ${scheduledRuntimes.length} 个 Ready 任务。` - : '没有可调度的 Ready 任务。'; - setAgentRunStatus(message); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setAgentRunStatus(message); - setMessages((current) => [ - ...current, - { role: 'assistant', text: message }, - ]); - } - } - - const agentStatusCards = deriveAgentStatusCards( - manifest, - agentRunTrace ?? agentRunHistory[0]?.trace ?? null, - agentRuntimeById, - ); const professionalResultCandidates = taskRowsFromManifest(manifest).map( (task) => ({ agentId: agentConversationId(task), @@ -11025,7 +1983,7 @@ export function App({ .join('|'); useEffect(() => { const nextProjectPath = localProject?.projectPath; - if (!projectSupervisorOnly || !nextProjectPath || !onManifestChange) { + if (!nextProjectPath || !onManifestChange) { return; } const invoke = resolveTauriInvoke(); @@ -11054,7 +2012,7 @@ export function App({ onManifestChange(nextProjectPath, currentManifest, { projectId: currentManifest.projectId, revision: after.revision, - source: 'supervisor', + source: 'chat', }); } return; @@ -11063,18 +2021,12 @@ export function App({ return () => { cancelled = true; }; - }, [ - localProject?.projectPath, - manifest, - onManifestChange, - projectSupervisorOnly, - ]); + }, [localProject?.projectPath, manifest, onManifestChange]); useEffect(() => { const invoke = resolveTauriInvoke(); const nextProjectPath = localProject?.projectPath ?? null; if ( - !projectSupervisorOnly || - directCodexProductRuntime || + directProjectMode || !invoke || !nextProjectPath || professionalResultCandidates.length === 0 @@ -11156,55 +2108,27 @@ export function App({ // The semantic candidate key replaces the freshly allocated candidates array. // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - directCodexProductRuntime, + directProjectMode, localProject?.projectPath, professionalResultCandidateKey, - projectSupervisorOnly, - projectSupervisorRuntime?.runId, ]); useEffect(() => { - if (!projectSupervisorOnly || !onAgentRuntimeSummariesChange) { + if (!onAgentRuntimeSummariesChange) { return; } + // 工作台壳不再持有任何根 Runtime:普通项目的运行态由 `DirectProjectChatView` + // 自己的订阅拥有,策划链路只投影设计 Agent 会话。这里保留回调契约,根 Runtime + // 传 null,运行态叠加交给各入口自己的事实源。 onAgentRuntimeSummariesChange( - projectAgentRuntimeSummaries( - manifest, - projectSupervisorRuntime, - agentRuntimeById, - ), + projectAgentRuntimeSummaries(manifest, null, agentRuntimeById), ); - }, [ - agentRuntimeById, - manifest, - onAgentRuntimeSummariesChange, - projectSupervisorOnly, - projectSupervisorRuntime, - ]); + }, [agentRuntimeById, manifest, onAgentRuntimeSummariesChange]); useEffect(() => { - if (!projectSupervisorOnly || !onAgentResultsChange) { + if (!onAgentResultsChange) { return; } onAgentResultsChange(Object.values(professionalAgentResultsById)); - }, [ - onAgentResultsChange, - professionalAgentResultsById, - projectSupervisorOnly, - ]); - const mainProjectSummary = localProject - ? summarizeMainProjectHeader(manifest, agentStatusCards) - : null; - const visibleAgentRunHistory = agentRunHistory.slice( - 0, - agentRunHistoryVisibleCount, - ); - const visibleMainProjectFiles = projectFiles - .filter((file) => file.kind === 'file' && !file.path.startsWith('.agent/')) - .slice(0, 8); - const visibleMainProjectAssets = manifest.assets - .filter( - (asset) => asset.localPath && !asset.localPath.startsWith('.agent/'), - ) - .slice(0, 8); + }, [onAgentResultsChange, professionalAgentResultsById]); const chatProjectAssets = manifest.assets.filter( (asset) => asset.localPath && !asset.localPath.startsWith('.agent/'), ); @@ -11213,70 +2137,6 @@ export function App({ // 传 `null` 表示回退到 manifest 中最新的版本。 const chatProjectVersions = manifest.versions ?? []; const chatActiveVersionId = activeVersionId; - const visibleMainProjectCheckpoints = projectCheckpoints.slice(0, 5); - const currentProjectTitle = localProject - ? manifest.name.trim() || projectNameFromPath(localProject.projectPath) - : seedManifest.name; - useEffect(() => { - if (!supervisorChatOnly) { - return; - } - setWindowTitle(localProject ? currentProjectTitle : undefined); - return () => setWindowTitle(undefined); - }, [currentProjectTitle, localProject, setWindowTitle, supervisorChatOnly]); - const agentRunHistoryOmittedCount = - Math.max(0, agentRunHistoryFiles.length - agentRunHistoryVisibleCount) + - agentRunHistoryOverflowCount; - const canShowMoreAgentRunHistory = - agentRunHistoryVisibleCount < agentRunHistoryFiles.length; - - async function showMoreAgentRunHistory() { - const invoke = resolveTauriInvoke(); - const nextProjectPath = resolveChatProjectPath(localProject); - if (!invoke || agentRunHistoryLoadingMoreRef.current) { - return; - } - if (!nextProjectPath) { - setAgentRunStatus('请先初始化本地项目'); - return; - } - const nextVisibleCount = Math.min( - agentRunHistoryVisibleCount + AGENT_RUN_HISTORY_VISIBLE_STEP, - agentRunHistoryFiles.length, - ); - const nextFiles = agentRunHistoryFiles.slice( - agentRunHistoryVisibleCount, - nextVisibleCount, - ); - agentRunHistoryLoadingMoreRef.current = true; - setAgentRunHistoryLoadingMore(true); - try { - const nextItems = await readAgentRunHistoryItems( - invoke, - nextFiles, - nextProjectPath, - ); - setAgentRunHistory((current) => - [...current, ...nextItems].sort( - (left, right) => right.trace.updatedAt - left.trace.updatedAt, - ), - ); - setAgentRunHistoryVisibleCount(nextVisibleCount); - } finally { - agentRunHistoryLoadingMoreRef.current = false; - setAgentRunHistoryLoadingMore(false); - } - } - - function handleAgentRunHistoryScroll(event: UIEvent) { - if (!canShowMoreAgentRunHistory) { - return; - } - const target = event.currentTarget; - if (target.scrollHeight - target.scrollTop - target.clientHeight <= 24) { - void showMoreAgentRunHistory(); - } - } const visibleMessages = latestVisibleItems( messages, @@ -11286,110 +2146,13 @@ export function App({ 0, messages.length - visibleMessages.length, ); - const hasEarlierConversationMessages = - hiddenConversationCount > 0 || - (directCodexProductRuntime && directHistoryHasMore); - const projectSupervisorTransientReply = - projectSupervisorResponseStream?.accumulatedText.trim() ?? ''; - const projectSupervisorNeedsUserInput = agentRuntimeNeedsUserInput( - projectSupervisorRuntime, - ); - const projectSupervisorHasConversationControls = Boolean( - projectSupervisorRuntime?.pendingToolAction || - projectSupervisorRuntime?.userInputRequest || - projectSupervisorNeedsUserInput, - ); - const visibleAgentConversationMessages = latestVisibleItems( - agentConversationMessages, - agentConversationVisibleCount, - ); - const hiddenAgentConversationCount = Math.max( - 0, - agentConversationMessages.length - visibleAgentConversationMessages.length, - ); - const selectedAgentLlmStatus = selectedAgent - ? formatAgentDialogLlmStatus(llmConfigStatus, selectedAgent) - : null; - const selectedAgentLlmWarning = selectedAgent - ? formatAgentLlmConfigWarning(llmConfigStatus, selectedAgent) - : null; - const selectedAgentNeedsUserInput = agentRuntimeNeedsUserInput( - agentConversationRuntime, - ); - const selectedAgentSteerRuntime = selectedAgent - ? matchingAgentRuntimeForSteer( - [agentConversationRuntime], - selectedAgent.id, - agentConversationSessionId, - ) - : null; - + const hasEarlierConversationMessages = hiddenConversationCount > 0; async function showEarlierConversationMessages() { - if (directCodexProductRuntime && directHistoryHasMore) { - const invoke = resolveTauriInvoke(); - const projectPath = localProject?.projectPath; - if (invoke && projectPath && !directHistoryLoadingRef.current) { - directHistoryLoadingRef.current = true; - try { - // 一次点击连拉,直到出现新回合或取不动为止:一屏全是工具卡片 / 思考文本时, - // 单发一页会让用户点了"显示更早"却看不到任何变化。 - const pages = await readDirectHistoryPages({ - existingEntries: selectDirectChatEntries(directThreadChat), - beforeItemId: directHistoryOldestItemIdRef.current, - readSlice: (beforeItemId) => - invoke( - 'read_direct_project_history_slice', - beforeItemId - ? { - projectPath, - beforeItemId, - limit: CONVERSATION_VISIBLE_STEP, - } - : { projectPath, limit: CONVERSATION_VISIBLE_STEP }, - ), - }); - if (localProjectPathRef.current !== projectPath) { - return; - } - // 连拉到的页一次性进聊天 reducer:与运行态同形、同身份,重复读取只补不重。 - setDirectThreadChat((state) => - mergeDirectHistoryItems(state, pages.items), - ); - // 首页或中途读取失败时保留旧值:失败页没有可靠的 hasMore,不能因为 - // 一次瞬时 IO 抖动把「显示更早」按钮永久收掉。 - if (!pages.error) { - setDirectHistoryHasMore(pages.hasMore); - } - directHistoryOldestItemIdRef.current = - pages.firstItemId ?? directHistoryOldestItemIdRef.current; - if (pages.error) { - // 已经取到的页照常进视图,失败只走下面同一个报错出口。 - throw pages.error; - } - } catch (error) { - setWorkspaceStatus( - `读取更早的对话历史失败:${error instanceof Error ? error.message : String(error)}`, - ); - } finally { - directHistoryLoadingRef.current = false; - } - } - return; - } setConversationVisibleCount((current) => Math.min(messages.length, current + CONVERSATION_VISIBLE_STEP), ); } - function showEarlierAgentConversationMessages() { - setAgentConversationVisibleCount((current) => - Math.min( - agentConversationMessages.length, - current + CONVERSATION_VISIBLE_STEP, - ), - ); - } - function handleConversationScroll(event: UIEvent) { if (!hasEarlierConversationMessages) { return; @@ -11399,505 +2162,119 @@ export function App({ } } - function handleSupervisorChatScroll(event: UIEvent) { + function handlePlanningChatScroll(event: UIEvent) { handleConversationScroll(event); const messageList = event.currentTarget; const distanceFromBottom = messageList.scrollHeight - messageList.scrollTop - messageList.clientHeight; - supervisorChatShouldFollowLatestRef.current = + planningChatShouldFollowLatestRef.current = distanceFromBottom <= AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD; } - function handleAgentConversationScroll(event: UIEvent) { - if (hiddenAgentConversationCount === 0) { - return; - } - if (event.currentTarget.scrollTop <= 24) { - showEarlierAgentConversationMessages(); - } - } - - useEffect(() => { - if (!selectedAgent) { - return; - } - const refreshedAgent = agentStatusCards.find( - (agent) => agent.id === selectedAgent.id, - ); - if (!refreshedAgent) { - closeAgentConversation(); - return; - } - if (!sameAgentStatusCard(selectedAgent, refreshedAgent)) { - setSelectedAgent(refreshedAgent); - } - }, [agentStatusCards, selectedAgent]); - /** - * 发起一轮 direct-codex 对话回合:提交与队列出队共用同一条路径,避免两条入口的 - * 消息落盘/回合 id/附件参数走样。 + * 策划输入盒导入本地文件:文件直接写进策划工作区(`references/`),既不进游戏资源 + * 清单,也不生成回合附件;导入期间通知工作台壳暂缓切换游戏运行态。 */ - function startDirectCodexConversationTurn(input: { - prompt: string; - attachments?: DirectCodexTurnAttachment[]; - references?: ChatReference[]; - content?: DirectCodexUserContentPart[]; - }) { - const clientTurnId = createDirectCodexConversationTurnId(); - supervisorChatShouldFollowLatestRef.current = true; - setMessages((current) => [ - ...current, - { - role: 'user', - text: input.prompt, - runtimeOwned: true, - messageId: directCodexConversationMessageId(clientTurnId, 'user'), - updatedAt: Date.now(), - }, - ]); - void executeChatAgentReply({ - prompt: input.prompt, - clientTurnId, - attachments: input.attachments?.length ? input.attachments : undefined, - references: input.references, - userItem: chatComposerDraftToDirectCodexUserItem( - { - text: input.prompt, - references: input.references ?? [], - content: input.content ?? [], - }, - directCodexConversationMessageId(clientTurnId, 'user'), - ), - }); - } - - /** - * 策划文件直接导入工作区;游戏文件沿用资产上传与回合附件链路。 - */ - async function handleChatComposerUploadFiles(files: readonly File[]) { + async function handleDesignComposerUploadFiles(files: readonly File[]) { const invoke = resolveTauriInvoke(); const nextProjectPath = resolveChatProjectPath(localProject); if (!invoke || !nextProjectPath) { - setChatAttachmentNotice('需要先打开本地项目,才能上传文件'); + setChatFileImportNotice('需要先打开本地项目,才能导入文件'); return; } - if (designAgentActiveRef.current) { - if (chatFilesImporting || files.length === 0) return; - setChatFilesImporting(true); - onDesignFilesImportingChange?.(nextProjectPath, true); - setChatAttachmentNotice('正在导入文件'); - try { - const notice = await importDesignFiles(invoke, nextProjectPath, files); - if (localProjectPathRef.current === nextProjectPath) { - setChatAttachmentNotice(notice); - } - } finally { - setChatFilesImporting(false); - onDesignFilesImportingChange?.(nextProjectPath, false); - } + if (chatFilesImporting || files.length === 0) { return; } - const remaining = MAX_CHAT_COMPOSER_ATTACHMENTS - chatAttachments.length; - const accepted = files.slice(0, Math.max(remaining, 0)); - if (accepted.length === 0) { - setChatAttachmentNotice( - `最多同时携带 ${MAX_CHAT_COMPOSER_ATTACHMENTS} 个附件,请先移除已有附件`, - ); - return; - } - setChatAttachmentNotice('正在上传文件'); + setChatFilesImporting(true); + onDesignFilesImportingChange?.(nextProjectPath, true); + setChatFileImportNotice('正在导入文件'); try { - const imported = await uploadLocalFilesAsAttachments( - invoke, - nextProjectPath, - accepted, - ); - const attachments = toDirectCodexTurnAttachments(imported); - if (localProjectPathRef.current !== nextProjectPath) { - return; - } - setChatAttachments((current) => - [...current, ...attachments].slice(0, MAX_CHAT_COMPOSER_ATTACHMENTS), - ); - const failed = attachments.filter( - (attachment) => attachment.status === 'failed', - ); - setChatAttachmentNotice( - failed.length > 0 - ? `${failed.length} 个文件未能上传:${failed[0]?.name ?? ''}` - : `已上传 ${attachments.length} 个文件,将在下次发送时作为本轮附件`, - ); - void refreshManifest(nextProjectPath); - } catch (error) { + const notice = await importDesignFiles(invoke, nextProjectPath, files); if (localProjectPathRef.current === nextProjectPath) { - setChatAttachmentNotice( - error instanceof Error ? error.message : String(error), - ); + setChatFileImportNotice(notice); } - } - } - - function removeChatComposerAttachment(index: number) { - setChatAttachments((current) => - current.filter((_, currentIndex) => currentIndex !== index), - ); - } - - /** 回合运行中再次发送:进本地 FIFO 队列;队列满时拒绝并保留草稿,不静默丢消息。 */ - function enqueueChatTurnForRunningTurn(input: { - prompt: string; - attachments: DirectCodexTurnAttachment[]; - references: ChatReference[]; - content: DirectCodexUserContentPart[]; - }): boolean { - if (isChatTurnQueueFull(chatTurnQueueRef.current)) { - setChatComposerNotice(chatQueueFullNotice()); - return false; - } - queuedChatTurnSequenceRef.current += 1; - const turn = createQueuedChatTurn({ - id: `queued-chat-turn-${Date.now()}-${queuedChatTurnSequenceRef.current}`, - prompt: input.prompt, - attachments: input.attachments, - references: input.references, - content: input.content, - createdAt: Date.now(), - }); - const nextQueue = enqueueChatTurn(chatTurnQueueRef.current, turn); - chatTurnQueueRef.current = nextQueue; - setChatTurnQueue(nextQueue); - setChatComposerNotice('已加入发送队列,当前回合结束后自动发送'); - return true; - } - - function cancelQueuedChatTurn(id: string) { - const nextQueue = removeQueuedChatTurn(chatTurnQueueRef.current, id); - chatTurnQueueRef.current = nextQueue; - setChatTurnQueue(nextQueue); - if (nextQueue.length === 0) { - setChatComposerNotice(''); - } - } - - /** 队首出队并立即发出:只在 DirectProject 收到 `turn.completed` 后调用。 */ - function dispatchNextQueuedChatTurn() { - if (chatAgentBusyRef.current || directTurnRunningRef.current) { - return; - } - directTurnCompletionPendingRef.current = false; - const { next, rest } = dequeueChatTurn(chatTurnQueueRef.current); - if (!next) { - return; - } - chatTurnQueueRef.current = rest; - setChatTurnQueue(rest); - if (rest.length === 0) { - setChatComposerNotice(''); - } - startDirectCodexConversationTurn({ - prompt: next.prompt, - attachments: next.attachments, - references: next.references, - content: next.content, - }); - } - - /** - * 回合事件可能先于本地 invoke 的 finally 到达,因此先记 pending,等本地忙碌态复位后再出队。 - */ - useEffect(() => { - if (!directCodexProductRuntime) { - directTurnCompletionPendingRef.current = false; - previousDirectTurnRunningRef.current = directTurnRunning; - return; - } - const wasRunning = previousDirectTurnRunningRef.current; - previousDirectTurnRunningRef.current = directTurnRunning; - if (wasRunning && !directTurnRunning) { - directTurnCompletionPendingRef.current = true; - } - if ( - directTurnCompletionPendingRef.current && - !chatAgentBusy && - !directTurnRunning - ) { - directTurnCompletionPendingRef.current = false; - dispatchNextQueuedChatTurn(); - } - // 队列出队函数读取本轮 render 的项目输入;这里只由三个生命周期信号触发。 - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chatAgentBusy, directCodexProductRuntime, directTurnRunning]); - - /** 终止当前 direct-codex 回合:只取消这一轮,UI 由回合的 finally 复位。 */ - async function handleCancelDirectCodexTurn() { - if (directCodexTurnCancelling) { - return; - } - const invoke = resolveTauriInvoke(); - const directProjectPath = resolveChatProjectPath(localProject); - // 繁忙判据与「终止」按钮的可见条件一致:本地 invoke 正在跑,或订阅说还有一条回合没结束。 - // 只认 `turn.started` 推导出来的 `directTurnRunning` 会漏掉"刚提交、事件还没到"的窗口。 - if (!invoke || !directProjectPath || !supervisorChatBusy) { - setProjectSupervisorRuntimeError('当前没有正在运行的回合,无法终止。'); - return; - } - setDirectCodexTurnCancelling(true); - setChatComposerNotice('正在终止当前回合'); - try { - const result = await invoke( - 'cancel_direct_codex_turn', - { projectPath: directProjectPath }, - ); - const message = result?.message?.trim(); - if (result?.outcome === 'released') { - // 这一轮已经没有人替它收尾(执行进程已退出 / 从没进执行器),Rust 侧强制释放了 - // 守卫并补了终态事件;这里按同一个收口函数同步把界面复位,不等 IPC 通知。 - // 时刻取宿主观测到的这一刻:终止返回就是这一轮的终态,原生随后补的事件若先到, - // 收口已经是冻结值,不会被抬高,也不会复活成"永远运行中"。 - // 但取消回包可能晚于新回合的开始:先核对身份(clientTurnId 对应的本轮用户条目), - // 只收口确实是这一轮的那一次,避免在新回合的回调里把旧轮的时间盖上来。 - const cancelledUserItemId = result.clientTurnId - ? directCodexConversationMessageId(result.clientTurnId, 'user') - : ''; - setDirectThreadChat((state) => - directThreadTurnMatchesUser(state, cancelledUserItemId) - ? finishDirectThreadTurn(state, Date.now()) - : state, - ); - setChatAgentBusy(false); - setProjectSupervisorRuntimeError(''); - setChatComposerNotice( - message ?? '已结束这一轮占用,可以直接重新发送消息', - ); - return; - } - if (message) { - setChatComposerNotice(message); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setProjectSupervisorRuntimeError(`终止失败:${message}`); - setChatComposerNotice(''); } finally { - setDirectCodexTurnCancelling(false); + setChatFilesImporting(false); + onDesignFilesImportingChange?.(nextProjectPath, false); } } - function handleProjectSupervisorOnlySubmit( - event: FormEvent, - ) { + function handlePlanningChatSubmit(event: FormEvent) { event.preventDefault(); - const prompt = chatInput.trim(); - const references = chatReferences; - const pendingAttachments = chatAttachments; - if ( - !directCodexProductRuntime && - supervisorChatOnly && - prompt.startsWith('/') - ) { - void handleChatSubmit(event); + const content = chatComposerRef.current?.getDraft().content ?? []; + const legacyContent = directCodexContentToLegacyContentDto( + content, + manifest.assets, + ); + const canonicalPrompt = legacyContent.text; + if (!hasMeaningfulDirectCodexContent(content)) { return; } - if ( - !directCodexProductRuntime && - agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current) - ) { - setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题'); + if (chatAgentBusy) { return; } - if ( - !prompt && - references.length === 0 && - chatContent.length === 0 && - pendingAttachments.length === 0 - ) { + if (!canonicalPrompt) { return; } - if (supervisorChatBusy) { - // 回合运行中再次发送:direct-codex 面板把消息放进本地 FIFO 队列,当前回合结束后 - // 依次发出;其它面板保持原有"运行中不接受新输入"的行为。 - if (directCodexProductRuntime) { - const enqueued = enqueueChatTurnForRunningTurn({ - prompt, - attachments: pendingAttachments, - references, - content: chatContent, - }); - if (enqueued) { - setChatInput(''); - setChatContent([]); - setChatReferences([]); - setChatAttachments([]); - setChatAttachmentNotice(''); - } - } - return; - } - if (directCodexProductRuntime && prompt === '/history') { - const nextProjectPath = requireChatProjectForUserAction(); - if (!nextProjectPath) { - return; - } - setChatInput(''); - setChatReferences([]); - setChatContent([]); - void loadProjectConversation(nextProjectPath, false, 'replace'); - return; - } - if (supervisorChatOnly || directCodexProductRuntime || designAgentActive) { - supervisorChatShouldFollowLatestRef.current = true; - } - if (directCodexProductRuntime) { - // 待发附件随本轮提交一次性交给回合;提交后清空,避免同一批附件重复挂到下一轮。 - setChatInput(''); - setChatReferences([]); - setChatAttachments([]); - setChatContent([]); - setChatAttachmentNotice(''); - setChatComposerNotice(''); - startDirectCodexConversationTurn({ - prompt, - attachments: pendingAttachments, - references, - content: chatContent, - }); - return; - } - setChatInput(''); - setChatReferences([]); - setChatContent([]); + planningChatShouldFollowLatestRef.current = true; + const clientTurnId = createAgentChatRunId('design-agent-turn'); + chatComposerRef.current?.clear(); setMessages((current) => [ ...current, { role: 'user', - text: prompt, + text: canonicalPrompt, runtimeOwned: true, updatedAt: Date.now(), }, ]); - void executeChatAgentReply({ prompt, references }); + void executeChatAgentReply({ prompt: canonicalPrompt, clientTurnId }); } - const visibleProfessionalAgentCards = agentStatusCards.filter( - (agent) => - agent.id !== PROJECT_SUPERVISOR_AGENT_ID && - (agent.runtimeStatus !== null || agent.hasRecentEvidence), - ); const useDesignAgentSurface = Boolean(designAgentView) || designAgentActive; - if (projectSupervisorOnly && supervisorChatOnly) { - const supervisorProjectPath = - localProject?.projectPath || initialProjectPath || projectPath; + if (directProjectMode) { + // 普通项目固定走 DirectProject 自己的聊天容器:订阅、历史、发送、队列和附件都由 + // 容器持有,工作台壳只提供项目身份、入口首轮需求和两条权限门。 return ( - setRuntimeConfigOpen(false)} - onConfirmConfirmation={confirmUiCommand} - onOpenRuntimeConfig={() => setRuntimeConfigOpen(true)} - onScroll={handleSupervisorChatScroll} - onShowEarlierMessages={showEarlierConversationMessages} - hasEarlierConversationMessages={hasEarlierConversationMessages} - onSubmit={handleProjectSupervisorOnlySubmit} - onToolAction={handleProjectSupervisorToolAction} - onUserInput={handleProjectSupervisorUserInput} - pendingConfirmation={pendingUiConfirmation} - pendingCommand={pendingCommand} - onCancelPendingCommand={handlePendingCommandCancel} - onConfirmPendingCommand={() => void handlePendingCommandConfirm()} - projectPath={supervisorProjectPath} - runtime={projectSupervisorRuntime} - runtimeConfigOpen={runtimeConfigOpen} - runtimeError={projectSupervisorRuntimeError} - transientReply={ - designAgentActive - ? designAgentTransientReply - : projectSupervisorTransientReply - } - hasConversationControls={projectSupervisorHasConversationControls} - hiddenConversationCount={hiddenConversationCount} - needsUserInput={projectSupervisorNeedsUserInput} - visibleMessages={visibleMessages} - workspaceStatus={workspaceStatus} - expectedRunId={projectSupervisorExpectedRunId} - versions={chatProjectVersions} + ); } - if (projectSupervisorOnly) { - return ( - + void handleCancelDirectCodexTurn()} - onRemoveAttachment={removeChatComposerAttachment} - onUploadFiles={(files) => void handleChatComposerUploadFiles(files)} - queuedTurns={chatTurnQueue} - turnCancelling={directCodexTurnCancelling} composerRef={chatComposerRef} chatProjectAssets={chatProjectAssets} - directCodex={directCodexProductRuntime} - directTurnRunning={directCodexProductRuntime && directTurnRunning} - directTurnStartedAt={directThreadChat.turnStartedAt} - directEntries={ - directCodexProductRuntime - ? selectDirectChatEntries(directThreadChat) - : [] - } hiddenConversationCount={hiddenConversationCount} hasEarlierConversationMessages={hasEarlierConversationMessages} - messagesRef={supervisorChatMessagesRef} - needsUserInput={ - directCodexProductRuntime ? false : projectSupervisorNeedsUserInput - } + messagesRef={planningChatMessagesRef} + needsUserInput={false} onCancelConfirmation={cancelUiCommandConfirmation} - onCancelPendingCommand={handlePendingCommandCancel} - onChatInputChange={handleChatComposerChange} onConfirmConfirmation={confirmUiCommand} - onConfirmPendingCommand={() => void handlePendingCommandConfirm()} - onScroll={handleSupervisorChatScroll} + onScroll={handlePlanningChatScroll} onShowEarlierMessages={showEarlierConversationMessages} - onSubmit={handleProjectSupervisorOnlySubmit} - pendingConfirmation={ - directCodexProductRuntime ? null : pendingUiConfirmation - } - pendingCommand={directCodexProductRuntime ? pendingCommand : null} + onSubmit={handlePlanningChatSubmit} + pendingConfirmation={pendingUiConfirmation} projectPath={localProject?.projectPath ?? projectPath} conversationMessages={messages} - transientReply={ - designAgentActive - ? designAgentTransientReply - : directCodexProductRuntime - ? '' - : projectSupervisorTransientReply - } + transientReply={designAgentTransientReply} showDesignReasoning={designAgentActive} designReasoning={designAgentReasoning} designReasoningEntries={ useDesignAgentSurface ? (designAgentView?.reasoningEntries ?? []) : [] } visibleMessages={visibleMessages} - visibleProfessionalAgentCards={visibleProfessionalAgentCards} - showProfessionalCollaboration={ - !directCodexProductRuntime && orchestrationMode === 'professional-dag' - } workspaceStatus={workspaceStatus} designView={useDesignAgentSurface ? designAgentView : null} onDesignApprove={ @@ -11916,7 +2293,6 @@ export function App({ designAgentReasoningTurnRef.current = { projectPath: nextProjectPath, clientTurnId, - text: '', }; designAgentPendingViewRef.current = null; setDesignAgentTransientReplyTarget(''); @@ -11955,7 +2331,7 @@ export function App({ ) { return; } - setProjectSupervisorRuntimeError(String(error)); + setProjectChatError(String(error)); }) .finally(() => { if ( @@ -12002,265 +2378,19 @@ export function App({ } : undefined } - runtime={projectSupervisorRuntime} - error={projectSupervisorRuntimeError} - runtimeByAgentId={agentRuntimeById} - controlBusy={ - directCodexProductRuntime ? supervisorChatBusy : chatAgentBusy - } - readOnly={directCodexProductRuntime} + error={projectChatError} + controlBusy={chatAgentBusy} + attachmentNotice={chatFileImportNotice} + importingFiles={chatFilesImporting} + onUploadFiles={(files) => void handleDesignComposerUploadFiles(files)} versions={chatProjectVersions} - professionalResultsByAgentId={professionalAgentResultsById} - onToolAction={handleProjectSupervisorToolAction} - onSupervisorRetry={handleProjectSupervisorRetry} - onProfessionalToolAction={handleProjectProfessionalAgentToolAction} - onProfessionalRetry={handleProjectProfessionalAgentRetry} - onUserInput={handleProjectSupervisorUserInput} /> - ); - } - - return ( -
- - {runtimeConfigOpen ? ( setRuntimeConfigOpen(false)} - onLog={(entry) => setCommandLog((current) => [...current, entry])} /> ) : null} - - {selectedAgent ? ( - - ) : null} - - {devMode ? ( -
- - -
- ) : null} -
+ ); } diff --git a/apps/ai-game-creator-shell/src/app/constants.ts b/apps/ai-game-creator-shell/src/app/constants.ts index 62fbb6f0c..7cae7093c 100644 --- a/apps/ai-game-creator-shell/src/app/constants.ts +++ b/apps/ai-game-creator-shell/src/app/constants.ts @@ -9,15 +9,11 @@ export function createLocalProjectId(): string { return `local-project-${crypto.randomUUID()}`; } -export const AGENT_RUN_HISTORY_MAX_COUNT = 100; -export const AGENT_RUN_HISTORY_INITIAL_VISIBLE_COUNT = 20; -export const AGENT_RUN_HISTORY_VISIBLE_STEP = 20; export const CONVERSATION_INITIAL_VISIBLE_COUNT = 20; export const CONVERSATION_VISIBLE_STEP = 20; /** 一次翻页操作最多连拉几页:见 ADR「分页锚点取原始条目 id」。 */ export const DIRECT_HISTORY_MAX_PAGES_PER_ACTION = 5; export const AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD = 48; -export const PROJECT_SUPERVISOR_AGENT_ID = 'project-supervisor'; export const launcherNotifications: Array<{ label: string; detail: string; diff --git a/apps/ai-game-creator-shell/src/app/initialTurnClaims.ts b/apps/ai-game-creator-shell/src/app/initialTurnClaims.ts new file mode 100644 index 000000000..b5467f2bf --- /dev/null +++ b/apps/ai-game-creator-shell/src/app/initialTurnClaims.ts @@ -0,0 +1,22 @@ +/** + * 入口首轮需求的认领记录。 + * + * 首页/立项链路把同一条首轮需求带进工作台,DirectProject 与立项策划是同一页面上的 + * 不同入口;认领按页面保存并按「项目路径 + claimScope」去重,保证同一条需求只被一个 + * 入口发出。 + */ +const initialTurnClaimsByPage = new WeakMap>(); + +export function claimInitialTurnForPage(projectPath: string, scope = '') { + let claimedProjectPaths = initialTurnClaimsByPage.get(window); + if (!claimedProjectPaths) { + claimedProjectPaths = new Set(); + initialTurnClaimsByPage.set(window, claimedProjectPaths); + } + const claimKey = `${projectPath}\u0000${scope}`; + if (claimedProjectPaths.has(claimKey)) { + return false; + } + claimedProjectPaths.add(claimKey); + return true; +} diff --git a/apps/ai-game-creator-shell/src/app/types.ts b/apps/ai-game-creator-shell/src/app/types.ts index 32b993a58..210cbd489 100644 --- a/apps/ai-game-creator-shell/src/app/types.ts +++ b/apps/ai-game-creator-shell/src/app/types.ts @@ -1029,96 +1029,6 @@ export interface AgentStatusCard { export type AgentTaskGraphState = 'active' | 'carried' | 'ready'; -export type PendingCommand = - | { - id: 'game.generate_draft'; - prompt: string; - } - | { - id: 'game.run_local'; - } - | { - id: 'asset.upload'; - file: File; - } - | { - id: 'asset.register'; - localPath: string; - kind: GameCreationAppAssetKind; - mediaType: string; - } - | { - id: 'command.run_limited'; - commandId: string; - } - | { - id: 'project.create'; - projectPath: string; - } - | { - id: 'project.checkpoint'; - } - | { - id: 'project.export_package'; - } - | { - id: 'project.index'; - } - | { - id: 'project.restore'; - checkpointId: string; - } - | { - id: 'project.policy_write'; - policy: ProjectPermissionPolicy; - } - | { - id: 'preview.start'; - } - | { - id: 'preview.open'; - } - | { - id: 'agent.kill' | 'agent.retry' | 'agent.resume'; - detail?: string; - } - | { - id: 'memory.write'; - scope: MemoryScope; - content: string; - mode?: MemoryWriteMode; - } - | { - id: 'memory.delete'; - scope: MemoryScope; - } - | { - id: 'canvas.project_open'; - canvasProjectId: string; - } - | { - id: 'canvas.project_sync'; - canvasProjectId: string; - } - | { - id: 'canvas.asset_import'; - localPath: string; - canvasProjectId: string; - canvasAssetId: string; - canvasAssetObjectId?: string; - kind: GameCreationAppAssetKind; - mediaType: string; - } - | { - id: 'canvas.asset_generate'; - prompt: string; - } - | { - id: 'canvas.export_import'; - exportPath: string; - canvasProjectId: string; - }; - export interface PendingUiConfirmation { commandId: GameCreationAppCommandDescriptor['id']; detail: string; diff --git a/apps/ai-game-creator-shell/src/components/RichTextInput.tsx b/apps/ai-game-creator-shell/src/components/RichTextInput.tsx new file mode 100644 index 000000000..02c942e50 --- /dev/null +++ b/apps/ai-game-creator-shell/src/components/RichTextInput.tsx @@ -0,0 +1,102 @@ +import { LexicalComposer } from '@lexical/react/LexicalComposer'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; +import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import { + COMMAND_PRIORITY_HIGH, + type EditorState, + KEY_ENTER_COMMAND, + type Klass, + type LexicalNode, +} from 'lexical'; +import type { ReactElement, ReactNode, Ref } from 'react'; +import { useEffect } from 'react'; + +type RichTextInputProps = { + namespace: string; + nodes: Klass[]; + initialEditorState?: EditorState | null; + contentEditable?: ReactElement; + placeholder?: ReactElement; + containerClassName?: string; + containerRef?: Ref; + disabled?: boolean; + onChange?: (editorState: EditorState) => void; + onEnter?: () => void; + children?: ReactNode; +}; + +function SubmitOnEnter({ onEnter }: { onEnter?: () => void }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (!onEnter) return undefined; + return editor.registerCommand( + KEY_ENTER_COMMAND, + (event) => { + if (!event || event.shiftKey || event.isComposing) return false; + event.preventDefault(); + onEnter(); + return true; + }, + COMMAND_PRIORITY_HIGH, + ); + }, [editor, onEnter]); + + return null; +} + +function SetEditorEditable({ disabled }: { disabled: boolean }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + editor.setEditable(!disabled); + }, [disabled, editor]); + + return null; +} + +export default function RichTextInput({ + namespace, + nodes, + initialEditorState, + contentEditable = , + placeholder, + containerClassName, + containerRef, + disabled = false, + onChange, + onEnter, + children, +}: RichTextInputProps) { + return ( + { + throw error; + }, + }} + > +
+ + {children} + + + {onChange ? : null} +
+
+ ); +} diff --git a/apps/ai-game-creator-shell/src/features/agent-runtime/index.ts b/apps/ai-game-creator-shell/src/features/agent-runtime/index.ts index 268e61dbf..9f8ccaddf 100644 --- a/apps/ai-game-creator-shell/src/features/agent-runtime/index.ts +++ b/apps/ai-game-creator-shell/src/features/agent-runtime/index.ts @@ -1,2 +1 @@ export * from './model'; -export * from './panels'; diff --git a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts index c8baf7bb4..246b9ab14 100644 --- a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts +++ b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts @@ -2,132 +2,21 @@ import type { GameCreationAppManifest, GameCreationAppTaskState, } from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { PROJECT_SUPERVISOR_AGENT_ID, seedManifest } from '../../app/constants'; +import { seedManifest } from '../../app/constants'; import type { - AgentConversationSessionListResult, - AgentConversationSessionRecord, - AgentGoalRecord, AgentRuntimeEventRecord, - AgentRuntimePendingToolActionSummary, AgentRuntimePlanStep, - AgentRuntimeResponseStream, AgentRuntimeResult, AgentRuntimeState, - AgentRuntimeSteerResult, AgentRuntimeTaskQueueSummary, AgentRuntimeTaskRecord, ChatMessage, - LocalConversationMessageRecord, - LocalProjectKind, - TauriInvoke, } from '../../app/types'; -const AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX = 'runtime-public-status-'; -const AGENT_RUNTIME_TASK_MESSAGE_ID_PREFIX = 'runtime-task-'; -const AGENT_RUNTIME_STEER_MESSAGE_ID_PREFIX = 'agent-steer-'; -const AGENT_RUNTIME_MESSAGE_CORRELATION_PATTERN = /^[0-9a-f]{32}$/; - -export type ProjectSupervisorRuntimeSubmission = { - runProfile: 'standard' | 'autonomous-game-build'; - source: 'project-supervisor-gui'; -}; - -export function resolveProjectSupervisorRuntimeSubmission({ - workspaceProjectKind, - orchestrationMode, - supervisorChatOnly, -}: { - workspaceProjectKind: LocalProjectKind; - orchestrationMode: 'single-supervisor' | 'professional-dag'; - supervisorChatOnly: boolean; -}): ProjectSupervisorRuntimeSubmission { - if (workspaceProjectKind === 'godot' || supervisorChatOnly) { - return { - runProfile: 'standard', - source: 'project-supervisor-gui', - }; - } - if (orchestrationMode !== 'single-supervisor') { - return { - runProfile: 'autonomous-game-build', - source: 'project-supervisor-gui', - }; - } - return { - runProfile: 'standard', - source: 'project-supervisor-gui', - }; -} - -function agentRuntimeMessageCorrelationId( - messageId: string | null | undefined, -) { - const normalized = messageId?.trim() ?? ''; - const prefix = normalized.startsWith( - AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX, - ) - ? AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX - : normalized.startsWith(AGENT_RUNTIME_TASK_MESSAGE_ID_PREFIX) - ? AGENT_RUNTIME_TASK_MESSAGE_ID_PREFIX - : normalized.startsWith(AGENT_RUNTIME_STEER_MESSAGE_ID_PREFIX) - ? AGENT_RUNTIME_STEER_MESSAGE_ID_PREFIX - : null; - if (!prefix) { - return null; - } - const correlationId = normalized.slice(prefix.length).split('-', 1)[0] ?? ''; - return AGENT_RUNTIME_MESSAGE_CORRELATION_PATTERN.test(correlationId) - ? correlationId - : null; -} - -export function createLocalConversationDraftMessage( - content: string, - updatedAt = Date.now(), -): LocalConversationMessageRecord { - return { - schemaVersion: 'game-creator-conversation.v1', - role: 'assistant', - content, - agentId: null, - updatedAt, - }; -} export function createAgentChatRunId(prefix: string) { return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } -export function parseAgentGoalItems(value: string) { - return value - .split(/\r?\n/) - .map((item) => item.trim()) - .filter(Boolean); -} - -export function formatAgentConversationSessionMeta( - session: AgentConversationSessionRecord, - sessions: AgentConversationSessionRecord[], - activeSessionId: string | null, -) { - const details = [String(session.messageCount)]; - if (session.sessionId === activeSessionId) { - details.push('活动'); - } - if (session.forkedFromSessionId) { - const source = sessions.find( - (candidate) => candidate.sessionId === session.forkedFromSessionId, - ); - const sourceLabel = source?.title ?? session.forkedFromSessionId; - details.push( - session.forkedMessageCount === null || - session.forkedMessageCount === undefined - ? `分支自 ${sourceLabel}` - : `分支自 ${sourceLabel}(${session.forkedMessageCount} 条)`, - ); - } - return details.join(' · '); -} - export function agentRuntimePlanStepsFromPlan( plan: string[], ): AgentRuntimePlanStep[] { @@ -390,30 +279,6 @@ export function normalizeAgentRuntimeState( }; } -export function mergeAgentGoalRecordFromRuntime( - goal: AgentGoalRecord | null, - runtime: AgentRuntimeState, -) { - if ( - !goal || - !runtime.goalId || - goal.goalId !== runtime.goalId || - goal.agentId !== runtime.agentId || - goal.sessionId !== runtime.sessionId - ) { - return goal; - } - return { - ...goal, - revision: runtime.goalRevision ?? goal.revision, - status: runtime.goalStatus ?? goal.status, - outcome: runtime.goalOutcome ?? goal.outcome, - constraints: runtime.goalConstraints ?? goal.constraints, - verification: runtime.goalVerification ?? goal.verification, - updatedAt: Math.max(goal.updatedAt, runtime.updatedAt), - }; -} - export function mergeAgentRuntimeStateIntoMap( current: Record, incoming: AgentRuntimeState, @@ -503,149 +368,11 @@ export function agentRuntimeStateFromResult( ); } -export function normalizeProjectSupervisorResponseStream( - stream: AgentRuntimeResponseStream | null | undefined, - runtime: AgentRuntimeState, -) { - if (!stream) { - return null; - } - const integerFields = [ - stream.appliedSteerCursor, - stream.responseRevision, - stream.sequence, - stream.startedAt, - stream.updatedAt, - ]; - if ( - stream.schemaVersion !== 'game-creator-runtime-response-stream.v1' || - stream.agentId !== PROJECT_SUPERVISOR_AGENT_ID || - stream.agentId !== runtime.agentId || - stream.taskId !== runtime.taskId || - stream.sessionId !== runtime.sessionId || - stream.runId !== runtime.runId || - stream.requestKind !== 'final-reply' || - !stream.requestSlot.trim() || - integerFields.some( - (value) => - typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0, - ) || - stream.startedAt <= 0 || - stream.updatedAt < stream.startedAt || - typeof stream.accumulatedText !== 'string' || - Array.from(stream.accumulatedText).length > 32_000 || - /<\/?think>/i.test(stream.accumulatedText) - ) { - return null; - } - if ( - typeof runtime.appliedSteerCursor === 'number' && - stream.appliedSteerCursor !== runtime.appliedSteerCursor - ) { - return null; - } - if ((runtime.queuedSteerCount ?? 0) > 0) { - return null; - } - if ( - typeof runtime.loopIteration === 'number' && - Number.isSafeInteger(runtime.loopIteration) && - stream.requestSlot !== - `final-reply-loop-${runtime.loopIteration}-revision-${stream.responseRevision}` - ) { - return null; - } - if ( - (stream.status === 'streaming' || stream.status === 'ready') && - (runtime.status !== 'running' || - !['response', 'finalizing'].includes(runtime.phase)) - ) { - return null; - } - if (stream.status === 'ready' && !stream.accumulatedText.trim()) { - return null; - } - return stream; -} - -export function sameProjectSupervisorResponseStream( - left: AgentRuntimeResponseStream, - right: AgentRuntimeResponseStream, -) { - return ( - left.agentId === right.agentId && - left.taskId === right.taskId && - left.sessionId === right.sessionId && - left.runId === right.runId && - left.requestKind === right.requestKind && - left.requestSlot === right.requestSlot && - left.appliedSteerCursor === right.appliedSteerCursor && - left.responseRevision === right.responseRevision - ); -} - /** * Durable identity for one final-reply response. The response text is * intentionally excluded so two turns with identical wording remain * distinct messages in the conversation. */ -export function projectSupervisorResponseStreamIdentity( - stream: Pick< - AgentRuntimeResponseStream, - 'runId' | 'requestSlot' | 'responseRevision' - >, -) { - return [stream.runId, stream.requestSlot, stream.responseRevision].join( - '\u001f', - ); -} - -export function mergeProjectSupervisorResponseStream( - current: AgentRuntimeResponseStream | null, - incoming: AgentRuntimeResponseStream | null | undefined, - runtime: AgentRuntimeState, -) { - const currentForRuntime = normalizeProjectSupervisorResponseStream( - current, - runtime, - ); - if (!incoming) { - return null; - } - const next = normalizeProjectSupervisorResponseStream(incoming, runtime); - if (!next) { - return currentForRuntime; - } - if (next.status !== 'streaming' && next.status !== 'ready') { - return null; - } - if ( - !currentForRuntime || - !sameProjectSupervisorResponseStream(currentForRuntime, next) - ) { - return next; - } - if (next.sequence <= currentForRuntime.sequence) { - return currentForRuntime; - } - return next; -} - -export function conversationContainsProjectSupervisorResponseStream( - messages: LocalConversationMessageRecord[], - stream: AgentRuntimeResponseStream | null | undefined, -) { - return Boolean( - stream?.accumulatedText.trim() && - messages.some( - (message) => - message.role === 'assistant' && - message.content === stream.accumulatedText && - message.updatedAt >= stream.startedAt, - ), - ); -} - export function agentRuntimeWaitingOnFromPhase(phase: string) { switch (phase) { case 'planning': @@ -708,36 +435,6 @@ export function agentRuntimeNextStepFromPhase(phase: string) { } } -export function agentRuntimeStartStatus(result: AgentRuntimeResult) { - const pendingTask = (result.recentTasks ?? []).find( - (task) => task.status === 'pending', - ); - return pendingTask - ? `已加入后台队列:${pendingTask.runId}` - : `已启动后台任务:${result.state.runId}`; -} - -export function agentRuntimeStartedRunId( - result: AgentRuntimeResult, - requestedRunId: string, -) { - if (result.acceptedRunId?.trim()) { - return result.acceptedRunId.trim(); - } - if ( - result.recentTasks?.some((task) => task.runId === requestedRunId) || - result.state.runId === requestedRunId - ) { - return requestedRunId; - } - return ( - result.taskQueue?.latestRunId ?? - result.state.taskQueue?.latestRunId ?? - result.state.runId ?? - requestedRunId - ); -} - export function isAgentRuntimeTerminalState(runtime: AgentRuntimeState) { return ( ['completed', 'failed', 'cancelled', 'needs-reconciliation'].includes( @@ -795,202 +492,6 @@ export function agentRuntimeNeedsUserInput( ); } -export const AGENT_RUNTIME_USER_INPUT_REQUEST_TOOL = 'user.input_request'; - -export function agentRuntimePendingActionHasDedicatedCard( - action: AgentRuntimePendingToolActionSummary | null | undefined, -) { - return action?.tool === AGENT_RUNTIME_USER_INPUT_REQUEST_TOOL; -} - -export function matchingAgentRuntimeForSteer( - runtimes: Array, - agentId: string, - sessionId: string | null, - requestedRunProfile: NonNullable< - AgentRuntimeState['runProfile'] - > = 'standard', - requestedSource?: string, -) { - if (!sessionId) { - return null; - } - return ( - runtimes.find( - (runtime) => - runtime?.agentId === agentId && - runtime.sessionId === sessionId && - (runtime.runProfile ?? 'standard') === requestedRunProfile && - (!requestedSource || runtime.source === requestedSource) && - isAgentRuntimeSteerableState(runtime), - ) ?? null - ); -} - -export function projectSupervisorActiveSessionIdFromList( - sessionList: AgentConversationSessionListResult, -) { - if ( - !sessionList || - sessionList.agentId !== PROJECT_SUPERVISOR_AGENT_ID || - !Array.isArray(sessionList.sessions) - ) { - throw new Error('项目总控 Agent 会话列表返回格式无效'); - } - const activeSession = sessionList.activeSessionId - ? sessionList.sessions.find( - (session) => - session.sessionId === sessionList.activeSessionId && - session.archivedAt === null, - ) - : null; - return ( - activeSession?.sessionId || - sessionList.sessions.find((session) => session.archivedAt === null) - ?.sessionId || - null - ); -} - -export async function readProjectSupervisorActiveSessionId( - invoke: TauriInvoke, - projectPath: string, -) { - try { - const sessionList = await invoke( - 'list_game_creator_agent_sessions', - { - projectPath, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - }, - ); - return projectSupervisorActiveSessionIdFromList(sessionList); - } catch (error) { - if (isMissingAgentSessionCommandError(error)) { - return null; - } - throw error; - } -} - -export async function ensureProjectSupervisorActiveSessionId( - invoke: TauriInvoke, - projectPath: string, -) { - const existingSessionId = await readProjectSupervisorActiveSessionId( - invoke, - projectPath, - ); - if (existingSessionId) { - return existingSessionId; - } - const sessionList = await invoke( - 'create_game_creator_agent_session', - { - projectPath, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - title: '项目总控', - }, - ); - const sessionId = projectSupervisorActiveSessionIdFromList(sessionList); - if (!sessionId) { - throw new Error('项目总控 Agent active Session 创建失败'); - } - return sessionId; -} - -export async function submitProjectSupervisorRuntimeTask({ - invoke, - projectPath, - sessionId, - prompt, - runtime, - runProfile, - source, -}: { - invoke: TauriInvoke; - projectPath: string; - sessionId: string; - prompt: string; - runtime: AgentRuntimeState | null; - runProfile: 'standard' | 'autonomous-game-build'; - source: ProjectSupervisorRuntimeSubmission['source']; -}) { - const steerRuntime = matchingAgentRuntimeForSteer( - [runtime], - PROJECT_SUPERVISOR_AGENT_ID, - sessionId, - runProfile, - source, - ); - if (steerRuntime) { - const steer = await invoke( - 'steer_game_creator_agent_runtime_task', - { - projectPath, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - sessionId, - runId: steerRuntime.runId, - steerId: createAgentChatRunId('project-supervisor-steer'), - instruction: prompt, - runProfile, - source, - }, - ); - return { - mode: 'steer' as const, - runtimeResult: steer.runtime, - acceptedRunId: steerRuntime.runId, - }; - } - const requestedRunId = createAgentChatRunId('project-supervisor-task'); - const runtimeResult = await invoke( - 'start_game_creator_supervisor_runtime_task', - { - projectPath, - sessionId, - task: prompt, - runId: requestedRunId, - runProfile, - source, - }, - ); - return { - mode: 'start' as const, - runtimeResult, - acceptedRunId: agentRuntimeStartedRunId(runtimeResult, requestedRunId), - }; -} - -export function agentRuntimeSteerStatus(result: AgentRuntimeSteerResult) { - const runId = result.runtime.state.runId; - if (result.status === 'restarted') { - return `目标已变化,正在新 Run 重新理解并执行:${runId}`; - } - if (result.providerInterrupted) { - return `智能服务已根据追加指令调整,旧请求已安全中断:${runId}`; - } - if (result.interruptDecision === false) { - return `智能服务已回复且判定无需中断,当前 Run 继续:${runId}`; - } - if (result.interruptDecision === true) { - return `智能服务已判定需要改向;旧请求已结束或新规划已开始:${runId}`; - } - if (result.status === 'applied') { - return `追加指令已应用,当前 Run 正在继续:${runId}`; - } - return `追加指令已排队,等待当前 Run 应用:${runId}`; -} - -export function agentRuntimeCancelStatus( - runtime: AgentRuntimeState, - runId: string, -) { - return runtime.status === 'cancelling' || runtime.phase === 'cancelling' - ? `正在取消后台任务:${runId}` - : `已取消后台任务:${runId}`; -} - function agentRuntimeProviderRetryStatus(runtime: AgentRuntimeState) { if (runtime.phase !== 'waiting-for-provider-retry') { return null; @@ -1072,32 +573,6 @@ export function agentRuntimeConversationStatus(runtime: AgentRuntimeState) { return waitingOn ? `Agent 正在运行,等待${waitingOn}` : 'Agent 正在运行'; } -export function projectSupervisorChatRuntimeStatus(runtime: AgentRuntimeState) { - if (runtime.status === 'idle' || runtime.phase === 'idle') { - return '等待输入'; - } - if ( - runtime.status === 'needs-reconciliation' || - runtime.phase === 'needs-reconciliation' - ) { - return runtime.error - ? projectRuntimeVisibleError(runtime.error, '项目总控 Agent', true) - : '项目总控 Agent 运行状态需要核对,请打开运行详情后重试'; - } - if (isAgentRuntimeTerminalState(runtime)) { - if (runtime.status === 'failed' || runtime.phase === 'failed') { - return runtime.error - ? projectRuntimeVisibleError(runtime.error, '项目总控 Agent', true) - : 'Agent 运行失败'; - } - if (runtime.status === 'cancelled' || runtime.phase === 'cancelled') { - return '本轮已取消'; - } - return '本轮已完成'; - } - return agentRuntimeConversationStatus(runtime); -} - export function formatAgentRuntimeEvent(event: AgentRuntimeEventRecord) { const isFailureEvent = [ 'error', @@ -1146,21 +621,6 @@ export function formatAgentRuntimeTaskQueue( return `任务队列:${parts.join(' · ')}`; } -export function formatAgentRuntimeLoopProgress( - runtime: Pick< - AgentRuntimeState, - 'loopIteration' | 'maxLoopIterations' | 'toolActionBudget' - >, -) { - const loopIteration = runtime.loopIteration ?? 0; - if (loopIteration <= 0) { - return null; - } - return `Loop:${loopIteration}/${runtime.maxLoopIterations ?? 3} · 工具预算 ${ - runtime.toolActionBudget ?? 3 - }`; -} - export function formatAgentRuntimePlanStep( step: AgentRuntimePlanStep, fallbackIndex = 0, @@ -1193,31 +653,6 @@ export function agentRuntimeActivePlanStep( ); } -export function agentRuntimeCanCancel(status: string) { - return [ - 'pending', - 'running', - 'waiting-for-confirmation', - 'waiting-for-user-input', - ].includes(status); -} - -export function agentRuntimeCanRetry(status: string) { - return ['cancelled', 'failed', 'completed', 'idle'].includes(status); -} - -export function agentGoalStatusIsPaused(status: string | null | undefined) { - return ['pause-requested', 'pausing', 'paused'].includes(status ?? ''); -} - -export function agentGoalStatusIsTerminal(status: string | null | undefined) { - return ['completed', 'cleared'].includes(status ?? ''); -} - -export function agentRuntimeCanConfirm(status: string) { - return status === 'waiting-for-confirmation'; -} - export function formatAgentRuntimeDelegationSource(runtime: { source: string; parentAgentId?: string | null; @@ -1248,20 +683,6 @@ export function createAgentRuntimeUserInputResponseId() { return `app-user-input-${Date.now().toString(36)}-${entropy}`.slice(0, 160); } -export function isMissingAgentSessionCommandError(error: unknown) { - const message = error instanceof Error ? error.message : String(error); - const normalized = message.toLowerCase(); - return ( - normalized.includes('list_game_creator_agent_sessions') && - (normalized.includes('not found') || - normalized.includes('unknown command') || - normalized.includes('unexpected invoke') || - normalized.includes('unexpected command') || - normalized.includes('不存在') || - normalized.includes('未找到')) - ); -} - export function isMissingAgentRuntimeResumeCommandError(error: unknown) { const message = error instanceof Error ? error.message : String(error); const normalized = message.toLowerCase(); @@ -1276,20 +697,6 @@ export function isMissingAgentRuntimeResumeCommandError(error: unknown) { ); } -export function isMissingAgentGoalCommandError(error: unknown) { - const message = error instanceof Error ? error.message : String(error); - const normalized = message.toLowerCase(); - return ( - normalized.includes('game_creator_agent_goal') && - (normalized.includes('not found') || - normalized.includes('unknown command') || - normalized.includes('unexpected invoke') || - normalized.includes('unexpected command') || - normalized.includes('不存在') || - normalized.includes('未找到')) - ); -} - export function createDefaultChatMessages(): ChatMessage[] { // 默认问候「想做什么游戏?」已移除:它在对话记录里没有信息量,而且会出现在用户消息之后。 // 空对话由空状态提示(panels.tsx 的引导文案)承担,不再往消息列表里塞占位消息。 @@ -1315,210 +722,6 @@ export function projectNameFromPath(projectPath: string) { ); } -export function projectWorkspaceStatusForDisplay(workspaceStatus: string) { - const openedPrefix = '已打开:'; - if (!workspaceStatus.startsWith(openedPrefix)) { - return workspaceStatus; - } - const projectPath = workspaceStatus.slice(openedPrefix.length).trim(); - if (!/[\\/]/u.test(projectPath)) { - return workspaceStatus; - } - return `${openedPrefix}${projectNameFromPath(projectPath)}`; -} - -export function mergeProjectSupervisorConversation( - projectRecords: LocalConversationMessageRecord[], - supervisorRecords: LocalConversationMessageRecord[], -): ChatMessage[] { - const supervisorRecordIndexByCorrelation = new Map(); - supervisorRecords.forEach((record, index) => { - if (record.role !== 'user') { - return; - } - const correlationId = agentRuntimeMessageCorrelationId(record.messageId); - if ( - correlationId && - !supervisorRecordIndexByCorrelation.has(correlationId) - ) { - supervisorRecordIndexByCorrelation.set(correlationId, index); - } - }); - const records = [ - ...projectRecords.map((record, index) => { - const runtimeOwned = Boolean( - record.messageId - ?.trim() - .startsWith(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX), - ); - const accepted = - runtimeOwned && - record.content === '任务已接收,项目总控 Agent 正在启动处理。'; - const correlationId = agentRuntimeMessageCorrelationId(record.messageId); - const supervisorRecordIndex = correlationId - ? supervisorRecordIndexByCorrelation.get(correlationId) - : undefined; - const runtimeRecordIndex = - supervisorRecordIndex ?? supervisorRecords.length + index; - return { - record, - runtimeOwned, - sameSecondLane: runtimeOwned ? 1 : 0, - sameSecondOrder: runtimeOwned - ? runtimeRecordIndex * 4 + (accepted ? 1 : 2) - : index, - stableIndex: index, - }; - }), - ...supervisorRecords.map((record, index) => { - const userMessage = record.role === 'user'; - return { - record, - runtimeOwned: true, - sameSecondLane: 1, - // Conversation timestamps have second precision. Runtime task and - // public status IDs carry the same opaque run correlation digest, so - // status ordering follows its actual task record instead of guessing - // from independent user/accepted/terminal category counters. - sameSecondOrder: index * 4 + (userMessage ? 0 : 3), - stableIndex: projectRecords.length + index, - }; - }), - ] - .filter( - ({ record }) => record.role === 'user' || record.role === 'assistant', - ) - .sort( - (left, right) => - left.record.updatedAt - right.record.updatedAt || - left.sameSecondLane - right.sameSecondLane || - left.sameSecondOrder - right.sameSecondOrder || - left.stableIndex - right.stableIndex, - ); - const seen = new Set(); - const messages: ChatMessage[] = []; - for (const { record, runtimeOwned } of records) { - const messageId = record.messageId?.trim(); - const identity = messageId - ? `message:${messageId}` - : `identity:${JSON.stringify([ - record.role, - record.agentId, - record.updatedAt, - record.content, - ])}`; - if (seen.has(identity)) { - continue; - } - seen.add(identity); - messages.push({ - role: record.role as ChatMessage['role'], - text: record.content, - messageId: messageId ?? null, - agentId: record.agentId, - updatedAt: record.updatedAt, - runtimeOwned, - }); - } - return messages.length > 0 ? messages : createDefaultChatMessages(); -} - -export function projectSupervisorRuntimeStatusLabel( - runtime: AgentRuntimeState | null, - runtimeError: string, -) { - if ( - runtime?.status === 'needs-reconciliation' || - runtime?.phase === 'needs-reconciliation' - ) { - return '待核对'; - } - if ( - runtimeError || - runtime?.status === 'failed' || - runtime?.phase === 'failed' - ) { - return '失败'; - } - if (!runtime) { - return null; - } - if ( - runtime.userInputRequest || - runtime.status === 'waiting-for-user-input' || - runtime.phase === 'waiting-for-user-input' - ) { - return '需要回答'; - } - if ( - runtime.pendingToolAction || - runtime.status === 'waiting-for-confirmation' || - runtime.phase === 'waiting-for-confirmation' - ) { - return '等待确认'; - } - if ( - runtime.phase === 'waiting-for-delegate-receipts' || - runtime.phase === 'waiting-for-isolated-join' - ) { - return '等待专业 Agent'; - } - if (runtime.phase === 'waiting-for-manifest-tasks') { - return '等待项目任务'; - } - if ( - ['action', 'observation', 'executing'].includes(runtime.phase) || - runtime.status === 'executing' - ) { - return '执行'; - } - if (runtime.phase === 'response' || runtime.phase === 'finalizing') { - return '回复中'; - } - if (runtime.status === 'cancelled' || runtime.phase === 'cancelled') { - return '已取消'; - } - if (runtime.status === 'completed' || runtime.phase === 'completed') { - return '已完成'; - } - if (runtime.status === 'idle') { - return null; - } - return '分析'; -} - -export function projectSupervisorCollaboratingAgentRuntimes( - supervisorRuntime: AgentRuntimeState | null, - runtimeByAgentId: Record, -) { - if (!supervisorRuntime?.runId) { - return []; - } - const runtimesByAgentId = new Map(); - for (const runtime of Object.values(runtimeByAgentId)) { - const isVisibleChildSource = [ - 'agent-delegate', - 'agent-delegate-retry', - ].includes(runtime?.source ?? ''); - if ( - !runtime || - runtime.agentId === PROJECT_SUPERVISOR_AGENT_ID || - !isVisibleChildSource || - runtime.parentAgentId !== PROJECT_SUPERVISOR_AGENT_ID || - runtime.parentRunId !== supervisorRuntime.runId - ) { - continue; - } - const previous = runtimesByAgentId.get(runtime.agentId); - if (!previous || runtime.updatedAt >= previous.updatedAt) { - runtimesByAgentId.set(runtime.agentId, runtime); - } - } - return Array.from(runtimesByAgentId.values()).sort((left, right) => - left.agentId.localeCompare(right.agentId), - ); -} - export function projectProfessionalAgentLabel(agentId: string) { if (agentId === 'design-foundation') { return '玩法策划 Agent'; @@ -1560,65 +763,6 @@ export function isAgentFinalizationMessageId( return /^agent-finalization-[0-9a-f]{32}$/u.test(messageId ?? ''); } -export function projectRuntimeStatusPresentation(runtime: AgentRuntimeState) { - if (runtime.phase === 'needs-reconciliation') { - return { label: '待核对', tone: 'failed' }; - } - if ( - runtime.userInputRequest || - runtime.status === 'waiting-for-user-input' || - runtime.phase === 'waiting-for-user-input' - ) { - return { label: '待回答', tone: 'waiting' }; - } - if ( - runtime.pendingToolAction || - runtime.status === 'waiting-for-confirmation' || - runtime.phase === 'waiting-for-confirmation' - ) { - return { label: '待确认', tone: 'waiting' }; - } - if (runtime.status === 'failed' || runtime.phase === 'failed') { - return { label: '失败', tone: 'failed' }; - } - if (runtime.status === 'completed' || runtime.phase === 'completed') { - return { label: '已完成', tone: 'completed' }; - } - if (runtime.status === 'cancelled' || runtime.phase === 'cancelled') { - return { label: '已取消', tone: 'idle' }; - } - if (runtime.status === 'idle' || runtime.phase === 'idle') { - return { label: '等待中', tone: 'idle' }; - } - if (runtime.phase === 'planning') { - return { label: '分析中', tone: 'running' }; - } - if ( - runtime.phase === 'waiting-for-delegate-receipts' || - runtime.phase === 'waiting-for-isolated-join' - ) { - return { label: '协作中', tone: 'running' }; - } - if (runtime.phase === 'waiting-for-manifest-tasks') { - return { - label: '项目任务中', - tone: 'running', - }; - } - return { label: '执行中', tone: 'running' }; -} - -export function projectRuntimePlanProgress(runtime: AgentRuntimeState) { - const steps = (runtime.planSteps ?? []).filter( - (step) => agentRuntimePlanStepText(step).length > 0, - ); - return { - completed: steps.filter((step) => step.status === 'completed').length, - total: steps.length, - active: agentRuntimeActivePlanStep(runtime), - }; -} - export function projectRuntimeVisibleCurrentWork(runtime: AgentRuntimeState) { const providerRetryStatus = agentRuntimeProviderRetryStatus(runtime); if (providerRetryStatus) { @@ -2071,6 +1215,11 @@ export function projectRuntimeVisibleError( visibleMessage === 'LLM 服务暂时不可用,请检查配置后重试' || visibleMessage === '请先回答项目总控 Agent 当前的澄清问题' || visibleMessage === '待回答请求已变更,请刷新 Runtime 状态' || + // 工作台壳直接写入的运行态提示(App.tsx 的 setProjectChatError):这些文案本身 + // 就是给用户看的动作指引,不能被通用兜底改成「策划 Agent 执行失败」。 + visibleMessage === '待回答问题已变更,请刷新策划状态' || + visibleMessage === '需要在 Tauri App 内运行。' || + visibleMessage === '请先初始化本地项目' || /^回答提交失败:[^\r\n]{1,80}$/.test(visibleMessage); if ( preservePublicMessage && @@ -2082,125 +1231,6 @@ export function projectRuntimeVisibleError( return `${subject} 执行失败,请稍后重试`; } -export function projectSupervisorVisibleConversationText( - message: string, - role: ChatMessage['role'] | 'tool' = 'assistant', - subject = '项目总控 Agent', -) { - const failurePrefix = '后台任务失败:'; - if (role !== 'assistant' || !message.startsWith(failurePrefix)) { - return message; - } - return projectRuntimeVisibleError( - message.slice(failurePrefix.length), - subject, - true, - ); -} - -export function projectSupervisorChatMessageText( - message: Pick, -) { - return message.role === 'assistant' - ? projectSupervisorVisibleConversationText(message.text, message.role) - : message.text; -} - -export function projectRuntimeVisibleToolSummary(summary: string) { - return summary - .split('·') - .map((part) => part.trim()) - .filter( - (part) => - part.length > 0 && - !/^(contentChars|contentBytes|sha256|fingerprint)=/i.test(part), - ) - .join(' · '); -} - -export function projectSupervisorPendingActionPresentation( - action: AgentRuntimePendingToolActionSummary, -) { - if (action.tool === 'agent.delegate') { - const agentId = - action.inputSummary?.match(/(?:^|·)\s*agentId=([^·]+)/)?.[1]?.trim() ?? - ''; - const agentLabel = agentId - ? projectProfessionalAgentLabel(agentId) - : '专业 Agent'; - const isRepair = - action.inputSummary?.includes('repairOf=') && - !action.inputSummary.includes('repairOf=null'); - return { - title: isRepair ? `安排${agentLabel}返工` : `安排${agentLabel}执行任务`, - detail: isRepair - ? '确认后将在当前项目创建一轮带原验收合同的返工任务' - : '确认后将在当前项目启动新的专业任务', - }; - } - return { - title: action.tool, - detail: action.inputSummary - ? projectRuntimeVisibleToolSummary(action.inputSummary) - : '确认后继续当前项目任务', - }; -} - -export function projectSupervisorPendingRepairMatchesProfessional( - action: AgentRuntimePendingToolActionSummary | null | undefined, - runtime: AgentRuntimeState, -) { - const summary = action?.inputSummary ?? ''; - return Boolean( - action?.tool === 'agent.delegate' && - runtime.delegationId && - summary.includes(`agentId=${runtime.agentId}`) && - summary.includes(`repairOf=${runtime.delegationId}`), - ); -} - -export function formatProjectRuntimeUpdatedAt(updatedAt: number) { - if (!Number.isFinite(updatedAt) || updatedAt <= 0) { - return '更新时间未知'; - } - const milliseconds = - updatedAt < 1_000_000_000_000 ? updatedAt * 1000 : updatedAt; - return `${new Date(milliseconds).toLocaleTimeString('zh-CN', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} 更新`; -} - -export function formatProjectSupervisorCompactProgress( - runtime: AgentRuntimeState, - collaboratingAgentCount: number, -) { - const planSteps = (runtime.planSteps ?? []).filter( - (step) => agentRuntimePlanStepText(step).length > 0, - ); - const completedPlanStepCount = planSteps.filter( - (step) => step.status === 'completed', - ).length; - const activePlanStep = agentRuntimeActivePlanStep(runtime); - const currentPlanStep = activePlanStep - ? agentRuntimePlanStepText(activePlanStep) - : planSteps.length > 0 && completedPlanStepCount === planSteps.length - ? '已完成' - : '暂无'; - const waitingOn = - runtime.waitingOn?.trim() || agentRuntimeWaitingOnFromPhase(runtime.phase); - const nextStep = - runtime.nextStep?.trim() || agentRuntimeNextStepFromPhase(runtime.phase); - return [ - `计划完成:${completedPlanStepCount}/${planSteps.length}`, - `当前步骤:${currentPlanStep}`, - `等待:${waitingOn}`, - `下一步:${nextStep}`, - `专业 Agent 协作:${collaboratingAgentCount}`, - ].join(' · '); -} - export function taskRowsFromManifest( manifest: GameCreationAppManifest, ): GameCreationAppTaskState[] { diff --git a/apps/ai-game-creator-shell/src/features/agent-runtime/panels.tsx b/apps/ai-game-creator-shell/src/features/agent-runtime/panels.tsx deleted file mode 100644 index 95b50ccbe..000000000 --- a/apps/ai-game-creator-shell/src/features/agent-runtime/panels.tsx +++ /dev/null @@ -1,1327 +0,0 @@ -import { - ChevronDown, - ChevronUp, - Pause, - Pencil, - Play, - Target, - Trash2, -} from 'lucide-react'; -import { useEffect, useLayoutEffect, useRef, useState } from 'react'; - -import { closeDialogOnEscape } from '../../app/dialogs'; -import type { - AgentGoalRecord, - AgentRuntimeState, - AgentRuntimeUserInputRequest, -} from '../../app/types'; -import type { ProjectAgentResultSummary } from '../../view/project-development'; -import { - agentGoalStatusIsPaused, - agentGoalStatusIsTerminal, - agentRuntimeCanCancel, - agentRuntimeCanConfirm, - agentRuntimeCanRetry, - agentRuntimeNeedsUserInput, - agentRuntimeNextStepFromPhase, - agentRuntimePlanStepText, - agentRuntimeWaitingOnFromPhase, - createAgentRuntimeUserInputResponseId, - formatAgentRecentRuntimeTask, - formatAgentRuntimeDelegationSource, - formatAgentRuntimeEvent, - formatAgentRuntimeLoopProgress, - formatAgentRuntimePlanStep, - formatAgentRuntimeTaskQueue, - formatProjectRuntimeUpdatedAt, - formatProjectSupervisorCompactProgress, - isAgentRuntimeTerminalState, - projectProfessionalAgentLabel, - projectRuntimePlanProgress, - projectRuntimeStatusPresentation, - projectRuntimeVisibleCurrentWork, - projectRuntimeVisibleError, - projectRuntimeVisibleToolSummary, - projectSupervisorCollaboratingAgentRuntimes, - projectSupervisorPendingActionPresentation, - projectSupervisorRuntimeStatusLabel, -} from './model'; - -export function AgentGoalStatusPanel({ - goal, - runtime, - error, - controlBusy = false, - readOnly = false, - onStart, - onEdit, - onPause, - onResume, - onClear, -}: { - goal: AgentGoalRecord | null; - runtime: AgentRuntimeState | null; - error?: string | null; - controlBusy?: boolean; - readOnly?: boolean; - onStart?: () => void; - onEdit?: () => void; - onPause?: () => void; - onResume?: () => void; - onClear?: () => void; -}) { - const runtimeMatchesGoal = Boolean( - runtime?.goalId && (!goal || runtime.goalId === goal.goalId), - ); - const goalId = runtimeMatchesGoal ? runtime?.goalId : goal?.goalId; - const status = - (runtimeMatchesGoal ? runtime?.goalStatus : null) ?? goal?.status ?? null; - const revision = - (runtimeMatchesGoal ? runtime?.goalRevision : undefined) ?? - goal?.revision ?? - 0; - const outcome = - (runtimeMatchesGoal ? runtime?.goalOutcome : null) ?? goal?.outcome ?? ''; - const constraints = - (runtimeMatchesGoal ? runtime?.goalConstraints : undefined) ?? - goal?.constraints ?? - []; - const verification = - (runtimeMatchesGoal ? runtime?.goalVerification : undefined) ?? - goal?.verification ?? - []; - const hasGoal = Boolean(goalId); - const canStart = - Boolean(onStart) && (!hasGoal || agentGoalStatusIsTerminal(status)); - const canEdit = - Boolean(goal && onEdit) && - ['active', 'pause-requested', 'paused'].includes(status ?? ''); - const canPause = Boolean(goal && onPause) && status === 'active'; - const canResume = Boolean(goal && onResume) && status === 'paused'; - const canClear = - Boolean(goal && onClear) && - !['clearing', 'cleared', 'needs-reconciliation'].includes(status ?? ''); - return ( -
-
-
- 持久目标 - - {hasGoal - ? `Status:${status ?? '-'} · Revision:${revision}` - : '尚未开始'} - -
-
- {canStart ? ( - - ) : null} - {canEdit ? ( - - ) : null} - {canPause ? ( - - ) : null} - {canResume ? ( - - ) : null} - {canClear ? ( - - ) : null} -
-
- {hasGoal ? ( -
-

{`Outcome:${outcome || '-'}`}

- {`Verification:${verification.join(';') || '-'}`} - {constraints.length > 0 ? ( - {`Constraints:${constraints.join(';')}`} - ) : null} -
- ) : null} - {goal?.error ? {goal.error} : null} - {error ? {error} : null} -
- ); -} - -export function AgentRuntimeUserInputCard({ - request, - controlBusy, - onSubmit, -}: { - request: AgentRuntimeUserInputRequest; - controlBusy: boolean; - onSubmit?: ( - request: AgentRuntimeUserInputRequest, - responseId: string, - answers: Record, - ) => void | Promise; -}) { - const [answers, setAnswers] = useState>({}); - const [submitting, setSubmitting] = useState(false); - const [responseId] = useState( - () => request.responseId ?? createAgentRuntimeUserInputResponseId(), - ); - const answerPrepared = request.status === 'answer-prepared'; - const allAnswered = request.questions.every((question) => - Boolean(answers[question.id]?.trim()), - ); - const disabled = controlBusy || submitting || answerPrepared; - - return ( -
-
- 需要你的回答 - - {answerPrepared ? '已提交,正在继续' : '回答后继续当前任务'} - -
- {request.questions.map((question, index) => { - const answer = answers[question.id] ?? ''; - return ( -
- {`${index + 1}. ${question.header}`} -

{question.question}

-
- {question.options.map((option) => ( - - ))} -
-