gsd-pi 2.74.0-dev.14c45ac → 2.74.0-dev.16f2f3b
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/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +70 -10
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- 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 +5 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.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 +57 -21
- 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-ordering.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js +38 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +13 -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 +48 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +84 -10
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +5 -1
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +72 -23
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-ordering.test.ts +44 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +68 -3
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- /package/dist/web/standalone/.next/static/{ZMKM0OI0CrTgzKWbgfPOg → C7qugsXHwdw4-b4ROHvOE}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{ZMKM0OI0CrTgzKWbgfPOg → C7qugsXHwdw4-b4ROHvOE}/_ssgManifest.js +0 -0
|
@@ -234,7 +234,7 @@ test("chat-controller renders serverToolUse before trailing text matching conten
|
|
|
234
234
|
assert.equal(host.chatContainer.children[1]?.constructor?.name, "AssistantMessageComponent");
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
-
test("chat-controller
|
|
237
|
+
test("chat-controller keeps pre-tool prose visible until post-tool prose arrives, then prunes it", async () => {
|
|
238
238
|
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
239
239
|
fg: (_key: string, text: string) => text,
|
|
240
240
|
bg: (_key: string, text: string) => text,
|
|
@@ -273,7 +273,7 @@ test("chat-controller drops provisional pre-tool text for claude-code MCP turns"
|
|
|
273
273
|
assert.equal(host.chatContainer.children.length, 1);
|
|
274
274
|
assert.equal(host.chatContainer.children[0]?.constructor?.name, "AssistantMessageComponent");
|
|
275
275
|
|
|
276
|
-
// MCP tool appears; provisional text should
|
|
276
|
+
// MCP tool appears; provisional text should remain visible until post-tool prose exists.
|
|
277
277
|
await handleAgentEvent(
|
|
278
278
|
host,
|
|
279
279
|
{
|
|
@@ -294,11 +294,16 @@ test("chat-controller drops provisional pre-tool text for claude-code MCP turns"
|
|
|
294
294
|
},
|
|
295
295
|
} as any,
|
|
296
296
|
);
|
|
297
|
-
assert.equal(host.chatContainer.children.length,
|
|
298
|
-
assert.equal(host.chatContainer.children[0]?.constructor?.name, "
|
|
297
|
+
assert.equal(host.chatContainer.children.length, 2, "pre-tool prose should remain during tool-only window");
|
|
298
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "AssistantMessageComponent");
|
|
299
|
+
assert.equal(host.chatContainer.children[1]?.constructor?.name, "ToolExecutionComponent");
|
|
299
300
|
|
|
300
|
-
//
|
|
301
|
-
const finalContent = [
|
|
301
|
+
// Post-tool prose arrives: pre-tool prose should now be pruned.
|
|
302
|
+
const finalContent = [
|
|
303
|
+
{ type: "text", text: "Let me inspect the workspace first." },
|
|
304
|
+
mcpTool,
|
|
305
|
+
{ type: "text", text: "Which missing feature matters most to you?" },
|
|
306
|
+
];
|
|
302
307
|
await handleAgentEvent(
|
|
303
308
|
host,
|
|
304
309
|
{
|
|
@@ -306,7 +311,7 @@ test("chat-controller drops provisional pre-tool text for claude-code MCP turns"
|
|
|
306
311
|
message: makeAssistant(finalContent),
|
|
307
312
|
assistantMessageEvent: {
|
|
308
313
|
type: "text_delta",
|
|
309
|
-
contentIndex:
|
|
314
|
+
contentIndex: 2,
|
|
310
315
|
delta: "Which missing feature matters most to you?",
|
|
311
316
|
partial: makeAssistant(finalContent),
|
|
312
317
|
},
|
|
@@ -320,6 +325,73 @@ test("chat-controller drops provisional pre-tool text for claude-code MCP turns"
|
|
|
320
325
|
await handleAgentEvent(host, { type: "message_end", message: makeAssistant(finalContent) } as any);
|
|
321
326
|
});
|
|
322
327
|
|
|
328
|
+
test("chat-controller keeps pre-tool thinking visible for claude-code MCP turns without post-tool prose", async () => {
|
|
329
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
330
|
+
fg: (_key: string, text: string) => text,
|
|
331
|
+
bg: (_key: string, text: string) => text,
|
|
332
|
+
bold: (text: string) => text,
|
|
333
|
+
italic: (text: string) => text,
|
|
334
|
+
truncate: (text: string) => text,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const host = createHost();
|
|
338
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
339
|
+
|
|
340
|
+
const mcpTool = {
|
|
341
|
+
type: "toolCall",
|
|
342
|
+
id: "mcp-tool-thinking-1",
|
|
343
|
+
name: "read",
|
|
344
|
+
mcpServer: "filesystem",
|
|
345
|
+
arguments: { filePath: "/tmp/demo.txt" },
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
349
|
+
|
|
350
|
+
const thinkingOnly = [{ type: "thinking", thinking: "I should inspect the workspace." }];
|
|
351
|
+
await handleAgentEvent(
|
|
352
|
+
host,
|
|
353
|
+
{
|
|
354
|
+
type: "message_update",
|
|
355
|
+
message: makeAssistant(thinkingOnly),
|
|
356
|
+
assistantMessageEvent: {
|
|
357
|
+
type: "thinking_delta",
|
|
358
|
+
contentIndex: 0,
|
|
359
|
+
delta: "I should inspect the workspace.",
|
|
360
|
+
partial: makeAssistant(thinkingOnly),
|
|
361
|
+
},
|
|
362
|
+
} as any,
|
|
363
|
+
);
|
|
364
|
+
assert.equal(host.chatContainer.children.length, 1);
|
|
365
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "AssistantMessageComponent");
|
|
366
|
+
|
|
367
|
+
await handleAgentEvent(
|
|
368
|
+
host,
|
|
369
|
+
{
|
|
370
|
+
type: "message_update",
|
|
371
|
+
message: makeAssistant([thinkingOnly[0], mcpTool]),
|
|
372
|
+
assistantMessageEvent: {
|
|
373
|
+
type: "toolcall_end",
|
|
374
|
+
contentIndex: 1,
|
|
375
|
+
toolCall: {
|
|
376
|
+
...mcpTool,
|
|
377
|
+
externalResult: {
|
|
378
|
+
content: [{ type: "text", text: "file preview" }],
|
|
379
|
+
details: {},
|
|
380
|
+
isError: false,
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
partial: makeAssistant([thinkingOnly[0], mcpTool]),
|
|
384
|
+
},
|
|
385
|
+
} as any,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
assert.equal(host.chatContainer.children.length, 2, "thinking should remain visible while only tool output is present");
|
|
389
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "AssistantMessageComponent");
|
|
390
|
+
assert.equal(host.chatContainer.children[1]?.constructor?.name, "ToolExecutionComponent");
|
|
391
|
+
|
|
392
|
+
await handleAgentEvent(host, { type: "message_end", message: makeAssistant([thinkingOnly[0], mcpTool]) } as any);
|
|
393
|
+
});
|
|
394
|
+
|
|
323
395
|
test("chat-controller prunes orphaned provisional text after claude-code sub-turn shrink when MCP tools appear", async () => {
|
|
324
396
|
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
325
397
|
fg: (_key: string, text: string) => text,
|
|
@@ -374,7 +446,7 @@ test("chat-controller prunes orphaned provisional text after claude-code sub-tur
|
|
|
374
446
|
);
|
|
375
447
|
assert.equal(host.chatContainer.children.length, 2, "shrink keeps prior text until MCP tool context appears");
|
|
376
448
|
|
|
377
|
-
// MCP tool appears in sub-turn 2:
|
|
449
|
+
// MCP tool appears in sub-turn 2: tool-only windows keep provisional prose visible.
|
|
378
450
|
await handleAgentEvent(
|
|
379
451
|
host,
|
|
380
452
|
{
|
|
@@ -395,8 +467,10 @@ test("chat-controller prunes orphaned provisional text after claude-code sub-tur
|
|
|
395
467
|
},
|
|
396
468
|
} as any,
|
|
397
469
|
);
|
|
398
|
-
assert.equal(host.chatContainer.children.length,
|
|
399
|
-
assert.equal(host.chatContainer.children[0]?.constructor?.name, "
|
|
470
|
+
assert.equal(host.chatContainer.children.length, 3, "stale text runs are deferred until post-tool prose arrives");
|
|
471
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "AssistantMessageComponent");
|
|
472
|
+
assert.equal(host.chatContainer.children[1]?.constructor?.name, "AssistantMessageComponent");
|
|
473
|
+
assert.equal(host.chatContainer.children[2]?.constructor?.name, "ToolExecutionComponent");
|
|
400
474
|
|
|
401
475
|
const finalContent = [mcpTool, { type: "text", text: "Final visible question?" }];
|
|
402
476
|
await handleAgentEvent(
|
|
@@ -89,6 +89,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
89
89
|
);
|
|
90
90
|
const hasTextContent = message.content.some((c) => c.type === "text" && c.text.trim().length > 0);
|
|
91
91
|
const hasToolContent = message.content.some((c) => c.type === "toolCall" || c.type === "serverToolUse");
|
|
92
|
+
// Claude Code often emits long reasoning blocks ahead of user-visible text/tool
|
|
93
|
+
// output in the same lifecycle. Keep chat output visible without requiring a
|
|
94
|
+
// manual thinking toggle every turn.
|
|
95
|
+
const shouldCapThinking = hasTextContent || hasToolContent || message.provider === "claude-code";
|
|
92
96
|
|
|
93
97
|
if (hasVisibleContent) {
|
|
94
98
|
this.contentContainer.addChild(new Spacer(1));
|
|
@@ -122,7 +126,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
122
126
|
});
|
|
123
127
|
// Keep visible chat output readable when thinking traces are long.
|
|
124
128
|
// Tool-bearing turns can stream text in a later assistant message.
|
|
125
|
-
if (
|
|
129
|
+
if (shouldCapThinking) {
|
|
126
130
|
thinkingMarkdown.maxLines = 8;
|
|
127
131
|
}
|
|
128
132
|
this.contentContainer.addChild(thinkingMarkdown);
|
|
@@ -16,7 +16,13 @@ let lastContentLength = 0;
|
|
|
16
16
|
|
|
17
17
|
// --- Segment walker state (per streaming assistant turn) ---
|
|
18
18
|
type RenderedSegment =
|
|
19
|
-
| {
|
|
19
|
+
| {
|
|
20
|
+
kind: "text-run";
|
|
21
|
+
startIndex: number;
|
|
22
|
+
endIndex: number;
|
|
23
|
+
contentType: "text" | "thinking";
|
|
24
|
+
component: AssistantMessageComponent;
|
|
25
|
+
}
|
|
20
26
|
| { kind: "tool"; contentIndex: number; component: ToolExecutionComponent };
|
|
21
27
|
|
|
22
28
|
let renderedSegments: RenderedSegment[] = [];
|
|
@@ -319,44 +325,75 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
319
325
|
}
|
|
320
326
|
return false;
|
|
321
327
|
});
|
|
322
|
-
const shouldDropPreToolText = isClaudeCodeProvider && hasMcpToolBlock;
|
|
323
328
|
const firstToolIdx = blocks.findIndex((b: any) => b.type === "toolCall" || b.type === "serverToolUse");
|
|
329
|
+
const hasPostToolText = firstToolIdx >= 0
|
|
330
|
+
&& blocks.some(
|
|
331
|
+
(b: any, idx: number) => (
|
|
332
|
+
idx > firstToolIdx
|
|
333
|
+
&& b?.type === "text"
|
|
334
|
+
&& typeof b?.text === "string"
|
|
335
|
+
&& b.text.trim().length > 0
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
// Only prune provisional pre-tool prose after post-tool prose exists,
|
|
339
|
+
// so MCP tool-only windows do not blank the assistant content.
|
|
340
|
+
const shouldDropPreToolProse = isClaudeCodeProvider && hasMcpToolBlock && hasPostToolText;
|
|
324
341
|
type DesiredSegment =
|
|
325
|
-
| { kind: "text-run"; startIndex: number; endIndex: number }
|
|
342
|
+
| { kind: "text-run"; startIndex: number; endIndex: number; contentType: "text" | "thinking" }
|
|
326
343
|
| { kind: "tool"; contentIndex: number; toolId: string };
|
|
327
344
|
const desired: DesiredSegment[] = [];
|
|
328
345
|
let runStart = -1;
|
|
346
|
+
let runEnd = -1;
|
|
347
|
+
let runType: "text" | "thinking" | undefined;
|
|
348
|
+
const closeRun = () => {
|
|
349
|
+
if (runStart !== -1 && runType) {
|
|
350
|
+
desired.push({ kind: "text-run", startIndex: runStart, endIndex: runEnd, contentType: runType });
|
|
351
|
+
runStart = -1;
|
|
352
|
+
runEnd = -1;
|
|
353
|
+
runType = undefined;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
329
356
|
for (let i = 0; i < blocks.length; i++) {
|
|
330
357
|
const b = blocks[i];
|
|
331
|
-
const
|
|
358
|
+
const blockType = b.type === "text" || b.type === "thinking" ? b.type : undefined;
|
|
359
|
+
const isTextLike = blockType === "text" || blockType === "thinking";
|
|
332
360
|
const isTool = b.type === "toolCall" || b.type === "serverToolUse";
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
361
|
+
// For Claude Code MCP turns, prune only pre-tool prose, never thinking.
|
|
362
|
+
const shouldSkipProse = shouldDropPreToolProse && firstToolIdx >= 0 && i < firstToolIdx && blockType === "text";
|
|
363
|
+
if (shouldSkipProse) {
|
|
364
|
+
closeRun();
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (isTextLike) {
|
|
368
|
+
if (runStart === -1) {
|
|
369
|
+
runStart = i;
|
|
370
|
+
runEnd = i;
|
|
371
|
+
runType = blockType;
|
|
372
|
+
} else if (runType !== blockType) {
|
|
373
|
+
closeRun();
|
|
374
|
+
runStart = i;
|
|
375
|
+
runEnd = i;
|
|
376
|
+
runType = blockType;
|
|
377
|
+
} else {
|
|
378
|
+
runEnd = i;
|
|
336
379
|
}
|
|
337
|
-
if (runStart === -1) runStart = i;
|
|
338
380
|
} else {
|
|
339
|
-
|
|
340
|
-
desired.push({ kind: "text-run", startIndex: runStart, endIndex: i - 1 });
|
|
341
|
-
runStart = -1;
|
|
342
|
-
}
|
|
381
|
+
closeRun();
|
|
343
382
|
if (isTool) {
|
|
344
383
|
desired.push({ kind: "tool", contentIndex: i, toolId: b.id });
|
|
345
384
|
}
|
|
346
385
|
}
|
|
347
386
|
}
|
|
348
|
-
|
|
349
|
-
desired.push({ kind: "text-run", startIndex: runStart, endIndex: blocks.length - 1 });
|
|
350
|
-
}
|
|
387
|
+
closeRun();
|
|
351
388
|
|
|
352
389
|
// Claude Code MCP can emit provisional pre-tool prose that gets
|
|
353
390
|
// superseded by post-tool output. Prune stale text-run segments so
|
|
354
391
|
// the final assistant output remains below tool output.
|
|
355
|
-
if (
|
|
392
|
+
if (shouldDropPreToolProse && firstToolIdx >= 0) {
|
|
356
393
|
if (orphanedSegments.length > 0) {
|
|
357
394
|
const remainingOrphans: RenderedSegment[] = [];
|
|
358
395
|
for (const orphan of orphanedSegments) {
|
|
359
|
-
if (orphan.kind === "text-run") {
|
|
396
|
+
if (orphan.kind === "text-run" && orphan.contentType === "text") {
|
|
360
397
|
host.chatContainer.removeChild(orphan.component);
|
|
361
398
|
if (host.streamingComponent === orphan.component) {
|
|
362
399
|
host.streamingComponent = undefined;
|
|
@@ -367,10 +404,10 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
367
404
|
}
|
|
368
405
|
orphanedSegments = remainingOrphans;
|
|
369
406
|
}
|
|
370
|
-
const
|
|
407
|
+
const desiredTextKeys = new Set(
|
|
371
408
|
desired
|
|
372
409
|
.filter((seg): seg is Extract<DesiredSegment, { kind: "text-run" }> => seg.kind === "text-run")
|
|
373
|
-
.map((seg) => seg.startIndex),
|
|
410
|
+
.map((seg) => `${seg.contentType}:${seg.startIndex}`),
|
|
374
411
|
);
|
|
375
412
|
const desiredToolIndices = new Set(
|
|
376
413
|
desired
|
|
@@ -379,7 +416,11 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
379
416
|
);
|
|
380
417
|
const nextRendered: RenderedSegment[] = [];
|
|
381
418
|
for (const seg of renderedSegments) {
|
|
382
|
-
if (
|
|
419
|
+
if (
|
|
420
|
+
seg.kind === "text-run"
|
|
421
|
+
&& seg.contentType === "text"
|
|
422
|
+
&& !desiredTextKeys.has(`${seg.contentType}:${seg.startIndex}`)
|
|
423
|
+
) {
|
|
383
424
|
host.chatContainer.removeChild(seg.component);
|
|
384
425
|
if (host.streamingComponent === seg.component) {
|
|
385
426
|
host.streamingComponent = undefined;
|
|
@@ -411,7 +452,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
411
452
|
} else {
|
|
412
453
|
// text-run segment
|
|
413
454
|
const existing = renderedSegments.find(
|
|
414
|
-
(s) => s.kind === "text-run" && s.startIndex === seg.startIndex,
|
|
455
|
+
(s) => s.kind === "text-run" && s.startIndex === seg.startIndex && s.contentType === seg.contentType,
|
|
415
456
|
);
|
|
416
457
|
if (!existing) {
|
|
417
458
|
const comp = new AssistantMessageComponent(
|
|
@@ -422,7 +463,13 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
422
463
|
{ startIndex: seg.startIndex, endIndex: seg.endIndex },
|
|
423
464
|
);
|
|
424
465
|
host.chatContainer.addChild(comp);
|
|
425
|
-
renderedSegments.push({
|
|
466
|
+
renderedSegments.push({
|
|
467
|
+
kind: "text-run",
|
|
468
|
+
startIndex: seg.startIndex,
|
|
469
|
+
endIndex: seg.endIndex,
|
|
470
|
+
contentType: seg.contentType,
|
|
471
|
+
component: comp,
|
|
472
|
+
});
|
|
426
473
|
host.streamingComponent = comp;
|
|
427
474
|
}
|
|
428
475
|
}
|
|
@@ -433,7 +480,9 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
433
480
|
for (const seg of renderedSegments) {
|
|
434
481
|
if (seg.kind === "text-run") {
|
|
435
482
|
// Find corresponding desired segment to get current endIndex
|
|
436
|
-
const d = desired.find(
|
|
483
|
+
const d = desired.find(
|
|
484
|
+
(ds) => ds.kind === "text-run" && ds.startIndex === seg.startIndex && ds.contentType === seg.contentType,
|
|
485
|
+
);
|
|
437
486
|
if (d && d.kind === "text-run" && d.endIndex !== seg.endIndex) {
|
|
438
487
|
seg.endIndex = d.endIndex;
|
|
439
488
|
seg.component.setRange({ startIndex: seg.startIndex, endIndex: seg.endIndex });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { buildAssistantReplaySegments } from "./interactive-mode.js";
|
|
5
|
+
|
|
6
|
+
test("buildAssistantReplaySegments preserves tool-first ordering", () => {
|
|
7
|
+
const segments = buildAssistantReplaySegments([
|
|
8
|
+
{ type: "toolCall", id: "t1", name: "read", arguments: {} },
|
|
9
|
+
{ type: "text", text: "Done." },
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
assert.deepEqual(segments, [
|
|
13
|
+
{ kind: "tool", contentIndex: 0 },
|
|
14
|
+
{ kind: "assistant", startIndex: 1, endIndex: 1 },
|
|
15
|
+
]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("buildAssistantReplaySegments preserves interleaved assistant-tool-assistant runs", () => {
|
|
19
|
+
const segments = buildAssistantReplaySegments([
|
|
20
|
+
{ type: "text", text: "Let me check." },
|
|
21
|
+
{ type: "serverToolUse", id: "s1", name: "mcp__fs__glob", input: {} },
|
|
22
|
+
{ type: "thinking", thinking: "Tool result looks good." },
|
|
23
|
+
{ type: "text", text: "Here is the answer." },
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
assert.deepEqual(segments, [
|
|
27
|
+
{ kind: "assistant", startIndex: 0, endIndex: 0 },
|
|
28
|
+
{ kind: "tool", contentIndex: 1 },
|
|
29
|
+
{ kind: "assistant", startIndex: 2, endIndex: 3 },
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("buildAssistantReplaySegments ignores non-rendered non-tool blocks", () => {
|
|
34
|
+
const segments = buildAssistantReplaySegments([
|
|
35
|
+
{ type: "text", text: "before" },
|
|
36
|
+
{ type: "webSearchResult", toolUseId: "s1", content: {} },
|
|
37
|
+
{ type: "text", text: "after" },
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
assert.deepEqual(segments, [
|
|
41
|
+
{ kind: "assistant", startIndex: 0, endIndex: 0 },
|
|
42
|
+
{ kind: "assistant", startIndex: 2, endIndex: 2 },
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
@@ -127,6 +127,45 @@ function isExpandable(obj: unknown): obj is Expandable {
|
|
|
127
127
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
export type AssistantReplaySegment =
|
|
131
|
+
| { kind: "assistant"; startIndex: number; endIndex: number }
|
|
132
|
+
| { kind: "tool"; contentIndex: number };
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build replay segments for historical assistant messages so rebuild paths
|
|
136
|
+
* preserve the original content[] ordering between assistant prose and tools.
|
|
137
|
+
*/
|
|
138
|
+
export function buildAssistantReplaySegments(contentBlocks: Array<any>): AssistantReplaySegment[] {
|
|
139
|
+
const segments: AssistantReplaySegment[] = [];
|
|
140
|
+
let runStart = -1;
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < contentBlocks.length; i++) {
|
|
143
|
+
const block = contentBlocks[i];
|
|
144
|
+
const isAssistantText = block?.type === "text" || block?.type === "thinking";
|
|
145
|
+
const isTool = block?.type === "toolCall" || block?.type === "serverToolUse";
|
|
146
|
+
|
|
147
|
+
if (isAssistantText) {
|
|
148
|
+
if (runStart === -1) runStart = i;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (runStart !== -1) {
|
|
153
|
+
segments.push({ kind: "assistant", startIndex: runStart, endIndex: i - 1 });
|
|
154
|
+
runStart = -1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (isTool) {
|
|
158
|
+
segments.push({ kind: "tool", contentIndex: i });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (runStart !== -1) {
|
|
163
|
+
segments.push({ kind: "assistant", startIndex: runStart, endIndex: contentBlocks.length - 1 });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return segments;
|
|
167
|
+
}
|
|
168
|
+
|
|
130
169
|
type CompactionQueuedMessage = {
|
|
131
170
|
text: string;
|
|
132
171
|
mode: "steer" | "followUp";
|
|
@@ -2201,9 +2240,30 @@ export class InteractiveMode {
|
|
|
2201
2240
|
for (const message of sessionContext.messages) {
|
|
2202
2241
|
// Assistant messages need special handling for tool calls
|
|
2203
2242
|
if (message.role === "assistant") {
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2243
|
+
const hasToolBlocks = message.content.some((c) => c.type === "toolCall" || c.type === "serverToolUse");
|
|
2244
|
+
if (!hasToolBlocks) {
|
|
2245
|
+
this.addMessageToChat(message);
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
const assistantSegments: AssistantMessageComponent[] = [];
|
|
2250
|
+
const replaySegments = buildAssistantReplaySegments(message.content);
|
|
2251
|
+
|
|
2252
|
+
for (const segment of replaySegments) {
|
|
2253
|
+
if (segment.kind === "assistant") {
|
|
2254
|
+
const assistantComponent = new AssistantMessageComponent(
|
|
2255
|
+
message,
|
|
2256
|
+
this.hideThinkingBlock,
|
|
2257
|
+
this.getMarkdownThemeWithSettings(),
|
|
2258
|
+
this.settingsManager.getTimestampFormat(),
|
|
2259
|
+
{ startIndex: segment.startIndex, endIndex: segment.endIndex },
|
|
2260
|
+
);
|
|
2261
|
+
this.chatContainer.addChild(assistantComponent);
|
|
2262
|
+
assistantSegments.push(assistantComponent);
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
const content = message.content[segment.contentIndex];
|
|
2207
2267
|
if (content.type === "toolCall") {
|
|
2208
2268
|
const component = new ToolExecutionComponent(
|
|
2209
2269
|
content.name,
|
|
@@ -2259,6 +2319,11 @@ export class InteractiveMode {
|
|
|
2259
2319
|
}
|
|
2260
2320
|
}
|
|
2261
2321
|
}
|
|
2322
|
+
|
|
2323
|
+
// Match streaming-mode behavior: show metadata once on the final
|
|
2324
|
+
// assistant prose segment for this message.
|
|
2325
|
+
const lastAssistantSegment = assistantSegments[assistantSegments.length - 1];
|
|
2326
|
+
lastAssistantSegment?.setShowMetadata(true);
|
|
2262
2327
|
} else if (message.role === "toolResult") {
|
|
2263
2328
|
// Match tool results to pending tool components
|
|
2264
2329
|
const component = this.pendingTools.get(message.toolCallId);
|