Fix duplicate reasoning display

This commit is contained in:
Xubin Ren 2026-05-17 17:11:38 +08:00
parent e5be4dac7a
commit 9340567f2d
2 changed files with 88 additions and 0 deletions

View File

@ -49,12 +49,47 @@ export function buildDisplayUnits(messages: UIMessage[]): DisplayUnit[] {
out.push({ type: "cluster", messages: cluster });
continue;
}
const previous = out[out.length - 1];
if (previous?.type === "cluster" && assistantHasInlineReasoning(m)) {
previous.messages.push(reasoningOnlyMessageFromAnswer(m));
out.push({ type: "single", message: stripInlineReasoning(m) });
i += 1;
continue;
}
out.push({ type: "single", message: m });
i += 1;
}
return out;
}
function assistantHasInlineReasoning(message: UIMessage): boolean {
return (
message.role === "assistant"
&& message.kind !== "trace"
&& message.content.trim().length > 0
&& (!!message.reasoning?.trim() || !!message.reasoningStreaming)
);
}
function reasoningOnlyMessageFromAnswer(message: UIMessage): UIMessage {
return {
id: `${message.id}-reasoning`,
role: "assistant",
content: "",
createdAt: message.createdAt,
reasoning: message.reasoning,
reasoningStreaming: message.reasoningStreaming,
isStreaming: message.reasoningStreaming,
};
}
function stripInlineReasoning(message: UIMessage): UIMessage {
const next = { ...message };
delete next.reasoning;
delete next.reasoningStreaming;
return next;
}
export function assistantCopyFlags(units: DisplayUnit[]): boolean[] {
const flags = new Array<boolean>(units.length).fill(true);
let hasLaterUnitBeforeUser = false;

View File

@ -55,6 +55,59 @@ describe("ThreadMessages", () => {
expect(rows[1]).toHaveClass("mt-4");
});
it("folds final answer reasoning into the preceding activity cluster", () => {
const messages: UIMessage[] = [
{
id: "r1",
role: "assistant",
content: "",
reasoning: "search plan",
reasoningStreaming: false,
createdAt: 1,
},
{
id: "t1",
role: "tool",
kind: "trace",
content: "web_search()",
traces: ["web_search()"],
createdAt: 2,
},
{
id: "a1",
role: "assistant",
content: "final answer",
reasoning: "summarize results",
reasoningStreaming: false,
createdAt: 3,
},
];
const units = buildDisplayUnits(messages);
expect(units).toHaveLength(2);
expect(units[0]).toMatchObject({ type: "cluster" });
expect(units[0].type === "cluster" ? units[0].messages.map((m) => m.id) : []).toEqual([
"r1",
"t1",
"a1-reasoning",
]);
expect(units[1]).toMatchObject({
type: "single",
message: {
id: "a1",
content: "final answer",
},
});
if (units[1].type === "single") {
expect(units[1].message).not.toHaveProperty("reasoning");
}
render(<ThreadMessages messages={messages} isStreaming={false} />);
expect(screen.queryByRole("button", { name: /^thinking$/i })).not.toBeInTheDocument();
expect(screen.getByText("final answer")).toBeInTheDocument();
});
it("shows copy only on the last assistant slice before the next user turn", () => {
const messages: UIMessage[] = [
{