diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 1e7936569..ba86fce33 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -1818,7 +1818,6 @@ textarea { line-height: 1.55; } - .supervisor-chat-only-message-list .message--user { align-self: flex-end; border-color: #cfd6df; @@ -1923,7 +1922,6 @@ textarea { opacity: 0.55; } - .local-game-preview-frame { position: relative; width: 100%; @@ -1946,7 +1944,6 @@ textarea { transform-origin: center; } - @media (max-width: 600px) { .supervisor-chat-only-header { padding: 0 12px; @@ -1970,7 +1967,6 @@ textarea { } } - .project-supervisor-surface { display: grid; grid-template-columns: minmax(0, 1fr) minmax(220px, 280px); @@ -6082,9 +6078,7 @@ iframe.preview-frame { max-width: none; } -.game-workbench-chat - .supervisor-chat-only-runtime-controls - .pending-command { +.game-workbench-chat .supervisor-chat-only-runtime-controls .pending-command { gap: 12px; padding: 10px 12px; border: 1px solid var(--platform-surface-border); @@ -6149,7 +6143,9 @@ iframe.preview-frame { line-height: 1.4; } -.agent-runtime-status .project-runtime-pending-command .pending-command-actions { +.agent-runtime-status + .project-runtime-pending-command + .pending-command-actions { display: flex; align-items: center; gap: 8px; @@ -6165,9 +6161,7 @@ iframe.preview-frame { color: var(--platform-button-primary-text); } -.game-workbench-chat - .agent-runtime-status - .project-runtime-pending-command { +.game-workbench-chat .agent-runtime-status .project-runtime-pending-command { grid-template-columns: minmax(0, 1fr) 136px; } @@ -6206,8 +6200,12 @@ iframe.preview-frame { background: var(--platform-warm-bg); } -.game-workbench-chat .project-supervisor-surface > .project-runtime-pending-command, -.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command { +.game-workbench-chat + .project-supervisor-surface + > .project-runtime-pending-command, +.game-workbench-chat + .project-supervisor-conversation + .project-runtime-pending-command { display: grid; grid-template-columns: minmax(0, 1fr) 136px; align-items: center; @@ -6220,7 +6218,10 @@ iframe.preview-frame { background: var(--platform-warm-bg); } -.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command > span { +.game-workbench-chat + .project-supervisor-conversation + .project-runtime-pending-command + > span { min-width: 0; margin: 0; overflow-wrap: anywhere; @@ -6229,7 +6230,11 @@ iframe.preview-frame { font-weight: 800; } -.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command > span small { +.game-workbench-chat + .project-supervisor-conversation + .project-runtime-pending-command + > span + small { display: block; margin-top: 4px; color: var(--platform-text-soft); @@ -6238,7 +6243,10 @@ iframe.preview-frame { line-height: 1.4; } -.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions { +.game-workbench-chat + .project-supervisor-conversation + .project-runtime-pending-command + .pending-command-actions { display: grid; grid-template-columns: repeat(2, 64px); gap: 8px; @@ -6246,7 +6254,11 @@ iframe.preview-frame { flex: 0 0 136px; } -.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions button { +.game-workbench-chat + .project-supervisor-conversation + .project-runtime-pending-command + .pending-command-actions + button { width: 64px; min-width: 64px; max-width: 64px; @@ -6261,7 +6273,11 @@ iframe.preview-frame { white-space: nowrap; } -.game-workbench-chat .project-supervisor-conversation .project-runtime-pending-command .pending-command-actions button:last-child { +.game-workbench-chat + .project-supervisor-conversation + .project-runtime-pending-command + .pending-command-actions + button:last-child { border-color: var(--platform-button-primary-border); background: var(--platform-button-primary-fill); color: var(--platform-button-primary-text); @@ -7209,3 +7225,399 @@ iframe.preview-frame { grid-column: 1 / -1; } } + +/* ============================================================ + 立项策划前端样式(PlanGddSurface / 阶段进度 / GDD 审批卡 / 交付行 / + planning-lane-runtime-strip)。 + 这一段曾在 #193「Codex/agent chat layout fix」重写聊天样式时被整体误删: + 组件(TSX)原样保留、选择器全数消失,策划界面裸奔、正文弹层失去 fixed 定位 + 变成内联平铺。从 5802048c3^ 原样恢复。聊天区样式重构时请勿顺手清理本段—— + 这些选择器的使用方在 features/project-workspace/GddApprovalCard.tsx 与 + PlanningLaneRuntimeStrip.tsx。 + ============================================================ */ + +/* 策划区是一个面:阶段进度是标题栏,审批卡是正文。边框和圆角只画在外壳上,里面两块 + 不再各自带框。 */ +.plan-gdd-surface { + display: grid; + min-width: 0; + border: 1px solid #cfd7e6; + border-radius: 10px; + background: #fff; + overflow: hidden; +} + +.gdd-approval-card { + display: grid; + gap: 14px; + padding: 16px 18px 18px; + background: #f8fbff; +} + +.plan-gdd-surface--with-card .plan-gdd-stage-progress { + border-bottom: 1px solid #dbe4f1; +} + +.plan-gdd-stage-progress { + display: grid; + gap: 6px; + padding: 10px 12px; + background: #fff; + color: #526173; + font-size: 12px; +} + +.plan-gdd-stage-progress__header, +.plan-gdd-stage-progress__meta { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 8px 14px; +} + +/* 批准后的交付行。它是策划阶段唯一的产物出口,所以给一条分隔线把它和上面的状态 + 区分开,而不是混成第三行元信息。 */ +.plan-gdd-stage-progress__delivery { + display: grid; + gap: 8px; + margin-top: 4px; + padding-top: 9px; + border-top: 1px solid #e6ecf5; +} + +.plan-gdd-stage-progress__delivery code { + min-width: 0; + color: #3c4a5c; + font-size: 11px; + overflow-wrap: anywhere; +} + +.plan-gdd-stage-progress__delivery-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.plan-gdd-stage-progress__delivery-actions button { + min-height: 30px; + padding: 0 12px; + border: 1px solid #cfd7e6; + border-radius: 6px; + color: #27364a; + background: #fff; + font-size: 12px; +} + +.plan-gdd-stage-progress__delivery-actions button:hover:not(:disabled), +.plan-gdd-stage-progress__delivery-actions button:focus-visible { + border-color: #1f6feb; + color: #1f6feb; +} + +.plan-gdd-stage-progress__delivery-actions button:disabled { + opacity: 0.58; +} + +.plan-gdd-stage-progress__delivery-error { + color: #b42323; + overflow-wrap: anywhere; +} + +.plan-gdd-stage-progress__header strong { + color: #27364a; +} + +.plan-gdd-stage-progress__header span { + padding: 3px 8px; + border-radius: 999px; + background: #eef6ff; + color: #1f6feb; +} + +.gdd-approval-card__header { + display: grid; + gap: 5px; + min-width: 0; +} + +.gdd-approval-card h2, +.gdd-approval-card h3, +.gdd-approval-card p { + margin: 0; +} + +.gdd-approval-card h2 { + font-size: 18px; +} + +.gdd-approval-card__header p { + color: #526173; + overflow-wrap: anywhere; +} + +.gdd-approval-card__details-trace { + color: #6b7a8c; + font-size: 11px; + overflow-wrap: anywhere; +} + +.gdd-approval-card__details-trigger { + justify-self: start; + min-height: 30px; + padding: 0 10px; + border: 1px solid #cfd7e6; + border-radius: 6px; + color: #27364a; + background: #fff; +} + +.gdd-approval-card__decisions { + display: grid; + gap: 8px; +} + +.gdd-approval-card__decisions article { + display: grid; + gap: 3px; + padding: 9px 10px; + border: 1px solid #e2e8f0; + border-radius: 7px; + background: #fff; +} + +.gdd-approval-card__decisions article span { + color: #1f6feb; + font-size: 12px; +} + +.gdd-approval-card__decisions article small { + color: #526173; + overflow-wrap: anywhere; +} + +.gdd-approval-card__actions, +.gdd-approval-card__dialog-actions, +.gdd-approval-card__recovery { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.gdd-approval-card__actions button, +.gdd-approval-card__dialog-actions button, +.gdd-approval-card__recovery button { + min-height: 32px; + padding: 0 12px; + border: 1px solid #cfd7e6; + border-radius: 6px; + color: #fff; + background: #1f6feb; +} + +.gdd-approval-card__actions button:nth-child(n + 2), +.gdd-approval-card__dialog-actions button:first-child { + color: #27364a; + background: #fff; +} + +.gdd-approval-card button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.gdd-approval-card__recovery { + justify-content: space-between; + padding: 9px 10px; + border-radius: 7px; + color: #854d0e; + background: #fff7df; + font-size: 13px; +} + +.gdd-approval-card__error { + padding: 9px 10px; + border-radius: 7px; + color: #991b1b; + background: #fff1f2; + overflow-wrap: anywhere; +} + +.gdd-approval-card__dialog-backdrop { + position: fixed; + inset: 0; + z-index: 220; + display: grid; + padding: 24px; + background: rgb(0 0 0 / 56%); + place-items: center; +} + +.gdd-approval-card__dialog { + display: grid; + gap: 12px; + width: min(520px, 100%); + padding: 20px; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: #fff; + box-shadow: 0 18px 52px rgb(0 0 0 / 18%); +} + +.gdd-approval-card__details { + max-height: min(760px, calc(100vh - 32px)); + overflow: auto; +} + +.gdd-approval-card__details header, +.gdd-approval-card__details article { + display: grid; + gap: 5px; +} + +.gdd-approval-card__details article { + padding-top: 10px; + border-top: 1px solid #e2e8f0; +} + +.gdd-approval-card__details p { + white-space: pre-line; + overflow-wrap: anywhere; +} + +.gdd-approval-card__dialog textarea { + width: 100%; + min-height: 110px; + padding: 9px; + border: 1px solid #cfd7e6; + border-radius: 6px; + resize: vertical; +} + +.gdd-approval-card__dialog-hint { + padding: 9px 10px; + border-radius: 7px; + color: #854d0e; + background: #fff7df; + font-size: 13px; +} + +/* 审批卡是这一列里唯一没有天花板的成员:Fast GDD 的决定项越多它越高,能把消息列表和运行 + 时面板一起挤出可视区。和 `.agent-runtime-status` 一样给它自己的上限加内部滚动,而不是 + 让它去挤别人。 */ +/* 策划链路的会话列比另外两条多一个成员:策划面(阶段进度 + 审批卡)。#193(5802048c3 + 「Codex/agent chat layout fix」)把这个容器从基础规则继承来的 `display: grid` 改成 + `display: block`、并把消息列表从 `flex: 1 1 auto` 改成 `height: 100%`,于是列表独吞 + 整列高度,排在它后面的策划窄条、待确认命令、输入框和状态行全部被 `overflow: hidden` + 静默裁掉——组件照常渲染、DOM 里查得到,只是位置落在裁剪线外,屏幕上什么都没有。 + 实测:窄条 top=858 而容器 bottom=788,超出 384px;scrollHeight 1235 vs clientHeight 734。 + + 同一批改动还删掉了 `.game-workbench-chat .project-supervisor-composer` 的 4 条规则, + 所以列表上那个 `padding-bottom: 196px`(本意是给「浮起来的输入框」留位)从来没有对应 + 的定位规则,那个模型压根没成立过。 + + 命中判据是两个类的并集,两个都只属于策划链路,做游戏与做素材保持 master 的 block + 列原样不动。为什么不能只认策划面:`PlanGddSurface` 在 `planGddState === null` 时整个 + 返回 null,而 `planGddState` 初值就是 null、要靠一次独立的 IPC 才填上。首帧竞态、以及 + hydrate 持续失败时,策划 run 照跑、澄清卡照渲染,只认策划面就会漏掉这一格,裁剪原样 + 复发。窄条自己在场就等价于「有东西要用户动手」,把它并进来之后,「卡片存在」和「列 + 布局生效」才是同一件事。 + + 用 flex 列而不是 grid:子元素个数随待确认命令的有无浮动,固定行模板会错位;而且下面 + 那条 `.plan-gdd-surface { flex: 0 0 auto }` 本来就是照 flex 写的,容器变成 flex 之后它 + 才真正生效——在此之前它一直是死声明。 */ +.game-workbench-chat + .project-supervisor-conversation:has( + .plan-gdd-surface, + .planning-lane-runtime-strip + ) { + display: flex; + flex-direction: column; + gap: 10px; + /* 兜底:正常情况下上面的行分配已经让整列放得下,这条不会产生滚动条。但审批卡展开 + 到 52dvh 且窄条同时在场时仍可能超出,届时给滚动而不是继承 `overflow: hidden` + 的裁剪——这个 bug 的全部代价就来自「悄悄裁掉」,宁可多一根滚动条。 */ + overflow-y: auto; +} + +/* 整列里只有消息列表可收缩:其余成员各自有天花板(策划面按内容、窄条 52dvh 且内滚、 + 输入框定高),由列表吸收剩余空间。`height: 100%` 必须撤掉,否则它照旧独吞整列; + `padding-bottom` 也一并收回,输入框已经回到文档流里,不再需要给它留空。 */ +.game-workbench-chat + .project-supervisor-conversation:has( + .plan-gdd-surface, + .planning-lane-runtime-strip + ) + .project-supervisor-message-list { + flex: 1 1 auto; + height: auto; + min-height: 96px; + padding-bottom: 12px; + scroll-padding-bottom: 12px; +} + +/* 这三个成员不参与压缩:消息列表变长时不能把它们挤没——它们正是用户唯一需要动手的 + 地方(澄清卡、输入框)。 */ +.game-workbench-chat + .project-supervisor-conversation:has( + .plan-gdd-surface, + .planning-lane-runtime-strip + ) + .planning-lane-runtime-strip, +.game-workbench-chat + .project-supervisor-conversation:has( + .plan-gdd-surface, + .planning-lane-runtime-strip + ) + .project-supervisor-composer, +.game-workbench-chat + .project-supervisor-conversation:has( + .plan-gdd-surface, + .planning-lane-runtime-strip + ) + .project-supervisor-workspace-status { + flex: 0 0 auto; +} + +/* 这个面不参与 flex 压缩。它自己不会长高:标题栏是固定几行,审批卡下面有自己的 + max-height 和内滚,所以高度天然有界。让它可压缩的话,`overflow: hidden`(画圆角 + 要的)会把底部切掉——批准后交付行正好在那儿,表现是路径和两个按钮凭空消失。 */ +.game-workbench-chat .plan-gdd-surface { + flex: 0 0 auto; +} + +/* 只有审批卡在场时才需要「标题栏定高 + 正文占余下」的两行轨道。批准后卡片收掉, + 面里只剩标题栏,这条声明会给它套上一个不存在的第二行。 */ +.game-workbench-chat .plan-gdd-surface--with-card { + min-height: 0; + grid-template-rows: auto minmax(0, 1fr); +} + +.game-workbench-chat .gdd-approval-card { + min-height: 0; + max-height: clamp(240px, 52dvh, 640px); + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; +} + +/* 上面 24dvh/240px 的天花板是给做游戏链路常驻的调试面板设的。策划链路的窄条只在 + 需要用户动手时出现(澄清卡 / 失败恢复),内容是要读完再回答的题面,给它和审批 + 卡同级的空间。 */ +.game-workbench-chat .planning-lane-runtime-strip { + max-height: clamp(240px, 52dvh, 640px); +} + +/* 做方案在拿到第一个已登记资源之前,左边的资源画布是空的;工作台此时收成单栏, + 对话铺满。类名由 view/project-development/index.tsx 的 conversationOnlyWorkbench + 拼上(planningStartMode && resources.length === 0),画布是 display: none 不是 + 不渲染——页签、缩放、选中状态都保留,第一个资源登记后自动恢复双栏。 + 同批规则曾与上面的策划样式一起被 #193 误删。 */ +.game-workbench-layout.is-conversation-only { + grid-template-columns: minmax(0, 1fr); +} + +.game-workbench-layout.is-conversation-only .game-workbench-stage { + display: none; +} diff --git a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts index a7ffae767..e6ccb2fd3 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts @@ -3,6 +3,7 @@ import type { ProjectSupervisorComponentProps } from '../../src/features/app-she import { WorkspaceLauncherShell } from '../../src/features/app-shell/WorkspaceLauncher'; import { act, + agentRuntimeUserInputRequest, App, cleanup, createGameCreationAppManifest, @@ -1611,6 +1612,93 @@ export function registerHomeProjectCreationTests() { ).toHaveLength(1); }, ); + + it('surfaces the planning clarification card after 做方案 creates the project from home', async () => { + // 上面那条只断言到「run 起来了、source 对」。真实故障恰好落在它之后:plan 根 run + // 停在 waiting-for-user-input 并带回澄清请求,而工作台一直停在前端本地的占位文案, + // 澄清卡永远不出现。策划链路现有用例全部走「打开已有项目 + 直接注入 initialRuntime」, + // 正好绕开首页自动建项目这条路,所以这个缺口一直没人守。 + const projectPath = '/tmp/home-planning-clarification'; + const manifest = createGameCreationAppManifest( + 'local-project-draft', + 'home-planning-clarification', + ); + const supervisorHarness = createProjectSupervisorRuntimeHarness({ + projectPath, + expectedRunProfile: 'standard', + }); + let planRootRunId = ''; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'pick_local_project_directory') { + return projectPath; + } + if (command === 'is_local_project_directory_non_empty') { + return false; + } + if (command === 'create_automatic_local_game_project') { + return { + projectPath, + manifestPath: `${projectPath}/.agent/manifest.json`, + manifest, + }; + } + if (command === 'start_game_creator_supervisor_runtime_task') { + planRootRunId = String(args?.runId ?? ''); + } + const result = await supervisorHarness.invoke(command, args); + if (command !== 'read_game_creator_agent_runtime' || !planRootRunId) { + return result; + } + // 后端此刻的真实形态:pending 是 user.input_request,读命令把澄清请求投影在 + // 结果的**顶层**(`AgentRuntimeResult.user_input_request`,与 `state` 平级), + // 前端的 agentRuntimeStateFromResult 也优先读顶层。放进 state 会被顶层的 null + // 盖掉,那是 fixture 写错,不是产品缺陷。 + const runtimeResult = result as { state: Record }; + return { + ...runtimeResult, + state: { + ...runtimeResult.state, + status: 'waiting-for-user-input', + phase: 'waiting-for-user-input', + }, + userInputRequest: agentRuntimeUserInputRequest({ + agentId: 'project-supervisor', + sessionId: supervisorHarness.sessionId, + runId: planRootRunId, + requestId: 'request-plan-round-1', + actionId: 'action-plan-round-1', + }), + }; + }, + ); + window.__TAURI__ = { + core: { invoke }, + event: { listen: supervisorHarness.listen }, + }; + renderLauncherAt('/?launcher', 'home', true); + + fireEvent.click(screen.getByRole('button', { name: '做方案' })); + const promptInput = screen.getByLabelText('创作想法'); + nativeClipboardMock.text = '2D射击游戏'; + fireEvent.paste(promptInput); + await waitFor(() => { + expect(promptInput.textContent).toContain('2D射击游戏'); + }); + fireEvent.keyDown(promptInput, { key: 'Enter', code: 'Enter' }); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith( + 'start_game_creator_supervisor_runtime_task', + expect.objectContaining({ source: PROJECT_SUPERVISOR_PLAN_SOURCE }), + ); + }); + + const strip = await screen.findByLabelText('立项策划运行状态'); + expect( + within(strip).getByText('首版角色规范图采用哪种美术方向?'), + ).not.toBeNull(); + }); it('refreshes Direct Codex art commits while the turn is still running and after a later failure', async () => { const projectPath = 'C:\\Users\\tester\\Documents\\Genarrative GameAgent\\live-direct-art'; diff --git a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts index 9ee3e9fd8..cb3e568a0 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts @@ -1,3 +1,6 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + import { agentRuntimeUserInputRequest, createPlanGddStateView, @@ -521,4 +524,91 @@ export function registerPlanGddApprovalTests() { expectSupervisorRuntimePanelAbsent(); expect(screen.queryByText('策划子 Run 退出')).toBeNull(); }); + + it('keeps a stylesheet rule for every class the planning components reference', () => { + // #193「Codex/agent chat layout fix」重写聊天样式时,把策划前端的选择器整段 + // 误删:组件(TSX)原样保留、样式全数消失,审批卡/阶段条/交付行裸奔,正文弹层 + // 失去 fixed 定位变成内联平铺。组件和它的样式分居两个文件,重构样式的人看不见 + // 使用方——这条测试就是那根缺失的连线:类名清单直接从组件源码里推导,组件加了 + // 新类而样式没跟上、或样式又被顺手清掉,这里都会红。 + const componentSources = [ + 'src/features/project-workspace/GddApprovalCard.tsx', + 'src/features/project-workspace/PlanningLaneRuntimeStrip.tsx', + ] + .map((path) => + readFileSync( + resolve(process.cwd(), 'apps/ai-game-creator-shell', path), + 'utf8', + ), + ) + .join('\n'); + const referencedClasses = new Set(); + for (const match of componentSources.matchAll( + /className=(?:"([^"]+)"|\{`([^`]+)`\})/g, + )) { + // 条件类长在插值里:`plan-gdd-surface${showCard ? ' …--with-card' : ''}`。 + // 把 `${…}` 整段丢掉等于把它们排除在守门之外,而它们恰恰是最容易被顺手删干净 + // 的一档——`--with-card` 挂着审批卡的行模板,没有它卡片底部会被外壳的 + // `overflow: hidden` 切掉。只取插值里的字符串字面量:三元的条件、变量名都不是 + // 类名,不能混进清单。 + const literal = (match[1] ?? match[2] ?? '').replace( + /\$\{([^}]*)\}/g, + (_whole, expression: string) => + [...expression.matchAll(/'([^']*)'|"([^"]*)"/g)] + .map((piece) => piece[1] ?? piece[2] ?? '') + .join(' '), + ); + for (const name of literal.split(/\s+/)) { + if (name) { + referencedClasses.add(name); + } + } + } + expect(referencedClasses.size).toBeGreaterThan(10); + const styles = readFileSync( + resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + 'utf8', + ); + for (const name of referencedClasses) { + // 子串匹配会把 `.gdd-approval-card__header` 当成 `.gdd-approval-card` 的证据: + // 前缀类的规则被删光、只剩派生类时这里照样绿。要求类名后面不能再跟类名字符, + // 才是「存在这个类的精确 selector」。 + const exactSelector = new RegExp( + `\\.${name.replace(/[^\w-]/g, '\\$&')}(?![\\w-])`, + ); + expect( + exactSelector.test(styles), + `styles.css 缺少 .${name} 的精确 selector`, + ).toBe(true); + } + // 正文弹层必须是浮层:backdrop 一旦丢掉 fixed 定位,整个 GDD 会内联平铺进 + // 消息流里——这正是误删当时最刺眼的症状。 + expect(styles).toMatch( + /\.gdd-approval-card__dialog-backdrop\s*\{[^}]*position:\s*fixed/s, + ); + // 做方案的单栏工作台不在上面两个组件文件里(类名由 + // view/project-development/index.tsx 拼出),显式钉住:没有这两条规则时, + // 策划项目一打开就是左边一整片空资源画布。它们和策划样式死在 #193 同一刀里。 + expect(styles).toMatch( + /\.game-workbench-layout\.is-conversation-only\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\)/s, + ); + expect(styles).toMatch( + /\.game-workbench-layout\.is-conversation-only \.game-workbench-stage\s*\{[^}]*display:\s*none/s, + ); + // 工作台里的会话列是 `overflow: hidden` 的定高列。策划链路比另外两条多出窄条 + // (澄清卡 / 失败恢复),只有把这一列排成 flex 列、窄条不参与压缩,它才落在可视 + // 区里;否则组件照常渲染却被静默裁到线外,屏幕上什么都没有。命中判据必须包含窄条 + // 自身——只认 `.plan-gdd-surface` 时,`planGddState` 还没 hydrate 出来的那一格里 + // 澄清卡照样被裁。 + expect(styles).toMatch( + /\.game-workbench-chat\s+\.project-supervisor-conversation:has\([^)]*\.planning-lane-runtime-strip[^)]*\)\s*\{[^}]*display:\s*flex/s, + ); + // `--with-card` 有两条规则,上面的存在性检查只要还剩一条就绿。承重的是这一条: + // 策划面是 `display: grid` + `overflow: hidden` 的外壳,审批卡的 `max-height` + // 和内滚要靠这个行模板才有边界。只删它、留下那条分隔线,表现是批准后的交付行 + // 连同路径和两个按钮被切在壳外。 + expect(styles).toMatch( + /\.game-workbench-chat\s+\.plan-gdd-surface--with-card\s*\{[^}]*grid-template-rows:\s*auto minmax\(0, 1fr\)/s, + ); + }); }