diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 6ea787b23..03e1de93d 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -562,7 +562,6 @@ export function App({ !supervisorChatOnly && !planningStartMode && !designAgentActive; - const planningLaneActive = designAgentActive; const [devMode] = useState(() => projectSupervisorOnly ? false : isDeveloperMode(), ); @@ -1641,7 +1640,7 @@ export function App({ if ( !canSubscribeTauriEvents() || directCodexProductRuntime || - planningLaneActive + designAgentActive ) { return; } @@ -1744,7 +1743,7 @@ export function App({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [ directCodexProductRuntime, - planningLaneActive, + designAgentActive, refreshManifest, updateProjectSupervisorResponseStream, updateProjectSupervisorRuntime, @@ -1869,7 +1868,7 @@ export function App({ if ( !invoke || directCodexProductRuntime || - planningLaneActive || + designAgentActive || !nextProjectPath || !sessionId || !trackedRunId || @@ -1965,7 +1964,7 @@ export function App({ }, [ localProject?.projectPath, directCodexProductRuntime, - planningLaneActive, + designAgentActive, projectSupervisorSessionId, projectSupervisorRuntime?.phase, projectSupervisorRuntime?.runId, @@ -1980,7 +1979,7 @@ export function App({ if ( !projectSupervisorOnly || directCodexProductRuntime || - planningLaneActive || + designAgentActive || !invoke || !nextProjectPath || !supervisorRunId @@ -2061,7 +2060,7 @@ export function App({ }, [ localProject?.projectPath, directCodexProductRuntime, - planningLaneActive, + designAgentActive, projectSupervisorOnly, projectSupervisorRuntime?.phase, projectSupervisorRuntime?.runId, @@ -2073,7 +2072,7 @@ export function App({ (!supervisorChatOnly && !( projectSupervisorOnly && - (directCodexProductRuntime || planningLaneActive) + (directCodexProductRuntime || designAgentActive) )) || !supervisorChatShouldFollowLatestRef.current ) { @@ -2090,7 +2089,7 @@ export function App({ projectSupervisorRuntimeError, directThreadChat, directCodexProductRuntime, - planningLaneActive, + designAgentActive, projectSupervisorOnly, supervisorChatOnly, ]); 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 e2f6291ce..2b56a2a58 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 @@ -2052,13 +2052,6 @@ export function projectRuntimeVisibleError( ) { return `${subject} 保存运行记录失败,请检查项目目录后重试`; } - if ( - normalized.includes('planning_invalid') || - normalized.includes('gdd 结构无效') || - normalized.includes('策划输出格式') - ) { - return `${subject} 输出格式不符合当前 GDD 结构,请重试`; - } const containsInternalDiagnostics = normalized.includes('agentllm.') || /(?:^|[\s::])kind=/.test(normalized) || diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 7b02c4c97..1c52fb1f4 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -2846,7 +2846,7 @@ export default function ProjectDevelopmentView({ [activeVersionId, manifest], ); // 做方案在拿到第一个产物之前,左侧的资源区域是空的(文档 0 项、项目版本 0 项),而澄清 - // 问答、决定卡和 GDD 审批全挤在右边那一列里。这段时间先把画布收掉让对话铺满,一旦有第 + // 问答和阶段审批都在右侧对话列中。这段时间先把画布收掉让对话铺满,一旦有第 // 一个已登记资源就自动恢复双栏。用 CSS 收而不是不渲染:画布里的页签、缩放和选中状态都 // 留着,恢复双栏时不会被重置。 const conversationOnlyWorkbench = planningStartMode && resources.length === 0; diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 0b229f8d9..6bbcca23f 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -19,6 +19,10 @@ import { normalizeProjectResourceGraph } from '../../src/view/project-developmen import { ResourceDependencyOverlay } from '../../src/view/project-development/ResourceDependencyOverlay'; import type { ProjectAgentResultSummary } from '../../src/view/project-development/resourceProjectionModel'; import { projectResourcesFromReadModels } from '../../src/view/project-development/resourceProjectionModel'; +import { + generationPromptText, + typeGenerationPrompt, +} from '../resourceGenerationPromptTestUtils'; import { act, agentRuntimeUserInputRequest, @@ -345,9 +349,9 @@ async function submitBottomToolbarPanel( fireEvent.click(screen.getByRole('button', { name: label })); } const panel = await screen.findByRole('dialog', { name: label }); - fireEvent.change(within(panel).getByLabelText('生成提示词'), { - target: { value: options.prompt }, - }); + // 提示词输入区是与聊天同一份 `@` 引用输入区(Lexical):jsdom 没有可用 Selection, + // 浏览器输入事件不会落字,只能走编辑器更新(见 resourceGenerationPromptTestUtils)。 + await typeGenerationPrompt(panel, options.prompt); fireEvent.click(within(panel).getByRole('button', { name: label })); // 成功路径由宿主卸载面板:等它消失,下一个入口才不会撞上残留的浮层。 await waitFor(() => @@ -2978,7 +2982,17 @@ export function registerProjectWorkbenchFoundationTests() { await showResourcePage('角色与对象'); let heroDetailButton = await findResourceSelectButton('hero.png'); const heroCard = heroDetailButton.closest('.game-resource-card'); - expect(heroCard?.textContent).not.toContain('hero.png'); + // 卡面名称就是正式资源名(manifest `localPath` 的文件名,生成时来自 assetName、 + // 重命名时同步改写),长名由 CSS 省略、完整名挂在实际命中指针的整卡选中按钮 `title` 上。 + const heroName = heroCard?.querySelector('.game-resource-card-name'); + expect(heroName?.textContent).toBe('hero.png'); + expect(heroName?.getAttribute('data-resource-name')).toBe('hero.png'); + expect( + heroCard + ?.querySelector('.game-resource-card-select') + ?.getAttribute('title'), + ).toBe('hero.png'); + // 卡面仍然只给"名称 + 类型",不铺完整路径与来源这类详细文本。 expect(heroCard?.textContent).not.toContain('assets/hero.png'); expect(heroCard?.textContent).not.toContain('Agent 生成'); // 角标显示的是**资源类型**(功能分类),等于它所在的画布栏目;不再是"图片"这类媒体类型。 @@ -3002,13 +3016,21 @@ export function registerProjectWorkbenchFoundationTests() { }); await showResourcePage('待归类'); act(() => observer.triggerVisible()); - const documentSummary = await screen.findByText(/这是安全的卡片正文摘要。/); - // 卡面正文按扩展名走文档分支(媒体类型),但角标是资源类型:`design-document` 不在 - // canonical 目录里 → 落「待归类」,与它所在的栏目一致(这正是改前"标着文档、却在待归类栏"的分叉)。 + // 文档卡卡面**不铺正文**:只显示居中图标 + 正式资源名,正文只在独立的文档详情浮层里读。 + const documentCard = ( + await findResourceSelectButton('design.md') + ).closest('.game-resource-card'); expect( - documentSummary - .closest('.game-resource-card') - ?.querySelector('[data-resource-type="待归类"]'), + documentCard?.querySelector('.game-resource-card-name')?.textContent, + ).toBe('design.md'); + expect( + documentCard?.querySelector('.game-resource-card-document-visual'), + ).not.toBeNull(); + expect(documentCard?.textContent).not.toContain('这是安全的卡片正文摘要。'); + // 角标仍是资源类型:`design-document` 不在 canonical 目录里 → 落「待归类」, + // 与它所在的栏目一致(这正是改前"标着文档、却在待归类栏"的分叉)。 + expect( + documentCard?.querySelector('[data-resource-type="待归类"]'), ).not.toBeNull(); await showResourcePage('角色与对象'); expect( @@ -4249,9 +4271,9 @@ export function registerProjectWorkbenchFoundationTests() { expect(screen.queryByRole('toolbar', { name: '图片工具栏' })).toBeNull(); }); expect( - screen - .queryAllByTitle('选中资源') - .some((card) => card.getAttribute('aria-pressed') === 'true'), + Array.from( + document.querySelectorAll('.game-resource-card-select'), + ).some((card) => card.getAttribute('aria-pressed') === 'true'), ).toBe(false); }); @@ -12212,9 +12234,7 @@ export function registerProjectAgentStatusTests() { fireEvent.change(within(firstPanel).getByLabelText('素材名称'), { target: { value: '第一条设计图' }, }); - fireEvent.change(within(firstPanel).getByLabelText('生成提示词'), { - target: { value: '第一条界面' }, - }); + await typeGenerationPrompt(firstPanel, '第一条界面'); fireEvent.click( within(firstPanel).getByRole('button', { name: '生成 UI 设计图' }), ); @@ -12225,8 +12245,15 @@ export function registerProjectAgentStatusTests() { // 2) 面板关掉之后任务仍在「生成任务」面板里,阶段文案来自后端记录。 const taskPanel = await screen.findByRole('region', { name: '生成任务' }); expect(within(taskPanel).getByText('第一条设计图')).not.toBeNull(); - expect(within(taskPanel).getByText('正在生成。')).not.toBeNull(); - expect(within(taskPanel).getByText('生成中')).not.toBeNull(); + /** + * 阶段文案与徽标要**等**后端记录并进来:刚提交时任务还停在本地队列的「排队中。」, + * 记录合并发生在第一轮账本轮询(间隔 2s)之后。同步断言在这里读到的会是本地阶段, + * 在整套连跑时必然抢跑(隔离单跑时那一拍刚好落在窗口内,所以只在全量里红)。 + */ + await waitFor(() => { + expect(within(taskPanel).getByText('正在生成。')).not.toBeNull(); + expect(within(taskPanel).getByText('生成中')).not.toBeNull(); + }); // 非模态:没有全屏遮罩、没有 aria-modal。 expect(taskPanel.getAttribute('aria-modal')).toBeNull(); expect(document.querySelector('[aria-modal="true"]')).toBeNull(); @@ -12239,9 +12266,7 @@ export function registerProjectAgentStatusTests() { fireEvent.change(within(secondPanel).getByLabelText('素材名称'), { target: { value: '第二条设计图' }, }); - fireEvent.change(within(secondPanel).getByLabelText('生成提示词'), { - target: { value: '第二条界面' }, - }); + await typeGenerationPrompt(secondPanel, '第二条界面'); fireEvent.click( within(secondPanel).getByRole('button', { name: '生成 UI 设计图' }), ); @@ -12286,6 +12311,28 @@ export function registerProjectAgentStatusTests() { expect( calls.filter((call) => call.command === 'get_local_game_manifest').length, ).toBeGreaterThanOrEqual(manifestReadsBefore + 2); + + /** + * 5) 等轮询真的退出:两个 mock 任务都已收口 completed,再连等两个 poll 周期 + * (间隔 2s),账本读计数不再增长才算这条异步链跑完。 + * + * 宿主读账本用的是「调用时」的 `window.__TAURI__`,而 afterEach 会把它删掉、下一个用例 + * 再装自己的 mock:留下一条在途/在睡的轮询,下一轮醒来就会打进**下一个用例**的 spy + * (实测就是 `does not open a preview before a local project is initialized` 那条变红, + * 且看到的 projectPath 是本案的)。所以排空必须发生在本用例内部,而不是靠全局 harness 掩盖。 + */ + const ledgerReads = () => + calls.filter( + (call) => call.command === 'list_local_project_asset_generations', + ).length; + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 2_200)); + }); + const ledgerReadsAfterFirstQuietPeriod = ledgerReads(); + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 2_200)); + }); + expect(ledgerReads()).toBe(ledgerReadsAfterFirstQuietPeriod); }, 30_000); /** @@ -12799,9 +12846,7 @@ export function registerProjectAgentStatusTests() { fireEvent.change(within(panel).getByLabelText('素材名称'), { target: { value: '待提交设计图' }, }); - fireEvent.change(within(panel).getByLabelText('生成提示词'), { - target: { value: '主界面与背包页' }, - }); + await typeGenerationPrompt(panel, '主界面与背包页'); return panel; } @@ -12848,10 +12893,9 @@ export function registerProjectAgentStatusTests() { '项目权限策略拒绝执行:canvas.asset_generate', ), ); - expect( - (within(reopened).getByLabelText('生成提示词') as HTMLTextAreaElement) - .value, - ).toBe('主界面与背包页'); + await waitFor(() => + expect(generationPromptText(reopened)).toBe('主界面与背包页'), + ); expect( (within(reopened).getByLabelText('素材名称') as HTMLInputElement).value, ).toBe('待提交设计图');