策划列布局的命中判据补上窄条,hydrate 还没回来时澄清卡不再被裁
Project CI / Repository checks (pull_request) Successful in 4m24s
Project CI / Frontend tests (pull_request) Successful in 4m55s
Project CI / Backend tests (pull_request) Successful in 6m15s
Project CI / Native shell tests (pull_request) Successful in 14m53s

上一笔只用 `: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 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 10:03:24 +00:00
parent f039337d06
commit 763f4b5b4c
2 changed files with 39 additions and 9 deletions
+31 -9
View File
@@ -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;
}
@@ -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,
);
});
}