From 36ee986d5548ccc382a598c834f4edcd3953c965 Mon Sep 17 00:00:00 2001 From: TYY-2005 Date: Tue, 20 Oct 2026 09:57:28 +0800 Subject: [PATCH] fix(frontend): restore thinking-only steps after reconnect (#7144) * fix(frontend): restore thinking-only steps after reconnect * docs(frontend): explain reconnect answer boundary --------- Co-authored-by: TYY-2005 Co-authored-by: Willem Jiang --- frontend/src/core/threads/hooks.ts | 6 ++- .../unit/core/threads/message-merge.test.ts | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index 56b91d7f31..1ea79991f9 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -21,7 +21,7 @@ import { getBackendBaseURL } from "../config"; import { useI18n } from "../i18n/hooks"; import { getMessageRunId } from "../messages/run-duration"; import { - hasContent, + extractContentFromMessage, hasToolCalls, isHiddenFromUIMessage, } from "../messages/utils"; @@ -1072,7 +1072,9 @@ export function restoreReconnectedTurnMessageOrder( if ( message.type === "ai" && !isHiddenFromUIMessage(message) && - hasContent(message) && + // Thinking-only blocks (Anthropic) are not completed answers: require + // extractable text/image content to close the turn. + extractContentFromMessage(message).length > 0 && !hasToolCalls(message) ) { candidateStart = index + 1; diff --git a/frontend/tests/unit/core/threads/message-merge.test.ts b/frontend/tests/unit/core/threads/message-merge.test.ts index 2e5af0bc1c..0b6800217c 100644 --- a/frontend/tests/unit/core/threads/message-merge.test.ts +++ b/frontend/tests/unit/core/threads/message-merge.test.ts @@ -2503,6 +2503,43 @@ test("reconnected turn order treats a content-only streaming text as a boundary" ).toEqual([streamingText, human, toolStep, laterStep]); }); +test("reconnected turn order does not treat Anthropic thinking blocks as a completed answer", () => { + const thinkingStep = { + id: "thinking-step", + type: "ai", + content: [{ type: "thinking", thinking: "Still analyzing" }], + run_id: "run-r", + } as unknown as Message; + const toolStep = { + id: "tool-step", + type: "ai", + content: "", + tool_calls: [{ id: "tc-1", name: "read_file", args: {} }], + run_id: "run-r", + } as unknown as Message; + const human = { + id: "human-r", + type: "human", + content: "Question", + } as Message; + const laterStep = { + id: "later", + type: "ai", + content: "", + tool_calls: [{ id: "tc-2", name: "write_file", args: {} }], + run_id: "run-r", + } as unknown as Message; + + expect( + restoreReconnectedTurnMessageOrder([ + thinkingStep, + toolStep, + human, + laterStep, + ]), + ).toEqual([human, thinkingStep, toolStep, laterStep]); +}); + test("rendered message ledger does not retain explicitly superseded messages", () => { const retained = { id: "retained-answer",