fix(tui): ordering of messages (#8144)

This commit is contained in:
Alex Hancock
2026-03-27 09:38:08 -04:00
committed by GitHub
parent 98bab02aa5
commit d596113e72
+124 -109
View File
@@ -17,6 +17,10 @@ import type {
ToolCallStatus, ToolCallStatus,
ToolKind, ToolKind,
Stream, Stream,
ContentChunk,
ToolCall,
ToolCallUpdate,
SessionUpdate,
} from "@agentclientprotocol/sdk"; } from "@agentclientprotocol/sdk";
import { ndJsonStream } from "@agentclientprotocol/sdk"; import { ndJsonStream } from "@agentclientprotocol/sdk";
import { GooseClient } from "@aaif/goose-acp"; import { GooseClient } from "@aaif/goose-acp";
@@ -31,11 +35,14 @@ interface PendingPermission {
resolve: (response: RequestPermissionResponse) => void; resolve: (response: RequestPermissionResponse) => void;
} }
type ResponseItem =
| (ContentChunk & { itemType: "content_chunk" })
| (ToolCall & { itemType: "tool_call" });
interface Turn { interface Turn {
userText: string; userText: string;
toolCalls: Map<string, ToolCallInfo>; responseItems: ResponseItem[];
toolCallOrder: string[]; toolCallsById: Map<string, number>; // maps toolCallId to index in responseItems
agentText: string;
} }
function isErrorStatus(status: string): boolean { function isErrorStatus(status: string): boolean {
@@ -330,45 +337,60 @@ function buildTurnBodyLines({
permissionIdx: number; permissionIdx: number;
toolCallsExpanded: boolean; toolCallsExpanded: boolean;
}): React.ReactNode[] { }): React.ReactNode[] {
const toolCallIds = turn.toolCallOrder;
const toolCalls = turn.toolCalls;
const lines: React.ReactNode[] = []; const lines: React.ReactNode[] = [];
lines.push(<Box key="gap-top" height={1} />); lines.push(<Box key="gap-top" height={1} />);
for (let i = 0; i < toolCallIds.length; i++) { let toolCallIndex = 0;
const tcId = toolCallIds[i]!; let textChunkIndex = 0;
const tc = toolCalls.get(tcId);
if (!tc) continue;
if (toolCallsExpanded) { // Render items in the order they arrived
const cardLines = buildToolCallCardLines(tc, CONTENT_INDENT, width, true, `tc-${tcId}`); for (let i = 0; i < turn.responseItems.length; i++) {
lines.push(...cardLines); const item = turn.responseItems[i]!;
} else {
const compactLines = ToolCallCompact({
info: tc,
indent: CONTENT_INDENT,
width,
keyPrefix: `tc-${tcId}`,
showTabHint: i === 0,
});
lines.push(...compactLines);
}
}
if (turn.agentText) { if (item.itemType === "tool_call") {
if (toolCallIds.length > 0) { const tcId = item.toolCallId;
lines.push(<Box key="gap-agent" height={1} />); const info: ToolCallInfo = {
} toolCallId: item.toolCallId,
const rendered = renderMarkdown(turn.agentText); title: item.title,
const mdLines = rendered.split("\n"); status: item.status ?? "pending",
for (let i = 0; i < mdLines.length; i++) { kind: item.kind,
lines.push( rawInput: item.rawInput,
<Box key={`md-${i}`} paddingLeft={CONTENT_INDENT}> rawOutput: item.rawOutput,
<Text>{mdLines[i]}</Text> content: item.content,
</Box>, locations: item.locations,
); };
if (toolCallsExpanded) {
const cardLines = buildToolCallCardLines(info, CONTENT_INDENT, width, true, `tc-${tcId}`);
lines.push(...cardLines);
} else {
const compactLines = ToolCallCompact({
info,
indent: CONTENT_INDENT,
width,
keyPrefix: `tc-${tcId}`,
showTabHint: toolCallIndex === 0,
});
lines.push(...compactLines);
}
toolCallIndex++;
} else if (item.itemType === "content_chunk") {
if (item.content.type === "text") {
const text = item.content.text;
if (text) {
const rendered = renderMarkdown(text);
const mdLines = rendered.split("\n");
for (let j = 0; j < mdLines.length; j++) {
lines.push(
<Box key={`text-${textChunkIndex}-${j}`} paddingLeft={CONTENT_INDENT}>
<Text>{mdLines[j]}</Text>
</Box>,
);
}
textChunkIndex++;
}
}
} }
} }
@@ -597,43 +619,55 @@ function App({
setTurns((prev) => { setTurns((prev) => {
if (prev.length === 0) return prev; if (prev.length === 0) return prev;
const last = { ...prev[prev.length - 1]! }; const last = { ...prev[prev.length - 1]! };
last.agentText = last.agentText + text; const newItems = [...last.responseItems];
return [...prev.slice(0, -1), last];
// If last item is a content chunk with text, append to it; otherwise create new content chunk
if (newItems.length > 0 && newItems[newItems.length - 1]!.itemType === "content_chunk") {
const lastItem = newItems[newItems.length - 1] as ContentChunk & { itemType: "content_chunk" };
if (lastItem.content.type === "text") {
newItems[newItems.length - 1] = {
...lastItem,
content: {
...lastItem.content,
text: lastItem.content.text + text,
},
};
} else {
// Last item is not text, create new content chunk
newItems.push({
itemType: "content_chunk",
content: { type: "text", text },
});
}
} else {
// No items or last item is tool call, create new content chunk
newItems.push({
itemType: "content_chunk",
content: { type: "text", text },
});
}
return [...prev.slice(0, -1), { ...last, responseItems: newItems }];
}); });
}, []); }, []);
const handleToolCall = useCallback( const handleToolCall = useCallback(
(tc: { (tc: ToolCall) => {
toolCallId: string;
title: string;
status?: ToolCallStatus;
kind?: ToolKind;
rawInput?: unknown;
rawOutput?: unknown;
content?: ToolCallContent[];
locations?: Array<{ path: string; line?: number | null }>;
}) => {
setTurns((prev) => { setTurns((prev) => {
if (prev.length === 0) return prev; if (prev.length === 0) return prev;
const last = { ...prev[prev.length - 1]! }; const last = { ...prev[prev.length - 1]! };
const newMap = new Map(last.toolCalls);
const info: ToolCallInfo = { const newItems = [...last.responseItems];
toolCallId: tc.toolCallId, const newById = new Map(last.toolCallsById);
title: tc.title,
status: tc.status ?? "pending", // Add new tool call to the array
kind: tc.kind, const index = newItems.length;
rawInput: tc.rawInput, newItems.push({ ...tc, itemType: "tool_call" });
rawOutput: tc.rawOutput, newById.set(tc.toolCallId, index);
content: tc.content,
locations: tc.locations,
};
newMap.set(tc.toolCallId, info);
const newOrder = last.toolCallOrder.includes(tc.toolCallId)
? last.toolCallOrder
: [...last.toolCallOrder, tc.toolCallId];
return [ return [
...prev.slice(0, -1), ...prev.slice(0, -1),
{ ...last, toolCalls: newMap, toolCallOrder: newOrder }, { ...last, responseItems: newItems, toolCallsById: newById },
]; ];
}); });
}, },
@@ -641,33 +675,30 @@ function App({
); );
const handleToolCallUpdate = useCallback( const handleToolCallUpdate = useCallback(
(update: { (update: ToolCallUpdate) => {
toolCallId: string;
title?: string | null;
status?: ToolCallStatus | null;
kind?: ToolKind | null;
rawInput?: unknown;
rawOutput?: unknown;
content?: ToolCallContent[] | null;
locations?: Array<{ path: string; line?: number | null }> | null;
}) => {
setTurns((prev) => { setTurns((prev) => {
if (prev.length === 0) return prev; if (prev.length === 0) return prev;
const last = { ...prev[prev.length - 1]! }; const last = { ...prev[prev.length - 1]! };
const newMap = new Map(last.toolCalls);
const existing = newMap.get(update.toolCallId); const index = last.toolCallsById.get(update.toolCallId);
if (!existing) return prev; if (index === undefined) return prev;
const updated: ToolCallInfo = { ...existing };
const item = last.responseItems[index];
if (!item || item.itemType !== "tool_call") return prev;
const updated: ToolCall & { itemType: "tool_call" } = { ...item };
if (update.title != null) updated.title = update.title; if (update.title != null) updated.title = update.title;
if (update.status != null) updated.status = update.status; if (update.status != null) updated.status = update.status;
if (update.kind != null) updated.kind = update.kind; if (update.kind != null) updated.kind = update.kind;
if (update.rawInput !== undefined) updated.rawInput = update.rawInput; if (update.rawInput !== undefined) updated.rawInput = update.rawInput;
if (update.rawOutput !== undefined) if (update.rawOutput !== undefined) updated.rawOutput = update.rawOutput;
updated.rawOutput = update.rawOutput;
if (update.content != null) updated.content = update.content; if (update.content != null) updated.content = update.content;
if (update.locations != null) updated.locations = update.locations; if (update.locations != null) updated.locations = update.locations;
newMap.set(update.toolCallId, updated);
return [...prev.slice(0, -1), { ...last, toolCalls: newMap }]; const newItems = [...last.responseItems];
newItems[index] = updated;
return [...prev.slice(0, -1), { ...last, responseItems: newItems }];
}); });
}, },
[], [],
@@ -678,9 +709,8 @@ function App({
...prev, ...prev,
{ {
userText: text, userText: text,
toolCalls: new Map(), responseItems: [],
toolCallOrder: [], toolCallsById: new Map(),
agentText: "",
}, },
]); ]);
setViewTurnIdx(-1); setViewTurnIdx(-1);
@@ -778,27 +808,9 @@ function App({
appendAgent(update.content.text); appendAgent(update.content.text);
} }
} else if (update.sessionUpdate === "tool_call") { } else if (update.sessionUpdate === "tool_call") {
handleToolCall({ handleToolCall(update);
toolCallId: update.toolCallId,
title: update.title,
status: update.status,
kind: update.kind,
rawInput: update.rawInput,
rawOutput: update.rawOutput,
content: update.content,
locations: update.locations,
});
} else if (update.sessionUpdate === "tool_call_update") { } else if (update.sessionUpdate === "tool_call_update") {
handleToolCallUpdate({ handleToolCallUpdate(update);
toolCallId: update.toolCallId,
title: update.title,
status: update.status,
kind: update.kind,
rawInput: update.rawInput,
rawOutput: update.rawOutput,
content: update.content,
locations: update.locations,
});
} }
}, },
requestPermission: async ( requestPermission: async (
@@ -931,7 +943,11 @@ function App({
const effectiveIdx = const effectiveIdx =
viewTurnIdx === -1 ? turns.length - 1 : viewTurnIdx; viewTurnIdx === -1 ? turns.length - 1 : viewTurnIdx;
const currentTurn = turns[effectiveIdx]; const currentTurn = turns[effectiveIdx];
if (!currentTurn || currentTurn.toolCallOrder.length === 0) return; if (!currentTurn) return;
// Check if there are any tool calls in the response items
const hasToolCalls = currentTurn.responseItems.some(item => item.itemType === "tool_call");
if (!hasToolCalls) return;
setToolCallsExpanded((prev) => !prev); setToolCallsExpanded((prev) => !prev);
return; return;
@@ -1003,9 +1019,8 @@ function App({
const emptyTurn: Turn = { const emptyTurn: Turn = {
userText: "", userText: "",
toolCalls: new Map(), responseItems: [],
toolCallOrder: [], toolCallsById: new Map(),
agentText: "",
}; };
const bodyLines = buildTurnBodyLines({ const bodyLines = buildTurnBodyLines({