From d170326a6b58f81b42810102dc9840655af6ddbb Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 25 Aug 2026 07:55:22 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E8=A2=AB=20#193=20?= =?UTF-8?q?=E8=AF=AF=E5=88=A0=E7=9A=84=E7=AD=96=E5=88=92=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E6=A0=B7=E5=BC=8F=EF=BC=8C=E5=B9=B6=E5=8A=A0?= =?UTF-8?q?=E4=B8=8A=E7=BB=84=E4=BB=B6=E5=88=B0=E6=A0=B7=E5=BC=8F=E7=9A=84?= =?UTF-8?q?=E5=AE=88=E9=97=A8=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 症状:立项策划界面整体裸奔——审批卡、阶段进度条、交付行没有任何框和布局, 「查看 GDD 正文」弹层因 backdrop 丢掉 fixed 定位而内联平铺进消息流。 成因:策划前端经 squash PR #159 完整进入 master,组件(TSX)至今与源分支最终态 逐字节一致;随后 #193「Codex/agent chat layout fix」重写聊天区样式时,把 styles.css 里策划前端的选择器整段删除——gdd-approval-card 34 条、plan-gdd* 17 条、planning-lane-runtime-strip 上限——未搬往任何其他文件(全仓检索为 0)。同一 个 PR 把 project-development.suite.ts 大幅重写,原有的 CSS 守门断言一并消失, 所以没有任何测试变红。 修复:从 5802048c3^ 原样取回全部 42 个规则块,整段追加在 styles.css 末尾并注明 来历。这些选择器在当前文件中出现次数为 0,纯加法、零冲突,不触碰 #193/#194 有意 重构的聊天区规则;追加在末尾也保持了原有的同权重覆盖关系(策划窄条的上限仍压过 调试面板的 240px 天花板)。 守门:plan-gdd.suite.ts(两轮样式重构都未被触碰的文件)新增一条测试,类名清单 直接从 GddApprovalCard / PlanningLaneRuntimeStrip 源码推导,逐个断言 styles.css 里有对应规则,另单独锁弹层 backdrop 的 fixed 定位。A/B 验证:抽掉恢复段即红,报 「styles.css 缺少 .plan-gdd-surface 的规则」。组件加新类而样式缺失、或样式再次 被顺手清理,这条都会拦住。 Co-Authored-By: Claude Opus 5 --- apps/ai-game-creator-shell/src/styles.css | 310 ++++++++++++++++++ .../tests/appSurface/plan-gdd.suite.ts | 49 +++ 2 files changed, 359 insertions(+) diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 1e7936569..c87664904 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -7209,3 +7209,313 @@ 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` 一样给它自己的上限加内部滚动,而不是 + 让它去挤别人。 */ +/* 这个面不参与 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); +} 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..14f38e183 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,50 @@ 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, + )) { + const literal = (match[1] ?? match[2] ?? '').replace(/\$\{[^}]*\}/g, ' '); + 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) { + expect( + styles.includes(`.${name}`), + `styles.css 缺少 .${name} 的规则`, + ).toBe(true); + } + // 正文弹层必须是浮层:backdrop 一旦丢掉 fixed 定位,整个 GDD 会内联平铺进 + // 消息流里——这正是误删当时最刺眼的症状。 + expect(styles).toMatch( + /\.gdd-approval-card__dialog-backdrop\s*\{[^}]*position:\s*fixed/s, + ); + }); } -- 2.52.0 From f829cef77446b43ba02cf101f4a3a41394d83880 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 25 Aug 2026 08:10:37 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=E8=A1=A5=E5=9B=9E=E5=90=8C=E6=89=B9?= =?UTF-8?q?=E8=A2=AB=E5=88=A0=E7=9A=84=E5=8D=95=E6=A0=8F=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0=E8=A7=84=E5=88=99=EF=BC=8C=E7=AD=96=E5=88=92=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E4=B8=8D=E5=86=8D=E4=B8=80=E6=89=93=E5=BC=80=E5=B0=B1?= =?UTF-8?q?=E6=98=AF=E7=A9=BA=E7=94=BB=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一发只按三组策划类根提取,漏了挂在工作台布局上的 is-conversation-only—— TSX 侧(planningStartMode && resources.length === 0 时给 game-workbench-layout 拼类名)经 #159 完整在主线上,规则同样死在 #193 那一刀里。症状:做方案项目一 打开,左边一整片空资源画布(文档 0 项、美术 0 项),本该收成单栏让对话铺满。 按「现有代码引用 + 现有 css 无规则 + #193 前有规则」三条同时成立做了全仓清点: 28 个被删类里受害者仅此一个,其余 27 个的引用方已在同批重构中移除,不属误删。 守门测试显式钉住这两条规则——它们不在策划组件文件里,靠类名推导扫不到。 Co-Authored-By: Claude Fable 5 --- apps/ai-game-creator-shell/src/styles.css | 13 +++++++++++++ .../tests/appSurface/plan-gdd.suite.ts | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index c87664904..af4501670 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -7519,3 +7519,16 @@ iframe.preview-frame { .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/plan-gdd.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts index 14f38e183..d6ed7813b 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 @@ -569,5 +569,14 @@ export function registerPlanGddApprovalTests() { 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, + ); }); } -- 2.52.0 From f039337d06fdb29b85244120baa0e67a74b3d464 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 25 Aug 2026 09:43:32 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=E7=AD=96=E5=88=92=E9=93=BE=E8=B7=AF?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=88=97=E5=B8=83=E5=B1=80=EF=BC=8C=E6=BE=84?= =?UTF-8?q?=E6=B8=85=E5=8D=A1=E4=B8=8E=E8=BE=93=E5=85=A5=E6=A1=86=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=A2=AB=E9=9D=99=E9=BB=98=E8=A3=81=E5=87=BA=E5=8F=AF?= =?UTF-8?q?=E8=A7=86=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:立项策划提交需求后,界面永远停在「任务已接收」,澄清问答卡不出现。后端、 模式判定、链路判据、readOnly、数据传递全部正确,卡片也确实渲染了——314px 高、 visible、opacity 1,只是被画在裁剪线外面。实测窄条 top=858 而容器 bottom=788, 超出 384px;scrollHeight 1235 vs clientHeight 734,输入框和状态行也一起在外面。 根因是 #193(5802048c3「Codex/agent chat layout fix」)重写这块布局时: - 给 .game-workbench-chat .project-supervisor-conversation 加了 display: block, 杀死它从基础规则继承的 display: grid,行模板 minmax(340px,1fr) auto auto auto 整个失效; - 把消息列表从 flex: 1 1 auto 改成 height: 100%,于是它独吞整列高度; - 删掉了 .game-workbench-chat .project-supervisor-composer 的 4 条规则,所以列表 上那条 padding-bottom: 196px(本意是给「浮起来的输入框」留位)从来没有对应的 定位规则,那个模型压根没成立过。 容器又是 overflow: hidden,所以排在列表后面的成员全部被无声裁掉。 只给策划链路恢复:`:has(.plan-gdd-surface)` 仅在策划面在场时命中,做游戏与做素材 保持 master 的 block 列原样不动。用 flex 列而不是 grid——子元素个数随「待确认命令」 的有无浮动,固定行模板会错位;而且既有的 .plan-gdd-surface { flex: 0 0 auto } 本来 就是照 flex 写的,容器变成 flex 之后它才第一次真正生效,此前一直是死声明。 另加 overflow-y: auto 兜底:审批卡展开到 52dvh 且窄条同时在场时理论上仍可能超出, 届时给滚动条而不是继承裁剪。这个 bug 的全部代价都来自「悄悄裁掉」。 顺带补一条 appSurface 用例,覆盖「首页做方案 → 自动建项目 → 自动投递 → 工作台出 澄清卡」这条此前零覆盖的路径。必须说明:它拦不住本次这类问题——jsdom 没有布局引擎, 高度恒为 0,裁剪与溢出对它天然不可见。它守的是数据流,不是版面。 Co-Authored-By: Claude Opus 5 --- apps/ai-game-creator-shell/src/styles.css | 189 ++++++++++++------ .../tests/appSurface/home.suite.ts | 88 ++++++++ 2 files changed, 216 insertions(+), 61 deletions(-) diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index af4501670..046a93732 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); @@ -7210,7 +7226,6 @@ iframe.preview-frame { } } - /* ============================================================ 立项策划前端样式(PlanGddSurface / 阶段进度 / GDD 审批卡 / 交付行 / planning-lane-runtime-strip)。 @@ -7223,7 +7238,7 @@ iframe.preview-frame { /* 策划区是一个面:阶段进度是标题栏,审批卡是正文。边框和圆角只画在外壳上,里面两块 不再各自带框。 */ -.plan-gdd-surface{ +.plan-gdd-surface { display: grid; min-width: 0; border: 1px solid #cfd7e6; @@ -7232,18 +7247,18 @@ iframe.preview-frame { overflow: hidden; } -.gdd-approval-card{ +.gdd-approval-card { display: grid; gap: 14px; padding: 16px 18px 18px; background: #f8fbff; } -.plan-gdd-surface--with-card .plan-gdd-stage-progress{ +.plan-gdd-surface--with-card .plan-gdd-stage-progress { border-bottom: 1px solid #dbe4f1; } -.plan-gdd-stage-progress{ +.plan-gdd-stage-progress { display: grid; gap: 6px; padding: 10px 12px; @@ -7253,7 +7268,7 @@ iframe.preview-frame { } .plan-gdd-stage-progress__header, -.plan-gdd-stage-progress__meta{ +.plan-gdd-stage-progress__meta { display: flex; flex-wrap: wrap; align-items: center; @@ -7263,7 +7278,7 @@ iframe.preview-frame { /* 批准后的交付行。它是策划阶段唯一的产物出口,所以给一条分隔线把它和上面的状态 区分开,而不是混成第三行元信息。 */ -.plan-gdd-stage-progress__delivery{ +.plan-gdd-stage-progress__delivery { display: grid; gap: 8px; margin-top: 4px; @@ -7271,20 +7286,20 @@ iframe.preview-frame { border-top: 1px solid #e6ecf5; } -.plan-gdd-stage-progress__delivery code{ +.plan-gdd-stage-progress__delivery code { min-width: 0; color: #3c4a5c; font-size: 11px; overflow-wrap: anywhere; } -.plan-gdd-stage-progress__delivery-actions{ +.plan-gdd-stage-progress__delivery-actions { display: flex; flex-wrap: wrap; gap: 8px; } -.plan-gdd-stage-progress__delivery-actions button{ +.plan-gdd-stage-progress__delivery-actions button { min-height: 30px; padding: 0 12px; border: 1px solid #cfd7e6; @@ -7295,32 +7310,32 @@ iframe.preview-frame { } .plan-gdd-stage-progress__delivery-actions button:hover:not(:disabled), -.plan-gdd-stage-progress__delivery-actions button:focus-visible{ +.plan-gdd-stage-progress__delivery-actions button:focus-visible { border-color: #1f6feb; color: #1f6feb; } -.plan-gdd-stage-progress__delivery-actions button:disabled{ +.plan-gdd-stage-progress__delivery-actions button:disabled { opacity: 0.58; } -.plan-gdd-stage-progress__delivery-error{ +.plan-gdd-stage-progress__delivery-error { color: #b42323; overflow-wrap: anywhere; } -.plan-gdd-stage-progress__header strong{ +.plan-gdd-stage-progress__header strong { color: #27364a; } -.plan-gdd-stage-progress__header span{ +.plan-gdd-stage-progress__header span { padding: 3px 8px; border-radius: 999px; background: #eef6ff; color: #1f6feb; } -.gdd-approval-card__header{ +.gdd-approval-card__header { display: grid; gap: 5px; min-width: 0; @@ -7328,26 +7343,26 @@ iframe.preview-frame { .gdd-approval-card h2, .gdd-approval-card h3, -.gdd-approval-card p{ +.gdd-approval-card p { margin: 0; } -.gdd-approval-card h2{ +.gdd-approval-card h2 { font-size: 18px; } -.gdd-approval-card__header p{ +.gdd-approval-card__header p { color: #526173; overflow-wrap: anywhere; } -.gdd-approval-card__details-trace{ +.gdd-approval-card__details-trace { color: #6b7a8c; font-size: 11px; overflow-wrap: anywhere; } -.gdd-approval-card__details-trigger{ +.gdd-approval-card__details-trigger { justify-self: start; min-height: 30px; padding: 0 10px; @@ -7357,12 +7372,12 @@ iframe.preview-frame { background: #fff; } -.gdd-approval-card__decisions{ +.gdd-approval-card__decisions { display: grid; gap: 8px; } -.gdd-approval-card__decisions article{ +.gdd-approval-card__decisions article { display: grid; gap: 3px; padding: 9px 10px; @@ -7371,19 +7386,19 @@ iframe.preview-frame { background: #fff; } -.gdd-approval-card__decisions article span{ +.gdd-approval-card__decisions article span { color: #1f6feb; font-size: 12px; } -.gdd-approval-card__decisions article small{ +.gdd-approval-card__decisions article small { color: #526173; overflow-wrap: anywhere; } .gdd-approval-card__actions, .gdd-approval-card__dialog-actions, -.gdd-approval-card__recovery{ +.gdd-approval-card__recovery { display: flex; flex-wrap: wrap; align-items: center; @@ -7392,7 +7407,7 @@ iframe.preview-frame { .gdd-approval-card__actions button, .gdd-approval-card__dialog-actions button, -.gdd-approval-card__recovery button{ +.gdd-approval-card__recovery button { min-height: 32px; padding: 0 12px; border: 1px solid #cfd7e6; @@ -7402,17 +7417,17 @@ iframe.preview-frame { } .gdd-approval-card__actions button:nth-child(n + 2), -.gdd-approval-card__dialog-actions button:first-child{ +.gdd-approval-card__dialog-actions button:first-child { color: #27364a; background: #fff; } -.gdd-approval-card button:disabled{ +.gdd-approval-card button:disabled { cursor: not-allowed; opacity: 0.55; } -.gdd-approval-card__recovery{ +.gdd-approval-card__recovery { justify-content: space-between; padding: 9px 10px; border-radius: 7px; @@ -7421,7 +7436,7 @@ iframe.preview-frame { font-size: 13px; } -.gdd-approval-card__error{ +.gdd-approval-card__error { padding: 9px 10px; border-radius: 7px; color: #991b1b; @@ -7429,7 +7444,7 @@ iframe.preview-frame { overflow-wrap: anywhere; } -.gdd-approval-card__dialog-backdrop{ +.gdd-approval-card__dialog-backdrop { position: fixed; inset: 0; z-index: 220; @@ -7439,7 +7454,7 @@ iframe.preview-frame { place-items: center; } -.gdd-approval-card__dialog{ +.gdd-approval-card__dialog { display: grid; gap: 12px; width: min(520px, 100%); @@ -7450,28 +7465,28 @@ iframe.preview-frame { box-shadow: 0 18px 52px rgb(0 0 0 / 18%); } -.gdd-approval-card__details{ +.gdd-approval-card__details { max-height: min(760px, calc(100vh - 32px)); overflow: auto; } .gdd-approval-card__details header, -.gdd-approval-card__details article{ +.gdd-approval-card__details article { display: grid; gap: 5px; } -.gdd-approval-card__details article{ +.gdd-approval-card__details article { padding-top: 10px; border-top: 1px solid #e2e8f0; } -.gdd-approval-card__details p{ +.gdd-approval-card__details p { white-space: pre-line; overflow-wrap: anywhere; } -.gdd-approval-card__dialog textarea{ +.gdd-approval-card__dialog textarea { width: 100%; min-height: 110px; padding: 9px; @@ -7480,7 +7495,7 @@ iframe.preview-frame { resize: vertical; } -.gdd-approval-card__dialog-hint{ +.gdd-approval-card__dialog-hint { padding: 9px 10px; border-radius: 7px; color: #854d0e; @@ -7491,21 +7506,73 @@ iframe.preview-frame { /* 审批卡是这一列里唯一没有天花板的成员: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`(本意是给「浮起来的输入框」留位)从来没有对应 + 的定位规则,那个模型压根没成立过。 + + `:has(.plan-gdd-surface)` 只在策划面在场时命中,做游戏与做素材保持 master 的 block + 列原样不动。用 flex 列而不是 grid:子元素个数随待确认命令的有无浮动,固定行模板会 + 错位;而且下面那条 `.plan-gdd-surface { flex: 0 0 auto }` 本来就是照 flex 写的,容器 + 变成 flex 之后它才真正生效——在此之前它一直是死声明。 */ +.game-workbench-chat .project-supervisor-conversation:has(.plan-gdd-surface) { + 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) + .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, +.game-workbench-chat + .project-supervisor-conversation:has(.plan-gdd-surface) + .project-supervisor-composer, +.game-workbench-chat + .project-supervisor-conversation:has(.plan-gdd-surface) + .project-supervisor-workspace-status { + flex: 0 0 auto; +} + /* 这个面不参与 flex 压缩。它自己不会长高:标题栏是固定几行,审批卡下面有自己的 max-height 和内滚,所以高度天然有界。让它可压缩的话,`overflow: hidden`(画圆角 要的)会把底部切掉——批准后交付行正好在那儿,表现是路径和两个按钮凭空消失。 */ -.game-workbench-chat .plan-gdd-surface{ +.game-workbench-chat .plan-gdd-surface { flex: 0 0 auto; } /* 只有审批卡在场时才需要「标题栏定高 + 正文占余下」的两行轨道。批准后卡片收掉, 面里只剩标题栏,这条声明会给它套上一个不存在的第二行。 */ -.game-workbench-chat .plan-gdd-surface--with-card{ +.game-workbench-chat .plan-gdd-surface--with-card { min-height: 0; grid-template-rows: auto minmax(0, 1fr); } -.game-workbench-chat .gdd-approval-card{ +.game-workbench-chat .gdd-approval-card { min-height: 0; max-height: clamp(240px, 52dvh, 640px); overflow-y: auto; @@ -7516,7 +7583,7 @@ iframe.preview-frame { /* 上面 24dvh/240px 的天花板是给做游戏链路常驻的调试面板设的。策划链路的窄条只在 需要用户动手时出现(澄清卡 / 失败恢复),内容是要读完再回答的题面,给它和审批 卡同级的空间。 */ -.game-workbench-chat .planning-lane-runtime-strip{ +.game-workbench-chat .planning-lane-runtime-strip { max-height: clamp(240px, 52dvh, 640px); } 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'; -- 2.52.0 From 763f4b5b4cd6ecdba25a22ed3cbc02a4dfafd977 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 25 Aug 2026 10:03:24 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=E7=AD=96=E5=88=92=E5=88=97=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E7=9A=84=E5=91=BD=E4=B8=AD=E5=88=A4=E6=8D=AE=E8=A1=A5?= =?UTF-8?q?=E4=B8=8A=E7=AA=84=E6=9D=A1=EF=BC=8Chydrate=20=E8=BF=98?= =?UTF-8?q?=E6=B2=A1=E5=9B=9E=E6=9D=A5=E6=97=B6=E6=BE=84=E6=B8=85=E5=8D=A1?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E8=A2=AB=E8=A3=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一笔只用 `:has(.plan-gdd-surface)` 认策划链路,漏了一格:`PlanGddSurface` 在 `planGddState === null` 时整个返回 null,而 `planGddState` 初值就是 null,要靠 `hydrate_game_creator_plan_gdd_state` 这一次独立 IPC 才填上。首帧竞态、以及 hydrate 持续失败(非写锁争用的错误会把 state 留在 null)这两种情形下,策划 run 照跑、 `PlanningLaneRuntimeStrip` 照渲染澄清卡,但容器上的 `:has()` 不命中,#193 那套 `display: block` + 列表 `height: 100%` + `overflow: hidden` 原样回来,卡片又被静默裁到 可视区外。 判据改成 `.plan-gdd-surface` 与 `.planning-lane-runtime-strip` 的并集。两个类都只属于 策划链路,做游戏与做素材仍走 master 的 block 列。窄条在场本身就等价于「有东西要用户 动手」,并进来之后「卡片存在」和「列布局生效」是同一件事,不再依赖另一条异步链路的 时序。 守门加在已有的策划样式测试里(同一处已经钉着 #193 误删的几条规则):断言这条 flex 列 规则的 `:has()` 必须包含窄条类。已验证非空转——把并集改回单一类,这条断言变红。 Co-Authored-By: Claude Opus 5 --- apps/ai-game-creator-shell/src/styles.css | 40 ++++++++++++++----- .../tests/appSurface/plan-gdd.suite.ts | 8 ++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 046a93732..ba86fce33 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -7517,11 +7517,21 @@ iframe.preview-frame { 所以列表上那个 `padding-bottom: 196px`(本意是给「浮起来的输入框」留位)从来没有对应 的定位规则,那个模型压根没成立过。 - `:has(.plan-gdd-surface)` 只在策划面在场时命中,做游戏与做素材保持 master 的 block - 列原样不动。用 flex 列而不是 grid:子元素个数随待确认命令的有无浮动,固定行模板会 - 错位;而且下面那条 `.plan-gdd-surface { flex: 0 0 auto }` 本来就是照 flex 写的,容器 - 变成 flex 之后它才真正生效——在此之前它一直是死声明。 */ -.game-workbench-chat .project-supervisor-conversation:has(.plan-gdd-surface) { + 命中判据是两个类的并集,两个都只属于策划链路,做游戏与做素材保持 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; @@ -7535,7 +7545,10 @@ iframe.preview-frame { 输入框定高),由列表吸收剩余空间。`height: 100%` 必须撤掉,否则它照旧独吞整列; `padding-bottom` 也一并收回,输入框已经回到文档流里,不再需要给它留空。 */ .game-workbench-chat - .project-supervisor-conversation:has(.plan-gdd-surface) + .project-supervisor-conversation:has( + .plan-gdd-surface, + .planning-lane-runtime-strip + ) .project-supervisor-message-list { flex: 1 1 auto; height: auto; @@ -7547,13 +7560,22 @@ iframe.preview-frame { /* 这三个成员不参与压缩:消息列表变长时不能把它们挤没——它们正是用户唯一需要动手的 地方(澄清卡、输入框)。 */ .game-workbench-chat - .project-supervisor-conversation:has(.plan-gdd-surface) + .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) + .project-supervisor-conversation:has( + .plan-gdd-surface, + .planning-lane-runtime-strip + ) .project-supervisor-composer, .game-workbench-chat - .project-supervisor-conversation:has(.plan-gdd-surface) + .project-supervisor-conversation:has( + .plan-gdd-surface, + .planning-lane-runtime-strip + ) .project-supervisor-workspace-status { flex: 0 0 auto; } 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 d6ed7813b..fd20d253e 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 @@ -578,5 +578,13 @@ export function registerPlanGddApprovalTests() { 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, + ); }); } -- 2.52.0 From cf01c99fe6f255df688b7d11d73bf0e8010437fc Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 25 Aug 2026 10:13:34 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=E7=B1=BB=E5=90=8D=E5=AE=88=E9=97=A8?= =?UTF-8?q?=E8=A1=A5=E4=B8=8A=E6=9D=A1=E4=BB=B6=E7=B1=BB=E4=B8=8E=E7=B2=BE?= =?UTF-8?q?=E7=A1=AE=20selector=20=E5=8C=B9=E9=85=8D=EF=BC=8C=E5=A0=B5?= =?UTF-8?q?=E6=8E=89=E4=B8=A4=E5=A4=84=E4=BC=9A=E9=9D=99=E9=BB=98=E6=94=BE?= =?UTF-8?q?=E8=A1=8C=E7=9A=84=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这条守门是为「#193 把策划选择器整段误删」设的,但它自己有两处会放行: 一、条件类进不了清单。抽取时 `.replace(/\$\{[^}]*\}/g, ' ')` 把插值整段丢掉,而两个 组件里唯一的条件类恰好长在插值里:``plan-gdd-surface${showCard ? ' …--with-card' : ''}``。 清单 25 个类里没有 `plan-gdd-surface--with-card`。它不是装饰性的: `.game-workbench-chat .plan-gdd-surface--with-card` 的 `grid-template-rows: auto minmax(0, 1fr)` 是审批卡内滚的边界,策划面自己是 `display: grid` + `overflow: hidden` 的外壳,删掉这条,批准后的交付行连同路径和两个按钮会被切在壳外。实测把该类的两条 规则整段删掉,改前的测试照样绿。 改成只取插值里的字符串字面量——三元的条件和变量名不是类名,不能混进清单。清单 25 → 26,新进的正是 `plan-gdd-surface--with-card`,没有杂质。 二、`styles.includes('.' + name)` 是子串匹配,`.gdd-approval-card__header` 可以冒充 `.gdd-approval-card` 的存在证据。改成要求类名后面不再跟类名字符。当前 26 个类逐一 校验都有精确 selector,所以这一处堵的是潜在放行,不是正在掩盖的缺失。 另按评审要求补一条负向钉子:单独钉住上面那条承重的行模板。存在性检查只要 `--with-card` 还剩任意一条规则就绿,而两条里只有它是承重的。 三条负向都已验证非空转:删两条规则 → 红;只删承重那条留下分隔线 → 红;把 `.gdd-approval-card__details` 改名只留派生类 → 红(改前用 includes 会绿)。 评审同时建议做 computed-style/布局断言,这里没做:appSurface 跑 jsdom,没有布局引擎, 且这套 suite 不把 styles.css 挂进 DOM。要真断言得换真实浏览器 runner,是另一件事。 Co-Authored-By: Claude Opus 5 --- .../tests/appSurface/plan-gdd.suite.ts | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) 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 fd20d253e..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 @@ -546,7 +546,18 @@ export function registerPlanGddApprovalTests() { for (const match of componentSources.matchAll( /className=(?:"([^"]+)"|\{`([^`]+)`\})/g, )) { - const literal = (match[1] ?? match[2] ?? '').replace(/\$\{[^}]*\}/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); @@ -559,9 +570,15 @@ export function registerPlanGddApprovalTests() { 'utf8', ); for (const name of referencedClasses) { + // 子串匹配会把 `.gdd-approval-card__header` 当成 `.gdd-approval-card` 的证据: + // 前缀类的规则被删光、只剩派生类时这里照样绿。要求类名后面不能再跟类名字符, + // 才是「存在这个类的精确 selector」。 + const exactSelector = new RegExp( + `\\.${name.replace(/[^\w-]/g, '\\$&')}(?![\\w-])`, + ); expect( - styles.includes(`.${name}`), - `styles.css 缺少 .${name} 的规则`, + exactSelector.test(styles), + `styles.css 缺少 .${name} 的精确 selector`, ).toBe(true); } // 正文弹层必须是浮层:backdrop 一旦丢掉 fixed 定位,整个 GDD 会内联平铺进 @@ -586,5 +603,12 @@ export function registerPlanGddApprovalTests() { 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, + ); }); } -- 2.52.0