预览激活回接「运行」入口,聊天 @ 清单打开选择器时按需重读
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m49s
Project CI / Backend tests (pull_request) Successful in 5m7s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m7s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m15s
Project CI / Native shell tests (pull_request) Successful in 6m37s
Project CI / Frontend tests (pull_request) Successful in 3m8s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 1m56s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m49s
Project CI / Backend tests (pull_request) Successful in 5m7s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m7s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m15s
Project CI / Native shell tests (pull_request) Successful in 6m37s
Project CI / Frontend tests (pull_request) Successful in 3m8s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 1m56s
- 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 记录本轮两处回归的背景、决策、取舍与验证方式
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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<LocalPreviewResult | null> {
|
||||
try {
|
||||
const status = await invoke<LocalPreviewStatus>(
|
||||
'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<LocalPreviewResult>(
|
||||
'start_local_game_preview',
|
||||
{ projectPath: nextProjectPath },
|
||||
|
||||
@@ -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)}
|
||||
|
||||
+17
-2
@@ -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();
|
||||
}}
|
||||
>
|
||||
<AtSign size={15} aria-hidden="true" />
|
||||
</button>
|
||||
<ComposerAttachmentMenu
|
||||
disabled={busy || modelValidating}
|
||||
onPickFiles={(files) => void onUploadFiles(files)}
|
||||
onOpenReferencePicker={() => composerRef.current?.openPicker()}
|
||||
onOpenReferencePicker={() => {
|
||||
onReferencePickerOpen?.();
|
||||
composerRef.current?.openPicker();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="project-chat-composer-controls-right">
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
/**
|
||||
* 运行入口「预览已经在跑就切视图」的回归用例。
|
||||
*
|
||||
* 背景:工作台壳过去用 `/preview open` 聊天命令切到已经在跑的客户端运行视图,那条命令链
|
||||
* 随 Project Supervisor 前端链路整体退役;按 ADR(`docs/adr/【ADR】DirectProject独立聊天
|
||||
* 容器与工作台钱包布局-2026-09-18.md`),这个动作由 DirectProject 的「运行」入口承接,
|
||||
* 结果经聊天自己的 `announce` 交给聊天的本地消息流。
|
||||
*
|
||||
* 因此运行入口进入客户端运行视图前先问 Rust 要活体预览:`activate_local_game_preview`
|
||||
* 只核对内存 registry 并回传 loopback 地址,活着就复用(不重启预览服务),没活着
|
||||
* (stopped / 不属于这个项目 / 权限位要求确认)才回落到 `start_local_game_preview`。
|
||||
*
|
||||
* 这条行为过去只剩 `scripts/check-native-shells.mjs` 的源码守卫与 Rust 实现,前端没有
|
||||
* 用例;一旦有人再把它当死链路删掉,守卫会先报「missing await invoke」而不知道现场。
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, it, vi } from 'vitest';
|
||||
|
||||
import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import {
|
||||
App,
|
||||
createGameCreationAppManifest,
|
||||
createProjectChatRuntimeHarness,
|
||||
expect,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from './appSurface/harness';
|
||||
|
||||
const PROJECT_PATH = '/tmp/preview-activation-project';
|
||||
const PROJECT_ID = 'preview-activation-project';
|
||||
const PREVIEW_URL = 'http://127.0.0.1:43210/game/index.html';
|
||||
|
||||
function createFixtureManifest(): GameCreationAppManifest {
|
||||
return createGameCreationAppManifest(PROJECT_ID, '预览切换项目');
|
||||
}
|
||||
|
||||
/**
|
||||
* `activate_local_game_preview` 的替身由用例给定:它决定「这条预览还活着吗」,
|
||||
* 其余命令沿用聊天 harness,运行入口之外的链路保持真实形状。
|
||||
*/
|
||||
function installTauri(activateLocalGamePreview: () => unknown) {
|
||||
const manifest = createFixtureManifest();
|
||||
const chatHarness = createProjectChatRuntimeHarness({
|
||||
projectPath: PROJECT_PATH,
|
||||
});
|
||||
const invokeCounts = new Map<string, number>();
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
invokeCounts.set(command, (invokeCounts.get(command) ?? 0) + 1);
|
||||
if (command === 'get_local_game_manifest') {
|
||||
return manifest;
|
||||
}
|
||||
if (command === 'get_local_game_project_revision') {
|
||||
return { revision: 3 };
|
||||
}
|
||||
if (command === 'activate_local_game_preview') {
|
||||
return activateLocalGamePreview();
|
||||
}
|
||||
if (command === 'start_local_game_preview') {
|
||||
return { url: PREVIEW_URL, port: 43210, root: PROJECT_PATH };
|
||||
}
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: invoke as never },
|
||||
event: { listen: chatHarness.listen as never },
|
||||
};
|
||||
return { invoke, invokeCounts };
|
||||
}
|
||||
|
||||
/** 「运行」入口由工作台壳的播放请求驱动,这里直接用同一形状的请求触发。 */
|
||||
function renderRunningProjectChat() {
|
||||
return render(
|
||||
<App
|
||||
initialProjectPath={PROJECT_PATH}
|
||||
initialProjectManifest={createFixtureManifest()}
|
||||
onPlayRequestHandled={vi.fn()}
|
||||
playRequest={{ projectPath: PROJECT_PATH, requestId: 1 }}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
/** 运行结果由聊天自己的本地消息流展示,这里按对话容器文本断言。 */
|
||||
async function expectChatAnnouncement(text: string) {
|
||||
const surface = await screen.findByLabelText('陶泥儿项目对话');
|
||||
await waitFor(() => expect(surface.textContent ?? '').toContain(text));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
window.history.pushState({}, '', '/');
|
||||
});
|
||||
|
||||
describe('运行入口切到已经在跑的客户端预览', () => {
|
||||
it('预览还活着时只切视图,不重启预览服务', async () => {
|
||||
const { invoke, invokeCounts } = installTauri(() => ({
|
||||
status: 'running',
|
||||
url: PREVIEW_URL,
|
||||
port: 43210,
|
||||
root: PROJECT_PATH,
|
||||
}));
|
||||
|
||||
renderRunningProjectChat();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', {
|
||||
projectPath: PROJECT_PATH,
|
||||
}),
|
||||
);
|
||||
await expectChatAnnouncement(`已切换到客户端运行视图:${PREVIEW_URL}`);
|
||||
expect(invokeCounts.get('start_local_game_preview') ?? 0).toBe(0);
|
||||
});
|
||||
|
||||
it('预览不在跑时回落到重新启动预览', async () => {
|
||||
const { invokeCounts } = installTauri(() => ({
|
||||
status: 'stopped',
|
||||
url: null,
|
||||
port: null,
|
||||
root: null,
|
||||
}));
|
||||
|
||||
renderRunningProjectChat();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(invokeCounts.get('start_local_game_preview') ?? 0).toBeGreaterThan(
|
||||
0,
|
||||
),
|
||||
);
|
||||
await expectChatAnnouncement(
|
||||
`运行通过,已载入客户端运行视图:${PREVIEW_URL}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('权限位要求确认时不把拒绝当错误,直接回落到重新启动预览', async () => {
|
||||
const { invokeCounts } = installTauri(() => {
|
||||
throw new Error('需要先确认预览权限');
|
||||
});
|
||||
|
||||
renderRunningProjectChat();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(invokeCounts.get('start_local_game_preview') ?? 0).toBeGreaterThan(
|
||||
0,
|
||||
),
|
||||
);
|
||||
await expectChatAnnouncement(
|
||||
`运行通过,已载入客户端运行视图:${PREVIEW_URL}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,14 @@
|
||||
- 代价与取舍:master 用例里钉住 `.project-supervisor-composer-controls`、`项目总控消息` 的断言改为当前类名 `.project-chat-composer-controls` 与「立项策划消息」;本分支 CSS 的 `project-supervisor-* → project-chat-*` 改名对 master 新增规则同样生效。`workspaceProjectKind` 在两条读它的路径(壳层 Cocos 插件门禁、Supervisor 提交路由)都退役后删除,插件可用性改由插件宿主自判。`.env` 的本地私有改动不进入本次合并。
|
||||
- 验证方式:`apps/ai-game-creator-shell` `npm run typecheck` 与仓库 `npm run typecheck`、`npx vitest run apps/ai-game-creator-shell/tests`(168 个文件,除 5 个 jsdom `localStorage` 环境失败文件与 1 条本分支既有的 `resourceTagStatsRefresh` 失败外全绿,`appSurface.test.ts` 198 passed / 13 skipped / 0 failed)、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`、改动文件 eslint 0 error。Rust 侧定向 `cargo test --bin genarrative-ai-game-creator-shell -- direct_codex_user_item | skill_pack:: | sessions::developer_project_file_and_memory_writes_advance_project_revision` 全过;整套 Rust 分片在本容器有 55 条环境性失败(`/sbin -> usr/bin` 让 `command.exec` 沙箱的 merged-usr 预检失败,报 `command.exec sandbox unavailable`),与本次合并无关:这些用例所在模块与 master 逐字相同。
|
||||
|
||||
## 2026-09-21 合并后两处回归:预览激活回接「运行」入口,聊天 `@` 清单按需重读
|
||||
|
||||
- 背景:合并 `origin/master` 后 CI 报两处回归。① `scripts/check-native-shells.mjs` 报 `AI game creator client preview activation drifted: missing await invoke<LocalPreviewStatus>('activate_local_game_preview'`:Supervisor 前端链路退役时(`0224ef7b9` 删 `pendingCommand` 死链那一步)把 `activate_local_game_preview` 的前端调用方一起带走,而这条守卫与 Rust `preview.rs` 的实现都还在。② `resourceTagStatsRefresh` 用例失败:聊天 `@` 选择器的标签统计与候选读 `useDirectProjectManifest` 自己那份清单,资源画布的标签写入既不发 `game-creator-manifest-invalidated`(Rust 只在 agent 驱动的写入上发),壳也不再往聊天推清单快照,于是用户改完标签打开 `@` 仍看到旧标签。
|
||||
- 决策:① 预览激活按 ADR 由「运行」入口承接:`executeRunLocal` 先调 `activate_local_game_preview`(Rust 侧只核对内存 registry 与项目归属),返回 running 且拿到 loopback 地址就只切客户端运行视图并经 `DirectProjectChatHandle.announce` 说一句 `已切换到客户端运行视图:<url>`,不再重复 `start_local_game_preview`;stopped / 不属于本项目 / 权限位要求确认都当「没有可复用预览」,回落到重新启动。同时把 `activate_local_game_preview` 从 `scripts/check-config.mjs` 的 native-only 白名单删除(该门禁要求 App invoke 与白名单互斥)。② 聊天 `@` 选择器打开时按需重读清单:`DirectProjectComposer` 的两处选择器入口(工具条按钮与附件菜单项)都先触发 `onReferencePickerOpen` 再 `openPicker`,由 `DirectProjectChatView` 用 `useDirectProjectManifest` 的 `refresh` 重读;不新增壳→聊天的清单推送通道,也不改 ADR 里「聊天自己订阅清单」的归属。
|
||||
- 代价与取舍:编辑器内直接敲 `@` 的候选菜单读同一份清单,本轮没有纳入按需重读(它只列素材名、不显示标签统计)。更根治的做法是让资源写入在 Rust 侧广播 `game-creator-manifest-invalidated`,本轮不做,先按现成的刷新入口收口。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src/App.tsx`、`src/view/project-development/chat/DirectProjectChatView.tsx`、`.../chat/components/DirectProjectComposer/DirectProjectComposer.tsx`、`scripts/check-config.mjs`;新增 `tests/previewActivation.test.tsx`(3 条:活体预览只切视图不重启、stopped 回落重启、权限确认回落重启;第一条在去掉接线后确实失败)。
|
||||
- 验证方式:`npx vitest run apps/ai-game-creator-shell/tests`(除 5 个 jsdom `localStorage` 环境失败文件外全绿:`previewActivation` 3/3、`resourceTagStatsRefresh` 2/2、`runVersionSwitchEventSubscription` 2/2、`resourceReferenceInput` 34/34、`appSurface.test.ts` 198 passed / 13 skipped)、`node scripts/check-native-shells.mjs --groups=contract`、`npm run ai-game-creator-shell:typecheck`、`npm run check:encoding`、`git diff --check`、改动文件 prettier / eslint 全过。
|
||||
|
||||
## 2026-09-21 渠道进安装身份:不同渠道的 AGC 包体在同一台设备并存
|
||||
|
||||
- 背景:渠道此前只决定更新端点(`plugins.updater.endpoints`)与渲染层平台 origin,`productName` / `identifier` 与渠道无关,于是所有渠道共用 `%LOCALAPPDATA%\陶泥儿` 安装目录、同一个卸载项(`HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\陶泥儿`,另有 `HKCU\Software\genarrative\陶泥儿`)以及同一份 `%APPDATA%\world.genarrative.ai-game-creator` 数据目录。本机 0.1.48 安装实测:主程序二进制里只有 1 处 `agc/dev-win/latest.json`、0 处 release 端点,说明渠道在产物里只体现为端点。后果是后装的渠道静默顶掉先装的渠道,并接管更新端点、平台服务器与本地登录态/项目数据。
|
||||
|
||||
Reference in New Issue
Block a user