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 28935038..8da11448 100644 --- a/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.test.ts +++ b/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.test.ts @@ -46,3 +46,16 @@ test("conversation panel asks trace panels to collapse after final response appe assert.match(source, /collapseWhenFinalResponse=\{hasFinalResponse\}/u); }); + +test("conversation panel does not consume replay opportunity before timer fires", async () => { + const source = await Bun.file(new URL("./ConversationPanel.tsx", import.meta.url)).text(); + const effectStart = source.indexOf("const replayRequestedTraceRef = useRef(null);"); + const timerStart = source.indexOf("const timer = window.setTimeout", effectStart); + const markRequested = source.indexOf("replayRequestedTraceRef.current = traceId", effectStart); + const replayCall = source.indexOf("onReplayTrace();", effectStart); + + assert.ok(effectStart > 0, "replay guard ref exists"); + assert.ok(timerStart > effectStart, "auto replay uses delayed timer"); + assert.ok(markRequested > timerStart, "route changes that cancel the timer must not mark replay as already requested"); + assert.ok(replayCall > markRequested, "replay is marked requested only immediately before dispatching replay"); +}); diff --git a/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx b/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx index 39b98d97..746d8fa7 100644 --- a/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx +++ b/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx @@ -130,12 +130,15 @@ const HISTORICAL_TRACE_REPLAY_DELAY_MS = 4500; function MessageCard({ message, firstScreenHistory, codeAgentTimeoutMs, onCancel, onRetry, onReplayTrace }: { message: ChatMessage; firstScreenHistory: boolean; codeAgentTimeoutMs: number; onCancel(): void; onRetry(): void; onReplayTrace(): void }): ReactElement { const traceKey = `hwlab.workbench.trace-open.${message.traceId ?? message.id}`; - const replayRequestedRef = useRef(false); + const replayRequestedTraceRef = useRef(null); useEffect(() => { - if (replayRequestedRef.current) return; + const traceId = message.traceId ?? message.runnerTrace?.traceId ?? null; + if (traceId && replayRequestedTraceRef.current === traceId) return; if (!shouldAutoReplayTrace(message)) return; - replayRequestedRef.current = true; - const timer = window.setTimeout(() => onReplayTrace(), HISTORICAL_TRACE_REPLAY_DELAY_MS); + const timer = window.setTimeout(() => { + replayRequestedTraceRef.current = traceId; + onReplayTrace(); + }, HISTORICAL_TRACE_REPLAY_DELAY_MS); return () => window.clearTimeout(timer); }, [message, onReplayTrace]); if (message.role === "agent") {