策划链路恢复列布局,澄清卡与输入框不再被静默裁出可视区
Project CI / Repository checks (pull_request) Successful in 4m19s
Project CI / Frontend tests (pull_request) Successful in 4m45s
Project CI / Backend tests (pull_request) Successful in 6m16s
Project CI / Native shell tests (pull_request) Successful in 14m16s

现象:立项策划提交需求后,界面永远停在「任务已接收」,澄清问答卡不出现。后端、
模式判定、链路判据、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 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 09:43:32 +00:00
parent b8dd1dc828
commit f039337d06
2 changed files with 216 additions and 61 deletions
+128 -61
View File
@@ -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` 一样给它自己的上限加内部滚动,而不是
让它去挤别人。 */
/* 策划链路的会话列比另外两条多一个成员:策划面(阶段进度 + 审批卡)。#1935802048c3
「Codex/agent chat layout fix」)把这个容器从基础规则继承来的 `display: grid` 改成
`display: block`、并把消息列表从 `flex: 1 1 auto` 改成 `height: 100%`,于是列表独吞
整列高度,排在它后面的策划窄条、待确认命令、输入框和状态行全部被 `overflow: hidden`
静默裁掉——组件照常渲染、DOM 里查得到,只是位置落在裁剪线外,屏幕上什么都没有。
实测:窄条 top=858 而容器 bottom=788,超出 384pxscrollHeight 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);
}
@@ -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<string, unknown>) => {
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<string, unknown> };
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';