lsd-pi 1.3.9 → 1.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/mcp-client/index.js +191 -83
- package/dist/resources/extensions/mcp-client/mcp-manager-component.js +220 -0
- package/dist/resources/extensions/slash-commands/plan.js +67 -13
- package/dist/resources/extensions/subagent/agents.js +7 -0
- package/dist/resources/extensions/subagent/index.js +25 -8
- package/dist/resources/extensions/subagent/model-resolution.js +1 -0
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +104 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +39 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +135 -18
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +21 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +146 -8
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +51 -13
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +75 -4
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +31 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +129 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +158 -18
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +163 -9
- package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +60 -13
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +86 -5
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +31 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/mcp-client/index.ts +212 -90
- package/src/resources/extensions/mcp-client/mcp-manager-component.ts +256 -0
- package/src/resources/extensions/mcp-client/tests/mcp-manager-component.test.ts +141 -0
- package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +18 -2
- package/src/resources/extensions/slash-commands/plan.ts +70 -13
- package/src/resources/extensions/subagent/agents.ts +9 -0
- package/src/resources/extensions/subagent/index.ts +30 -8
- package/src/resources/extensions/subagent/model-resolution.ts +1 -0
|
@@ -113,15 +113,19 @@ function createHost(options: { collapseToolCalls?: boolean; collapsedToolCallsEx
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
function addPendingTool(host: any, toolCallId: string, elapsed: number): { hidden?: boolean; component: any } {
|
|
116
|
-
const state: { hidden?: boolean; component: any } = { hidden: false, component: undefined };
|
|
116
|
+
const state: { hidden?: boolean; component: any; indented?: boolean } = { hidden: false, component: undefined, indented: false };
|
|
117
117
|
const component = {
|
|
118
118
|
render: () => (state.hidden ? [] : ["tool"]),
|
|
119
119
|
updateResult: () => { },
|
|
120
|
+
updateArgs: () => { },
|
|
120
121
|
setHidden: (hidden: boolean) => {
|
|
121
122
|
state.hidden = hidden;
|
|
122
123
|
},
|
|
123
124
|
isHidden: () => !!state.hidden,
|
|
124
125
|
setArgsComplete: () => { },
|
|
126
|
+
setIndented: (indented: boolean) => {
|
|
127
|
+
state.indented = indented;
|
|
128
|
+
},
|
|
125
129
|
getElapsed: () => elapsed,
|
|
126
130
|
};
|
|
127
131
|
state.component = component;
|
|
@@ -175,7 +179,7 @@ describe("chat-controller collapsed tool summary lifecycle", () => {
|
|
|
175
179
|
assert.equal(summaryLines(host).length, 1);
|
|
176
180
|
});
|
|
177
181
|
|
|
178
|
-
it("
|
|
182
|
+
it("keeps on-error tools visible when they fail", async () => {
|
|
179
183
|
const host = createHost();
|
|
180
184
|
|
|
181
185
|
await handleAgentEvent(host, {
|
|
@@ -186,7 +190,7 @@ describe("chat-controller collapsed tool summary lifecycle", () => {
|
|
|
186
190
|
await handleAgentEvent(host, toolStartEvent("tool-1", "bash", { command: "exit 1" }));
|
|
187
191
|
const pending = host.pendingTools.get("tool-1");
|
|
188
192
|
assert.ok(pending);
|
|
189
|
-
assert.equal(pending.isHidden(),
|
|
193
|
+
assert.equal(pending.isHidden(), false);
|
|
190
194
|
|
|
191
195
|
await handleAgentEvent(host, {
|
|
192
196
|
type: "tool_execution_end",
|
|
@@ -228,7 +232,7 @@ describe("chat-controller collapsed tool summary lifecycle", () => {
|
|
|
228
232
|
assert.ok(stripAnsi(summaries[0].render(160).join("\n")).includes("reading 2 files · 1.0s"));
|
|
229
233
|
});
|
|
230
234
|
|
|
231
|
-
it("
|
|
235
|
+
it("keeps last grouped tool label visible after completion", async () => {
|
|
232
236
|
const host = createHost();
|
|
233
237
|
|
|
234
238
|
await handleAgentEvent(host, {
|
|
@@ -236,19 +240,35 @@ describe("chat-controller collapsed tool summary lifecycle", () => {
|
|
|
236
240
|
message: assistantMessage(),
|
|
237
241
|
});
|
|
238
242
|
|
|
239
|
-
|
|
243
|
+
await handleAgentEvent(host, toolStartEvent("tool-1", "read", { path: "README.md" }));
|
|
244
|
+
await handleAgentEvent(host, toolEndEvent("tool-1", "read"));
|
|
245
|
+
|
|
246
|
+
const summaries = summaryLines(host);
|
|
247
|
+
assert.equal(summaries.length, 1);
|
|
248
|
+
assert.ok(stripAnsi(summaries[0].render(160).join("\n")).includes("└ README.md"));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("keeps non-groupable web fetches visible", async () => {
|
|
252
|
+
const host = createHost();
|
|
253
|
+
|
|
254
|
+
await handleAgentEvent(host, {
|
|
255
|
+
type: "message_start",
|
|
256
|
+
message: assistantMessage(),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const first = addPendingTool(host, "tool-1", 200);
|
|
240
260
|
await handleAgentEvent(host, toolEndEvent("tool-1", "fetch_page"));
|
|
241
261
|
|
|
242
|
-
addPendingTool(host, "tool-2", 300);
|
|
262
|
+
const second = addPendingTool(host, "tool-2", 300);
|
|
243
263
|
await handleAgentEvent(host, toolEndEvent("tool-2", "fetch_page"));
|
|
244
264
|
|
|
245
265
|
const summaries = summaryLines(host);
|
|
246
|
-
assert.equal(summaries.length,
|
|
247
|
-
assert.
|
|
248
|
-
assert.
|
|
266
|
+
assert.equal(summaries.length, 0);
|
|
267
|
+
assert.equal(first.hidden, false);
|
|
268
|
+
assert.equal(second.hidden, false);
|
|
249
269
|
});
|
|
250
270
|
|
|
251
|
-
it("
|
|
271
|
+
it("groups mixed collapsed file/nav tools into one summary", async () => {
|
|
252
272
|
const host = createHost();
|
|
253
273
|
|
|
254
274
|
await handleAgentEvent(host, {
|
|
@@ -263,9 +283,10 @@ describe("chat-controller collapsed tool summary lifecycle", () => {
|
|
|
263
283
|
await handleAgentEvent(host, toolEndEvent("tool-2", "find"));
|
|
264
284
|
|
|
265
285
|
const summaries = summaryLines(host);
|
|
266
|
-
assert.equal(summaries.length,
|
|
267
|
-
|
|
268
|
-
assert.ok(
|
|
286
|
+
assert.equal(summaries.length, 1);
|
|
287
|
+
const rendered = stripAnsi(summaries[0].render(160).join("\n"));
|
|
288
|
+
assert.ok(rendered.includes("reading 1 file"));
|
|
289
|
+
assert.ok(rendered.includes("finding 1 path"));
|
|
269
290
|
});
|
|
270
291
|
|
|
271
292
|
it("resets grouping after visible tool result", async () => {
|
|
@@ -345,6 +366,32 @@ describe("chat-controller collapsed tool summary lifecycle", () => {
|
|
|
345
366
|
assert.ok(stripAnsi(summaries[1].render(160).join("\n")).includes("0.4s"));
|
|
346
367
|
});
|
|
347
368
|
|
|
369
|
+
it("stops grouped spinner on aborted assistant message", async () => {
|
|
370
|
+
const host = createHost();
|
|
371
|
+
|
|
372
|
+
await handleAgentEvent(host, {
|
|
373
|
+
type: "message_start",
|
|
374
|
+
message: assistantMessage(),
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
await handleAgentEvent(host, toolStartEvent("tool-1", "read", { path: "README.md" }));
|
|
378
|
+
|
|
379
|
+
await handleAgentEvent(host, {
|
|
380
|
+
type: "message_end",
|
|
381
|
+
message: {
|
|
382
|
+
...assistantMessage(),
|
|
383
|
+
stopReason: "aborted",
|
|
384
|
+
errorMessage: "Operation aborted",
|
|
385
|
+
},
|
|
386
|
+
} as any);
|
|
387
|
+
|
|
388
|
+
const summaries = summaryLines(host);
|
|
389
|
+
assert.equal(summaries.length, 1);
|
|
390
|
+
const rendered = stripAnsi(summaries[0].render(160).join("\n"));
|
|
391
|
+
assert.ok(!rendered.includes("…"));
|
|
392
|
+
assert.ok(rendered.includes("README.md") || rendered === "");
|
|
393
|
+
});
|
|
394
|
+
|
|
348
395
|
it("keeps collapsed tool calls visible in intermediate mode", async () => {
|
|
349
396
|
const host = createHost({ collapseToolCalls: true, collapsedToolCallsExpanded: true });
|
|
350
397
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { Loader, Spacer, Text } from "@gsd/pi-tui";
|
|
2
|
+
import { Loader, Markdown, Spacer, Text } from "@gsd/pi-tui";
|
|
3
3
|
|
|
4
4
|
import type { InteractiveModeEvent, InteractiveModeStateHost } from "../interactive-mode-state.js";
|
|
5
5
|
import { theme } from "../theme/theme.js";
|
|
@@ -51,6 +51,14 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
51
51
|
host.collapsedToolSummaryLine = undefined;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
const clearPendingCollapsedToolSummaries = (): void => {
|
|
55
|
+
for (const child of host.chatContainer.children) {
|
|
56
|
+
if (child instanceof ToolSummaryLine && child.hasPendingTools()) {
|
|
57
|
+
child.clearPendingTools();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
54
62
|
// Tools that always render as their own visible row, never folded into a summary line
|
|
55
63
|
const ALWAYS_DIRECT_TOOLS = new Set([
|
|
56
64
|
"bash", "bg_shell",
|
|
@@ -94,14 +102,13 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
94
102
|
return undefined;
|
|
95
103
|
};
|
|
96
104
|
|
|
97
|
-
const
|
|
105
|
+
const findOrCreateSummaryForPending = (
|
|
98
106
|
toolName: string,
|
|
99
|
-
elapsed: number,
|
|
100
107
|
anchor?: { render: (width: number) => string[] },
|
|
101
108
|
): ToolSummaryLine => {
|
|
102
109
|
let summary = findAdjacentCollapsedToolSummary(toolName, anchor);
|
|
103
110
|
if (!summary) {
|
|
104
|
-
summary = new ToolSummaryLine();
|
|
111
|
+
summary = new ToolSummaryLine(host.ui);
|
|
105
112
|
summary.setHidden(host.collapsedToolCallsExpanded);
|
|
106
113
|
if (anchor) {
|
|
107
114
|
const anchorIndex = host.chatContainer.children.indexOf(anchor);
|
|
@@ -115,10 +122,47 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
115
122
|
}
|
|
116
123
|
}
|
|
117
124
|
host.collapsedToolSummaryLine = summary;
|
|
125
|
+
return summary;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const findCollapsedSummaryByPendingTool = (toolCallId: string): ToolSummaryLine | undefined => {
|
|
129
|
+
for (const child of host.chatContainer.children) {
|
|
130
|
+
if (child instanceof ToolSummaryLine && child.hasPendingTool(toolCallId)) {
|
|
131
|
+
return child;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const appendCollapsedToolSummary = (
|
|
138
|
+
toolName: string,
|
|
139
|
+
elapsed: number,
|
|
140
|
+
anchor?: { render: (width: number) => string[] },
|
|
141
|
+
): ToolSummaryLine => {
|
|
142
|
+
const summary = findOrCreateSummaryForPending(toolName, anchor);
|
|
118
143
|
summary.addTool(toolName, elapsed);
|
|
119
144
|
return summary;
|
|
120
145
|
};
|
|
121
146
|
|
|
147
|
+
const clearStreamingPostToolComponents = (): void => {
|
|
148
|
+
for (const entry of host.streamingPostToolComponents) {
|
|
149
|
+
host.chatContainer.removeChild(entry.component);
|
|
150
|
+
}
|
|
151
|
+
host.streamingPostToolComponents = [];
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Find the index of the last tool-type content block (toolCall or serverToolUse).
|
|
156
|
+
*/
|
|
157
|
+
const findLastToolBlockIndex = (content: Array<{ type: string }>): number => {
|
|
158
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
159
|
+
if (content[i].type === "toolCall" || content[i].type === "serverToolUse") {
|
|
160
|
+
return i;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return -1;
|
|
164
|
+
};
|
|
165
|
+
|
|
122
166
|
switch (event.type) {
|
|
123
167
|
case "session_state_changed":
|
|
124
168
|
switch (event.reason) {
|
|
@@ -128,6 +172,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
128
172
|
resetCollapsedToolSummary();
|
|
129
173
|
host.streamingComponent = undefined;
|
|
130
174
|
host.streamingMessage = undefined;
|
|
175
|
+
host.streamingPostToolComponents = [];
|
|
131
176
|
host.pendingTools.clear();
|
|
132
177
|
host.clearAgentPtyComponents();
|
|
133
178
|
host.pendingMessagesContainer.clear();
|
|
@@ -222,7 +267,15 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
222
267
|
if (host.streamingComponent && event.message.role === "assistant") {
|
|
223
268
|
host.streamingMessage = event.message;
|
|
224
269
|
host.streamingComponent.updateContent(host.streamingMessage);
|
|
225
|
-
|
|
270
|
+
|
|
271
|
+
// Remove previously added post-tool text components (they'll be re-created below)
|
|
272
|
+
clearStreamingPostToolComponents();
|
|
273
|
+
|
|
274
|
+
const lastToolIdx = findLastToolBlockIndex(host.streamingMessage.content);
|
|
275
|
+
|
|
276
|
+
for (let ci = 0; ci < host.streamingMessage.content.length; ci++) {
|
|
277
|
+
const content = host.streamingMessage.content[ci];
|
|
278
|
+
|
|
226
279
|
// Keep collapsed summary active across assistant text updates.
|
|
227
280
|
// Streamed message updates include all prior text blocks, so resetting
|
|
228
281
|
// here fragments one contiguous collapsed-tool group into many lines.
|
|
@@ -252,12 +305,15 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
252
305
|
if (adjacentSummary) {
|
|
253
306
|
component.setIndented(true);
|
|
254
307
|
}
|
|
308
|
+
const summary = findOrCreateSummaryForPending(content.name, component);
|
|
309
|
+
summary.addPendingTool(content.id, content.name, content.arguments ?? {});
|
|
255
310
|
}
|
|
256
311
|
component.setHidden(shouldHide);
|
|
257
312
|
host.pendingTools.set(content.id, component);
|
|
258
313
|
host.updateEditorExpandHint();
|
|
259
314
|
} else {
|
|
260
315
|
host.pendingTools.get(content.id)?.updateArgs(content.arguments);
|
|
316
|
+
findCollapsedSummaryByPendingTool(content.id)?.updatePendingToolArgs(content.id, content.arguments ?? {});
|
|
261
317
|
}
|
|
262
318
|
} else if (content.type === "serverToolUse") {
|
|
263
319
|
if (!host.pendingTools.has(content.id)) {
|
|
@@ -282,6 +338,8 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
282
338
|
if (adjacentSummary) {
|
|
283
339
|
component.setIndented(true);
|
|
284
340
|
}
|
|
341
|
+
const summary = findOrCreateSummaryForPending(content.name, component);
|
|
342
|
+
summary.addPendingTool(content.id, content.name, content.input ?? {});
|
|
285
343
|
}
|
|
286
344
|
component.setHidden(shouldHide);
|
|
287
345
|
host.pendingTools.set(content.id, component);
|
|
@@ -304,6 +362,22 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
304
362
|
});
|
|
305
363
|
}
|
|
306
364
|
}
|
|
365
|
+
} else if (content.type === "text" && content.text.trim() && ci > lastToolIdx && lastToolIdx >= 0) {
|
|
366
|
+
// Text blocks that appear AFTER tool blocks need to be rendered
|
|
367
|
+
// after the tool rows to preserve content order.
|
|
368
|
+
const mdTheme = host.getMarkdownThemeWithSettings();
|
|
369
|
+
const component = new Markdown(content.text.trim(), 1, 0, mdTheme);
|
|
370
|
+
host.chatContainer.addChild(component);
|
|
371
|
+
host.streamingPostToolComponents.push({ index: ci, component });
|
|
372
|
+
} else if (content.type === "thinking" && content.thinking.trim() && ci > lastToolIdx && lastToolIdx >= 0 && !host.hideThinkingBlock) {
|
|
373
|
+
// Thinking blocks after tool blocks — render in correct position.
|
|
374
|
+
const mdTheme = host.getMarkdownThemeWithSettings();
|
|
375
|
+
const component = new Markdown(content.thinking.trim(), 1, 0, mdTheme, {
|
|
376
|
+
color: (text: string) => theme.fg("thinkingText", text),
|
|
377
|
+
italic: true,
|
|
378
|
+
});
|
|
379
|
+
host.chatContainer.addChild(component);
|
|
380
|
+
host.streamingPostToolComponents.push({ index: ci, component });
|
|
307
381
|
}
|
|
308
382
|
}
|
|
309
383
|
host.ui.requestRender();
|
|
@@ -332,6 +406,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
332
406
|
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
|
333
407
|
}
|
|
334
408
|
host.pendingTools.clear();
|
|
409
|
+
clearPendingCollapsedToolSummaries();
|
|
335
410
|
} else {
|
|
336
411
|
host.playNotificationSoundOnAgentEnd = true;
|
|
337
412
|
for (const [, component] of host.pendingTools.entries()) {
|
|
@@ -375,6 +450,8 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
375
450
|
if (adjacentSummary) {
|
|
376
451
|
component.setIndented(true);
|
|
377
452
|
}
|
|
453
|
+
const summary = findOrCreateSummaryForPending(event.toolName, component);
|
|
454
|
+
summary.addPendingTool(event.toolCallId, event.toolName, event.args ?? {});
|
|
378
455
|
}
|
|
379
456
|
component.setHidden(shouldHide);
|
|
380
457
|
host.pendingTools.set(event.toolCallId, component);
|
|
@@ -428,11 +505,13 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
428
505
|
component.updateResult({ ...event.result, isError: event.isError });
|
|
429
506
|
const collapseToolCalls = host.settingsManager.getCollapseToolCalls?.() ?? false;
|
|
430
507
|
if (collapseToolCalls && shouldCollapse(event.toolName, event.isError) && !ALWAYS_DIRECT_TOOLS.has(event.toolName)) {
|
|
508
|
+
findCollapsedSummaryByPendingTool(event.toolCallId)?.removePendingTool(event.toolCallId);
|
|
431
509
|
appendCollapsedToolSummary(event.toolName, component.getElapsed(), component);
|
|
432
510
|
component.setHidden(!host.collapsedToolCallsExpanded);
|
|
433
511
|
component.setIndented(false);
|
|
434
512
|
host.updateEditorExpandHint();
|
|
435
513
|
} else {
|
|
514
|
+
findCollapsedSummaryByPendingTool(event.toolCallId)?.removePendingTool(event.toolCallId);
|
|
436
515
|
component.setHidden(false);
|
|
437
516
|
resetCollapsedToolSummary();
|
|
438
517
|
}
|
|
@@ -452,8 +531,10 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
452
531
|
host.chatContainer.removeChild(host.streamingComponent);
|
|
453
532
|
host.streamingComponent = undefined;
|
|
454
533
|
host.streamingMessage = undefined;
|
|
534
|
+
clearStreamingPostToolComponents();
|
|
455
535
|
}
|
|
456
536
|
host.pendingTools.clear();
|
|
537
|
+
clearPendingCollapsedToolSummaries();
|
|
457
538
|
resetCollapsedToolSummary();
|
|
458
539
|
// Update hint: show expand/collapse if tool outputs exist, else clear
|
|
459
540
|
host.defaultEditor.bottomHint = "";
|
|
@@ -26,6 +26,7 @@ export interface InteractiveModeStateHost {
|
|
|
26
26
|
workingMessages: string[];
|
|
27
27
|
streamingComponent?: any;
|
|
28
28
|
streamingMessage?: any;
|
|
29
|
+
streamingPostToolComponents: Array<{ index: number; component: any }>;
|
|
29
30
|
retryEscapeHandler?: () => void;
|
|
30
31
|
retryLoader?: any;
|
|
31
32
|
autoCompactionLoader?: any;
|
|
@@ -318,6 +318,7 @@ export class InteractiveMode {
|
|
|
318
318
|
// Streaming message tracking
|
|
319
319
|
private streamingComponent: AssistantMessageComponent | undefined = undefined;
|
|
320
320
|
private streamingMessage: AssistantMessage | undefined = undefined;
|
|
321
|
+
streamingPostToolComponents: Array<{ index: number; component: any }> = [];
|
|
321
322
|
|
|
322
323
|
// Tool execution tracking: toolCallId -> component
|
|
323
324
|
private pendingTools = new Map<string, ToolExecutionComponent>();
|
|
@@ -2773,9 +2774,24 @@ export class InteractiveMode {
|
|
|
2773
2774
|
for (const message of sessionContext.messages) {
|
|
2774
2775
|
// Assistant messages need special handling for tool calls
|
|
2775
2776
|
if (message.role === "assistant") {
|
|
2777
|
+
// Render content blocks in order to preserve correct visual ordering.
|
|
2778
|
+
// Text/thinking blocks before tools go into AssistantMessageComponent.
|
|
2779
|
+
// Tool blocks are added as separate ToolExecutionComponent instances.
|
|
2780
|
+
// Text/thinking blocks after tools are added as individual Markdown components.
|
|
2781
|
+
const lastToolIdx = (() => {
|
|
2782
|
+
for (let i = message.content.length - 1; i >= 0; i--) {
|
|
2783
|
+
if (message.content[i].type === "toolCall" || message.content[i].type === "serverToolUse") return i;
|
|
2784
|
+
}
|
|
2785
|
+
return -1;
|
|
2786
|
+
})();
|
|
2787
|
+
|
|
2788
|
+
// Phase 1: Render pre-tool text/thinking via AssistantMessageComponent
|
|
2776
2789
|
this.addMessageToChat(message);
|
|
2777
|
-
|
|
2778
|
-
|
|
2790
|
+
|
|
2791
|
+
// Phase 2: Render tool blocks and post-tool text/thinking in content order
|
|
2792
|
+
for (let ci = 0; ci < message.content.length; ci++) {
|
|
2793
|
+
const content = message.content[ci];
|
|
2794
|
+
|
|
2779
2795
|
if (content.type === "toolCall") {
|
|
2780
2796
|
const component = new ToolExecutionComponent(
|
|
2781
2797
|
content.name,
|
|
@@ -2837,6 +2853,19 @@ export class InteractiveMode {
|
|
|
2837
2853
|
// No result yet (aborted stream?) - show as pending
|
|
2838
2854
|
this.pendingTools.set(content.id, component);
|
|
2839
2855
|
}
|
|
2856
|
+
} else if (ci > lastToolIdx && lastToolIdx >= 0) {
|
|
2857
|
+
// Text/thinking blocks after tools — render as individual components
|
|
2858
|
+
// to maintain correct visual ordering relative to tool rows.
|
|
2859
|
+
if (content.type === "text" && content.text.trim()) {
|
|
2860
|
+
const md = new Markdown(content.text.trim(), 1, 0, this.getMarkdownThemeWithSettings());
|
|
2861
|
+
this.chatContainer.addChild(md);
|
|
2862
|
+
} else if (content.type === "thinking" && content.thinking.trim() && !this.hideThinkingBlock) {
|
|
2863
|
+
const md = new Markdown(content.thinking.trim(), 1, 0, this.getMarkdownThemeWithSettings(), {
|
|
2864
|
+
color: (text: string) => theme.fg("thinkingText", text),
|
|
2865
|
+
italic: true,
|
|
2866
|
+
});
|
|
2867
|
+
this.chatContainer.addChild(md);
|
|
2868
|
+
}
|
|
2840
2869
|
}
|
|
2841
2870
|
}
|
|
2842
2871
|
} else if (message.role === "toolResult") {
|