From d2ee808133f59841b5bc8e01e936cd97268b1096 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 5 Jun 2026 22:33:48 +0800 Subject: [PATCH] fix: collapse trace after final response --- web/hwlab-cloud-web/scripts/check.ts | 3 +-- .../conversation/ConversationPanel.test.ts | 6 ++++++ .../conversation/ConversationPanel.tsx | 10 +++++++++- .../conversation/MessageTracePanel.test.tsx | 16 ++++++++++++++++ .../conversation/MessageTracePanel.tsx | 10 +++++++--- .../state/issue853-refresh-regression.test.ts | 4 ++-- web/hwlab-cloud-web/src/styles/workbench.css | 7 ------- 7 files changed, 41 insertions(+), 15 deletions(-) diff --git a/web/hwlab-cloud-web/scripts/check.ts b/web/hwlab-cloud-web/scripts/check.ts index dc81fb2a..8fee3417 100644 --- a/web/hwlab-cloud-web/scripts/check.ts +++ b/web/hwlab-cloud-web/scripts/check.ts @@ -87,8 +87,7 @@ assert.match(css, /\.message-body p\s*\{\s*white-space:\s*pre-wrap;\s*\}/u, "fin assert.match(css, /@keyframes trace-update-pulse/u, "trace updates must have an explicit visual pulse"); assert.match(css, /@keyframes trace-working-border/u, "running trace frame must show an explicit working animation"); assert.match(css, /\.message-trace-body\s*\{[\s\S]*?max-height:\s*calc\(4 \* 1\.35em \+ 14px\);/u, "trace/tool output body must be capped to about four lines with internal scroll"); -assert.match(css, /\.message-card\.message-agent \.message-final-response\[data-first-screen-history="true"\]\s*\{[\s\S]*?max-height:\s*calc\(4 \* 1\.45em \+ 24px\);/u, "first-screen historical final response must have bounded own scroll instead of driving page-level LCP"); -assert.match(css, /\.message-card\.message-agent \.message-final-response\[data-first-screen-history="true"\] \.message-body > p,[\s\S]*?max-height:\s*calc\(3 \* 1\.45em \+ 8px\);/u, "first-screen historical final response inner blocks must be bounded so text nodes do not become page-level LCP"); +assert.doesNotMatch(css, /\.message-final-response[^{}]*\{[^}]*max-height\s*:/u, "final response must not be height-capped; only trace output keeps its own height cap"); assert.doesNotMatch(appSource, /message-trace-storage-key/u, "trace summary must not expose raw trace ids as visible chips"); assert.doesNotMatch(appSource, /last ·/u, "trace summary must not show raw last-event labels"); diff --git a/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.test.ts b/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.test.ts index 8549690d..86151fd3 100644 --- a/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.test.ts +++ b/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.test.ts @@ -43,3 +43,9 @@ test("conversation panel passes the first-screen markdown delay to historical me assert.match(source, /MessageMarkdown upgradeDelayMs=\{firstScreenHistory \? FIRST_SCREEN_HISTORY_MARKDOWN_DELAY_MS : undefined\}/u); }); + +test("conversation panel asks trace panels to collapse after final response appears", async () => { + const source = await Bun.file(new URL("./ConversationPanel.tsx", import.meta.url)).text(); + + assert.match(source, /collapseWhenFinalResponse=\{hasFinalResponse\}/u); +}); diff --git a/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx b/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx index a92379b9..9fbe9e0d 100644 --- a/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx +++ b/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx @@ -143,7 +143,15 @@ function MessageCard({ message, firstScreenHistory, codeAgentTimeoutMs, onCancel const hasFinalResponse = message.status !== "running" && message.text.trim().length > 0; return (
- {message.runnerTrace ? : null} + {message.runnerTrace ? ( + + ) : null}
{hasFinalResponse ? {message.text} : null}
diff --git a/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.test.tsx b/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.test.tsx index 9ef8200f..82a447d0 100644 --- a/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.test.tsx +++ b/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.test.tsx @@ -56,6 +56,22 @@ test("message trace panel exposes running status for live trace animation", () = assert.match(html, /open=""/u); }); +test("message trace panel collapses automatically when final response is available", () => { + const trace: RunnerTrace = { + traceId: "trc_completed_final", + status: "completed", + events: [ + { seq: 1, label: "agentrun:assistant:message", status: "completed", message: "最终回复" } + ], + eventCount: 1 + }; + + const html = renderToStaticMarkup(); + + assert.match(html, /data-trace-status="completed"/u); + assert.doesNotMatch(html, /]* open=""/u); +}); + test("message trace panel renders persisted fallback for expired historical trace", () => { const trace: RunnerTrace = { traceId: "trc_issue932_expired", diff --git a/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.tsx b/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.tsx index 18d6ac7e..5d2ac486 100644 --- a/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.tsx +++ b/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.tsx @@ -11,12 +11,14 @@ interface MessageTracePanelProps { trace: RunnerTrace; defaultOpen?: boolean; storageKey?: string; + collapseWhenFinalResponse?: boolean; markdownUpgradeDelayMs?: number; } -export function MessageTracePanel({ trace, defaultOpen, storageKey, markdownUpgradeDelayMs }: MessageTracePanelProps): ReactElement { +export function MessageTracePanel({ trace, defaultOpen, storageKey, collapseWhenFinalResponse, markdownUpgradeDelayMs }: MessageTracePanelProps): ReactElement { const traceRunning = isRunningTrace(trace); - const storedOpen = readStoredOpen(storageKey, defaultOpen ?? traceRunning); + const shouldCollapseForFinalResponse = collapseWhenFinalResponse === true && !traceRunning; + const storedOpen = shouldCollapseForFinalResponse ? false : readStoredOpen(storageKey, defaultOpen ?? traceRunning); const [open, setOpen] = useState(storedOpen); const [pulseKey, setPulseKey] = useState(0); const listRef = useRef(null); @@ -25,8 +27,10 @@ export function MessageTracePanel({ trace, defaultOpen, storageKey, markdownUpgr useEffect(() => { if (traceRunning) { setOpen(true); + } else if (shouldCollapseForFinalResponse) { + setOpen(false); } - }, [traceRunning, trace.traceId]); + }, [shouldCollapseForFinalResponse, traceRunning, trace.traceId]); useEffect(() => { if (storageKey) { diff --git a/web/hwlab-cloud-web/src/state/issue853-refresh-regression.test.ts b/web/hwlab-cloud-web/src/state/issue853-refresh-regression.test.ts index 94a78fc8..64d461f9 100644 --- a/web/hwlab-cloud-web/src/state/issue853-refresh-regression.test.ts +++ b/web/hwlab-cloud-web/src/state/issue853-refresh-regression.test.ts @@ -34,8 +34,8 @@ test("issue 853 left sidebar collapse only removes the left columns", () => { assert.doesNotMatch(css, /\.workbench-shell\.is-left-sidebar-collapsed \.workspace-body \{ grid-template-columns: 0 0 minmax\(0, 1fr\) var\(--resize-handle-width\) var\(--right-sidebar-width\); \}/u); }); -test("issue 853 completed trace panels keep stored open state instead of closing on refresh", () => { +test("issue 853 completed trace panels only auto-collapse after a final response exists", () => { const source = fs.readFileSync(path.join(srcRoot, "components/conversation/MessageTracePanel.tsx"), "utf8"); - assert.doesNotMatch(source, /setOpen\(false\);/u); + assert.match(source, /shouldCollapseForFinalResponse = collapseWhenFinalResponse === true && !traceRunning/u); assert.match(source, /if \(traceRunning\) \{\n\s+setOpen\(true\);\n\s+\}/u); }); diff --git a/web/hwlab-cloud-web/src/styles/workbench.css b/web/hwlab-cloud-web/src/styles/workbench.css index ff7c0416..53e2390b 100644 --- a/web/hwlab-cloud-web/src/styles/workbench.css +++ b/web/hwlab-cloud-web/src/styles/workbench.css @@ -199,13 +199,6 @@ button:disabled { cursor: not-allowed; opacity: 0.55; } .message-card.message-user { justify-self: end; width: fit-content; max-width: min(74%, 720px); padding: 0; border-color: #c9d7d2; background: #f7fbf8; } .message-card.message-agent { display: grid; gap: 6px; width: 100%; border-left-width: 4px; background: linear-gradient(90deg, #fbfffb 0%, #fff 42%); } .message-card.message-agent .message-final-response { border: 1px solid #dce7df; border-radius: 6px; background: #fbfdf9; padding: 8px 10px; } -.message-card.message-agent .message-final-response[data-first-screen-history="true"] { max-height: calc(4 * 1.45em + 24px); overflow: auto; scrollbar-gutter: stable; } -.message-card.message-agent .message-final-response[data-first-screen-history="true"] .message-body > p, -.message-card.message-agent .message-final-response[data-first-screen-history="true"] .message-body > ul, -.message-card.message-agent .message-final-response[data-first-screen-history="true"] .message-body > ol, -.message-card.message-agent .message-final-response[data-first-screen-history="true"] .message-body > blockquote, -.message-card.message-agent .message-final-response[data-first-screen-history="true"] .message-body > table, -.message-card.message-agent .message-final-response[data-first-screen-history="true"] .message-body > pre { max-height: calc(3 * 1.45em + 8px); overflow: auto; scrollbar-gutter: stable; } .message-head { display: none; } .message-body { line-height: 1.45; overflow-wrap: anywhere; } .message-body p { white-space: pre-wrap; }