fix(v02): load compact conversation traces (#856)

Co-authored-by: Codex Agent <codex@hwlab.local>
This commit is contained in:
Lyon
2026-06-04 21:12:07 +08:00
committed by GitHub
parent 1e085691e6
commit 84e649c294
3 changed files with 60 additions and 4 deletions
+1 -1
View File
@@ -166,7 +166,7 @@ export function App(): ReactElement {
/>
) : null}
<section className="center-workspace">
{route === "workspace" ? <ConversationPanel messages={store.state.messages} availability={store.state.codeAgentAvailability} live={store.state.live} chatPending={store.state.chatPending} codeAgentTimeoutMs={store.state.codeAgentTimeoutMs} onCopySession={() => copyTextToClipboard(firstNonEmptyString(store.composer.sessionId, store.state.workspace?.selectedAgentSessionId, store.state.workspace?.workspace?.selectedAgentSessionId, store.activeConversationId) ?? "")} onCancelMessage={(id) => void store.cancelAgentMessage(id)} onRetryMessage={(id) => void store.retryAgentMessage(id)} /> : null}
{route === "workspace" ? <ConversationPanel messages={store.state.messages} availability={store.state.codeAgentAvailability} live={store.state.live} chatPending={store.state.chatPending} codeAgentTimeoutMs={store.state.codeAgentTimeoutMs} onCopySession={() => copyTextToClipboard(firstNonEmptyString(store.composer.sessionId, store.state.workspace?.selectedAgentSessionId, store.state.workspace?.workspace?.selectedAgentSessionId, store.activeConversationId) ?? "")} onCancelMessage={(id) => void store.cancelAgentMessage(id)} onRetryMessage={(id) => void store.retryAgentMessage(id)} onReplayTrace={(id) => void store.replayAgentTrace(id)} /> : null}
{route === "skills" ? <SkillsView /> : null}
{route === "gate" ? <GateView /> : null}
{route === "help" ? <HelpView help={help} /> : null}
@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import { shouldAutoReplayTrace } from "./ConversationPanel";
import type { ChatMessage } from "../../types/domain";
const baseAgentMessage: ChatMessage = {
id: "msg_agent",
role: "agent",
title: "Code Agent 回复",
text: "done",
status: "idle" as ChatMessage["status"],
createdAt: "2026-06-04T12:13:54.661Z",
traceId: "trc_issue849",
runnerTrace: {
traceId: "trc_issue849",
status: "completed",
eventCount: 49,
eventsCompacted: false,
fullTraceLoaded: false
}
};
test("conversation panel auto replays compact historical traces", () => {
assert.equal(shouldAutoReplayTrace(baseAgentMessage), true);
});
test("conversation panel does not auto replay loaded, empty, or running traces", () => {
assert.equal(shouldAutoReplayTrace({ ...baseAgentMessage, runnerTrace: { ...baseAgentMessage.runnerTrace, events: [{ seq: 1, label: "assistant:message" }] } }), false);
assert.equal(shouldAutoReplayTrace({ ...baseAgentMessage, runnerTrace: { ...baseAgentMessage.runnerTrace, eventCount: 0 } }), false);
assert.equal(shouldAutoReplayTrace({ ...baseAgentMessage, status: "running" }), false);
assert.equal(shouldAutoReplayTrace({ ...baseAgentMessage, traceReplayStatus: "完整 trace 已回放:49 个原始 event" }), false);
assert.equal(shouldAutoReplayTrace({ ...baseAgentMessage, runnerTrace: { ...baseAgentMessage.runnerTrace, fullTraceLoaded: true } }), false);
});
@@ -1,5 +1,5 @@
import type { ReactElement } from "react";
import { useMemo } from "react";
import { useEffect, useMemo, useRef } from "react";
import { useScrollFollow } from "../../hooks/useScrollFollow";
import type { ChatMessage, CodeAgentAvailability, LiveSurface } from "../../types/domain";
@@ -20,9 +20,10 @@ interface ConversationPanelProps {
onCopySession(): void;
onCancelMessage(messageId: string): void;
onRetryMessage(messageId: string): void;
onReplayTrace(messageId: string): void;
}
export function ConversationPanel({ messages, availability, live, chatPending, codeAgentTimeoutMs, onCopySession, onCancelMessage, onRetryMessage }: ConversationPanelProps & { codeAgentTimeoutMs: number }): ReactElement {
export function ConversationPanel({ messages, availability, live, chatPending, codeAgentTimeoutMs, onCopySession, onCancelMessage, onRetryMessage, onReplayTrace }: ConversationPanelProps & { codeAgentTimeoutMs: number }): ReactElement {
const scrollFollow = useScrollFollow<HTMLElement>([messages]);
return (
<section className="view workbench-view" id="workspace" data-view="workspace" data-scroll-follow={scrollFollow.following ? "bottom" : "detached"} aria-labelledby="workspace-title" ref={scrollFollow.ref} onScroll={scrollFollow.onScroll}>
@@ -50,6 +51,7 @@ export function ConversationPanel({ messages, availability, live, chatPending, c
codeAgentTimeoutMs={codeAgentTimeoutMs}
onCancel={() => onCancelMessage(message.id)}
onRetry={() => onRetryMessage(message.id)}
onReplayTrace={() => onReplayTrace(message.id)}
/>
))}
</div>
@@ -98,8 +100,28 @@ function CodeAgentSummary({ availability, live, latestMessage }: { availability:
);
}
function MessageCard({ message, codeAgentTimeoutMs, onCancel, onRetry }: { message: ChatMessage; codeAgentTimeoutMs: number; onCancel(): void; onRetry(): void }): ReactElement {
export function shouldAutoReplayTrace(message: ChatMessage): boolean {
if (message.role !== "agent") return false;
const trace = message.runnerTrace;
const traceId = message.traceId ?? trace?.traceId;
if (!traceId) return false;
if (message.status === "running" || trace?.status === "running") return false;
if (message.traceReplayStatus) return false;
if (trace?.fullTraceLoaded === true) return false;
const events = Array.isArray(trace?.events) ? trace.events : [];
const declaredCount = Number(trace?.eventCount ?? 0);
return events.length === 0 && declaredCount > 0;
}
function MessageCard({ message, codeAgentTimeoutMs, onCancel, onRetry, onReplayTrace }: { message: ChatMessage; codeAgentTimeoutMs: number; onCancel(): void; onRetry(): void; onReplayTrace(): void }): ReactElement {
const traceKey = `hwlab.workbench.trace-open.${message.traceId ?? message.id}`;
const replayRequestedRef = useRef(false);
useEffect(() => {
if (replayRequestedRef.current) return;
if (!shouldAutoReplayTrace(message)) return;
replayRequestedRef.current = true;
onReplayTrace();
}, [message, onReplayTrace]);
if (message.role === "agent") {
const hasFinalResponse = message.status !== "running" && message.text.trim().length > 0;
return (