From 5315b233c876868750421a0ca3cb5314324ad71d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 21 Sep 2026 23:09:43 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=84=E8=A7=88=E6=BF=80=E6=B4=BB=E5=9B=9E?= =?UTF-8?q?=E6=8E=A5=E3=80=8C=E8=BF=90=E8=A1=8C=E3=80=8D=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E8=81=8A=E5=A4=A9=20@=20=E6=B8=85=E5=8D=95=E6=89=93?= =?UTF-8?q?=E5=BC=80=E9=80=89=E6=8B=A9=E5=99=A8=E6=97=B6=E6=8C=89=E9=9C=80?= =?UTF-8?q?=E9=87=8D=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx 新增 activateRunningPreview,运行入口先问 Rust 要活体预览(activate_local_game_preview),命中就只切客户端运行视图 - App.tsx 的 executeRunLocal 命中活体预览时经 DirectProject 聊天的 announce 播报「已切换到客户端运行视图」,不再重复调用 start_local_game_preview;stopped / 非本项目 / 权限位要求确认一律回落到重新启动 - check-config.mjs 从 native-only 白名单移除 activate_local_game_preview 并同步注释(该门禁要求 App invoke 与白名单互斥) - DirectProjectComposer 新增 onReferencePickerOpen,工具条按钮与附件菜单两处选择器入口打开前先刷新清单 - DirectProjectChatView 用 useDirectProjectManifest 的 refresh 重读聊天自己那份 @ 清单,不新增壳到聊天的清单推送通道 - 新增 tests/previewActivation.test.tsx,钉住切视图不重启 / stopped 回落重启 / 权限确认回落重启三条路径 - decision-log 记录本轮两处回归的背景、决策、取舍与验证方式 --- .../scripts/check-config.mjs | 4 +- apps/ai-game-creator-shell/src/App.tsx | 47 ++++++ .../chat/DirectProjectChatView.tsx | 5 + .../DirectProjectComposer.tsx | 19 ++- .../tests/previewActivation.test.tsx | 153 ++++++++++++++++++ .../shared-memory/decision-log.md | 8 + 6 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 apps/ai-game-creator-shell/tests/previewActivation.test.tsx diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index 567fcb258..eaba4e582 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -200,11 +200,11 @@ const allowedUncalledTauriCommands = [ 'read_agc_plugin_panel', 'set_agc_plugin_enabled', // 下面这些命令的调用方只有随 Project Supervisor 前端链路一起删除的旧命令聊天入口; - // 现在 App 前端、工作台与策划聊天都没有接线(检查点 / 恢复 / 索引 / 导出包 / 预览 / + // 现在 App 前端、工作台与策划聊天都没有接线(检查点 / 恢复 / 索引 / 导出包 / // 画板同步 / 素材登记 / 权限策略 / 本地草案 / 平台美术),Rust 侧只剩注册与实现, // `*_at` helper 仍由 Rust 用例覆盖。接回新入口还是删除属于 native 能力取舍,先按 // native-only 登记,避免孤儿检查一直报错。 - 'activate_local_game_preview', + // 预览不在本清单:`activate_local_game_preview` 已按 ADR 回接到 App 的「运行」入口。 'build_local_project_index', 'control_agent_run', 'create_local_project_checkpoint', diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index f9d9e29fb..7c47aeb5e 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -39,6 +39,7 @@ import type { LocalConversationResult, LocalGameProjectRevisionStatus, LocalPreviewResult, + LocalPreviewStatus, LocalProjectFileResult, LocalProjectKind, PendingUiConfirmation, @@ -1717,6 +1718,39 @@ export function App({ } } + /** + * 预览已经在跑时先切视图,不重启预览服务。 + * + * `activate_local_game_preview` 是 Rust 侧的「这条预览还活着、且属于这个项目」闸门: + * 它只核对内存 registry 里的状态并回传可用的 loopback 地址,前端据此进客户端运行视图。 + * 同一个动作过去由工作台壳的 `/preview open` 聊天命令承担,那条命令链随 Supervisor + * 前端链路一起退役,行为改由「运行」入口承接,结果经 DirectProject 聊天的 `announce` + * 交给聊天自己的消息流。返回 `null` 表示没有可复用的活体预览,调用方照旧重启预览。 + */ + async function activateRunningPreview( + invoke: TauriInvoke, + nextProjectPath: string, + ): Promise { + try { + const status = await invoke( + 'activate_local_game_preview', + { projectPath: nextProjectPath }, + ); + if ( + status.status !== 'running' || + !status.url || + !status.port || + !status.root + ) { + return null; + } + return { url: status.url, port: status.port, root: status.root }; + } catch { + // 没在跑、不属于这个项目,或权限位要求确认:都不是错误,回落到重新启动预览。 + return null; + } + } + async function executeRunLocal(announceToChat: boolean) { const invoke = resolveTauriInvoke(); if (!invoke) { @@ -1734,6 +1768,19 @@ export function App({ } try { + 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 }, diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx b/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx index 53209e796..839b37053 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx @@ -231,6 +231,11 @@ export function DirectProjectChatView({ turnCancelling={turnCancelling} onCancelQueuedTurn={cancelQueuedTurn} onCancelTurn={() => void cancelTurn()} + onReferencePickerOpen={() => { + // 素材标签、改名、删除这些写入发生在资源画布,聊天侧收不到失效事件: + // 打开 `@` 选择器时按需重读聊天自己那份清单,别把旧标签当现状。 + if (projectPath) void refresh(projectPath); + }} onRemoveAttachment={removeAttachment} onSubmit={submit} onUploadFiles={(files) => void uploadFiles(files)} diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx index 254c6c1e2..62c89b163 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx @@ -44,6 +44,7 @@ export function DirectProjectComposer({ turnCancelling, onCancelQueuedTurn, onCancelTurn, + onReferencePickerOpen, onRemoveAttachment, onSubmit, onUploadFiles, @@ -59,6 +60,14 @@ export function DirectProjectComposer({ turnCancelling: boolean; onCancelQueuedTurn: (id: string) => void; onCancelTurn: () => void; + /** + * 打开 `@` 选择器前刷新一次项目清单。 + * + * `@` 的标签统计与候选来自聊天自己订阅的那份清单(`useDirectProjectManifest`), + * 而素材标签、改名、删除这些写入走的是资源画布的通道,聊天侧收不到任何信号; + * 用户真正要看这份清单的时刻是打开选择器的这一刻,所以在这里补一次重读。 + */ + onReferencePickerOpen?: () => void; onRemoveAttachment: (index: number) => void; onSubmit: (content: DirectCodexUserContentPart[]) => boolean; onUploadFiles: (files: readonly File[]) => void; @@ -139,14 +148,20 @@ export function DirectProjectComposer({ aria-label="插入素材引用" title="插入素材引用" disabled={busy} - onClick={() => composerRef.current?.openPicker()} + onClick={() => { + onReferencePickerOpen?.(); + composerRef.current?.openPicker(); + }} >