Skip to content

Commit d9812b8

Browse files
committed
fix(ui): preserve interleaved tool card pairing
(cherry picked from commit 5553d61)
1 parent cc5c691 commit d9812b8

2 files changed

Lines changed: 66 additions & 30 deletions

File tree

ui/src/ui/chat/tool-cards.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,47 @@ describe("tool-cards", () => {
8585
expect(cards[0]?.inputText).toContain('"mode": "preview"');
8686
});
8787

88+
it("pairs interleaved nameless tool results in content order", () => {
89+
const cards = extractToolCards(
90+
{
91+
role: "assistant",
92+
content: [
93+
{
94+
type: "tool_use",
95+
name: "browser.open",
96+
input: { url: "https://example.com/a" },
97+
},
98+
{
99+
type: "tool_result",
100+
name: "browser.open",
101+
text: "Opened A",
102+
},
103+
{
104+
type: "tool_use",
105+
name: "browser.open",
106+
input: { url: "https://example.com/b" },
107+
},
108+
{
109+
type: "tool_result",
110+
name: "browser.open",
111+
text: "Opened B",
112+
},
113+
],
114+
},
115+
"msg:ordered",
116+
);
117+
118+
expect(cards).toHaveLength(2);
119+
expect(cards[0]).toMatchObject({
120+
inputText: '{\n "url": "https://example.com/a"\n}',
121+
outputText: "Opened A",
122+
});
123+
expect(cards[1]).toMatchObject({
124+
inputText: '{\n "url": "https://example.com/b"\n}',
125+
outputText: "Opened B",
126+
});
127+
});
128+
88129
it("builds sidebar content with input and empty output status", () => {
89130
const [card] = extractToolCards(
90131
{

ui/src/ui/chat/tool-cards.ts

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -149,40 +149,35 @@ export function extractToolCards(message: unknown, prefix = "tool"): ToolCard[]
149149
["toolcall", "tool_call", "tooluse", "tool_use"].includes(kind) ||
150150
(typeof item.name === "string" &&
151151
(item.arguments != null || item.args != null || item.input != null));
152-
if (!isToolCall) {
152+
if (isToolCall) {
153+
const args = coerceArgs(item.arguments ?? item.args ?? item.input);
154+
cards.push({
155+
id: resolveToolCardId(item, m, index, prefix),
156+
name: (item.name as string) ?? "tool",
157+
args,
158+
inputText: serializeToolInput(args),
159+
});
153160
continue;
154161
}
155-
const args = coerceArgs(item.arguments ?? item.args ?? item.input);
156-
cards.push({
157-
id: resolveToolCardId(item, m, index, prefix),
158-
name: (item.name as string) ?? "tool",
159-
args,
160-
inputText: serializeToolInput(args),
161-
});
162-
}
163162

164-
for (let index = 0; index < content.length; index++) {
165-
const item = content[index] ?? {};
166-
const kind = (typeof item.type === "string" ? item.type : "").toLowerCase();
167-
if (kind !== "toolresult" && kind !== "tool_result") {
168-
continue;
163+
if (kind === "toolresult" || kind === "tool_result") {
164+
const name = typeof item.name === "string" ? item.name : "tool";
165+
const cardId = resolveToolCardId(item, m, index, prefix);
166+
const existing = findLatestCard(cards, cardId, name);
167+
const text = extractToolText(item);
168+
const preview = extractToolPreview(text, name);
169+
if (existing) {
170+
existing.outputText = text;
171+
existing.preview = preview;
172+
continue;
173+
}
174+
cards.push({
175+
id: cardId,
176+
name,
177+
outputText: text,
178+
preview,
179+
});
169180
}
170-
const name = typeof item.name === "string" ? item.name : "tool";
171-
const cardId = resolveToolCardId(item, m, index, prefix);
172-
const existing = findLatestCard(cards, cardId, name);
173-
const text = extractToolText(item);
174-
const preview = extractToolPreview(text, name);
175-
if (existing) {
176-
existing.outputText = text;
177-
existing.preview = preview;
178-
continue;
179-
}
180-
cards.push({
181-
id: cardId,
182-
name,
183-
outputText: text,
184-
preview,
185-
});
186181
}
187182

188183
const role = typeof m.role === "string" ? m.role.toLowerCase() : "";

0 commit comments

Comments
 (0)