Merge pull request #944 from pikasTech/fix/issue940-trace-final-response

fix: collapse trace after final response
This commit is contained in:
Lyon
2026-06-05 22:37:01 +08:00
committed by GitHub
7 changed files with 41 additions and 15 deletions
+1 -2
View File
@@ -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");
@@ -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);
});
@@ -143,7 +143,15 @@ function MessageCard({ message, firstScreenHistory, codeAgentTimeoutMs, onCancel
const hasFinalResponse = message.status !== "running" && message.text.trim().length > 0;
return (
<article className={`message-card message-${message.role} tone-border-${toneClass(message.status)}`} data-message-status={message.status}>
{message.runnerTrace ? <MessageTracePanel trace={message.runnerTrace} defaultOpen={message.status === "running"} storageKey={traceKey} markdownUpgradeDelayMs={firstScreenHistory ? FIRST_SCREEN_HISTORY_MARKDOWN_DELAY_MS : undefined} /> : null}
{message.runnerTrace ? (
<MessageTracePanel
trace={message.runnerTrace}
defaultOpen={message.status === "running"}
storageKey={traceKey}
collapseWhenFinalResponse={hasFinalResponse}
markdownUpgradeDelayMs={firstScreenHistory ? FIRST_SCREEN_HISTORY_MARKDOWN_DELAY_MS : undefined}
/>
) : null}
<section className="message-final-response" aria-label="Final Response" data-first-screen-history={firstScreenHistory ? "true" : undefined}>
{hasFinalResponse ? <MessageMarkdown upgradeDelayMs={firstScreenHistory ? FIRST_SCREEN_HISTORY_MARKDOWN_DELAY_MS : undefined}>{message.text}</MessageMarkdown> : null}
</section>
@@ -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(<MessageTracePanel trace={trace} defaultOpen collapseWhenFinalResponse />);
assert.match(html, /data-trace-status="completed"/u);
assert.doesNotMatch(html, /<details[^>]* open=""/u);
});
test("message trace panel renders persisted fallback for expired historical trace", () => {
const trace: RunnerTrace = {
traceId: "trc_issue932_expired",
@@ -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<boolean>(storedOpen);
const [pulseKey, setPulseKey] = useState(0);
const listRef = useRef<HTMLDivElement | null>(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) {
@@ -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);
});
@@ -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; }