Merge pull request #4121 from jeremymcs/fix/4120-pinned-output-duplication
fix(tui): stop pinned latest-output from duplicating streaming text
This commit is contained in:
commit
3adafde442
2 changed files with 94 additions and 9 deletions
|
|
@ -0,0 +1,71 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { findLatestPinnableText } from "./chat-controller.js";
|
||||
|
||||
test("findLatestPinnableText: empty content returns empty string", () => {
|
||||
assert.equal(findLatestPinnableText([]), "");
|
||||
});
|
||||
|
||||
test("findLatestPinnableText: no tool calls returns empty string", () => {
|
||||
const blocks = [
|
||||
{ type: "text", text: "hello" },
|
||||
{ type: "text", text: "world" },
|
||||
];
|
||||
assert.equal(findLatestPinnableText(blocks), "");
|
||||
});
|
||||
|
||||
test("findLatestPinnableText: returns text preceding a tool call", () => {
|
||||
const blocks = [
|
||||
{ type: "text", text: "doing the thing" },
|
||||
{ type: "toolCall", id: "1", name: "Read" },
|
||||
];
|
||||
assert.equal(findLatestPinnableText(blocks), "doing the thing");
|
||||
});
|
||||
|
||||
test("findLatestPinnableText: ignores trailing streaming text after the last tool call (regression: pinned mirror duplicated chat-container tokens)", () => {
|
||||
const blocks = [
|
||||
{ type: "text", text: "first prose" },
|
||||
{ type: "toolCall", id: "1", name: "Read" },
|
||||
{ type: "text", text: "second prose still streaming" },
|
||||
];
|
||||
assert.equal(findLatestPinnableText(blocks), "first prose");
|
||||
});
|
||||
|
||||
test("findLatestPinnableText: with multiple tools, picks text before the most recent tool call", () => {
|
||||
const blocks = [
|
||||
{ type: "text", text: "first" },
|
||||
{ type: "toolCall", id: "1", name: "Read" },
|
||||
{ type: "text", text: "second" },
|
||||
{ type: "toolCall", id: "2", name: "Grep" },
|
||||
{ type: "text", text: "third streaming" },
|
||||
];
|
||||
assert.equal(findLatestPinnableText(blocks), "second");
|
||||
});
|
||||
|
||||
test("findLatestPinnableText: treats serverToolUse the same as toolCall", () => {
|
||||
const blocks = [
|
||||
{ type: "text", text: "before web search" },
|
||||
{ type: "serverToolUse", id: "ws1", name: "web_search" },
|
||||
{ type: "text", text: "answer streaming" },
|
||||
];
|
||||
assert.equal(findLatestPinnableText(blocks), "before web search");
|
||||
});
|
||||
|
||||
test("findLatestPinnableText: skips empty/whitespace-only text blocks", () => {
|
||||
const blocks = [
|
||||
{ type: "text", text: "real prose" },
|
||||
{ type: "text", text: " " },
|
||||
{ type: "text", text: "" },
|
||||
{ type: "toolCall", id: "1", name: "Read" },
|
||||
];
|
||||
assert.equal(findLatestPinnableText(blocks), "real prose");
|
||||
});
|
||||
|
||||
test("findLatestPinnableText: thinking blocks are not pinnable", () => {
|
||||
const blocks = [
|
||||
{ type: "thinking", thinking: "internal" },
|
||||
{ type: "toolCall", id: "1", name: "Read" },
|
||||
];
|
||||
assert.equal(findLatestPinnableText(blocks), "");
|
||||
});
|
||||
|
|
@ -22,6 +22,28 @@ function hasAssistantToolBlocks(message: { content: Array<any> }): boolean {
|
|||
return message.content.some((c) => c.type === "toolCall" || c.type === "serverToolUse");
|
||||
}
|
||||
|
||||
// Pick the latest non-empty text block that appears strictly before the most
|
||||
// recent tool call. Text blocks that come after the last tool call are still
|
||||
// streaming live into the chat container, so mirroring them into the pinned
|
||||
// "Latest Output" zone would render the same tokens twice.
|
||||
export function findLatestPinnableText(contentBlocks: Array<any>): string {
|
||||
let lastToolIdx = -1;
|
||||
for (let i = contentBlocks.length - 1; i >= 0; i--) {
|
||||
const c = contentBlocks[i];
|
||||
if (c?.type === "toolCall" || c?.type === "serverToolUse") {
|
||||
lastToolIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let i = lastToolIdx - 1; i >= 0; i--) {
|
||||
const c = contentBlocks[i];
|
||||
if (c?.type === "text" && typeof c.text === "string" && c.text.trim()) {
|
||||
return c.text.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Tracks the latest assistant text for the pinned message zone
|
||||
let lastPinnedText = "";
|
||||
// Whether any tool execution has been added in this assistant turn (triggers pinned display)
|
||||
|
|
@ -286,15 +308,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|||
if (hasTools) hasToolsInTurn = true;
|
||||
|
||||
if (hasToolsInTurn) {
|
||||
// Collect the latest text block(s) from the assistant message
|
||||
let latestText = "";
|
||||
for (let i = contentBlocks.length - 1; i >= 0; i--) {
|
||||
const c = contentBlocks[i] as any;
|
||||
if (c.type === "text" && c.text?.trim()) {
|
||||
latestText = c.text.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
const latestText = findLatestPinnableText(contentBlocks);
|
||||
|
||||
if (latestText && latestText !== lastPinnedText) {
|
||||
lastPinnedText = latestText;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue