gsd-pi 2.73.0-dev.e1c09f2 → 2.73.1-dev.088d28f
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/resource-loader.js +2 -2
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +9 -3
- package/dist/resources/extensions/gsd/auto/phases.js +15 -9
- package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +54 -11
- package/dist/resources/extensions/gsd/auto-start.js +23 -6
- package/dist/resources/extensions/gsd/auto.js +13 -1
- package/dist/resources/extensions/gsd/bootstrap/crash-log.js +31 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -7
- package/dist/resources/extensions/gsd/commands-handlers.js +8 -2
- package/dist/resources/extensions/gsd/crash-recovery.js +51 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +36 -2
- package/dist/resources/extensions/gsd/milestone-actions.js +19 -1
- package/dist/resources/extensions/gsd/notification-widget.js +2 -2
- package/dist/resources/extensions/gsd/preferences-models.js +43 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +22 -0
- package/dist/resources/extensions/gsd/state.js +36 -0
- package/dist/update-check.d.ts +1 -0
- package/dist/update-check.js +13 -5
- package/dist/update-cmd.js +4 -3
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- 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/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 +11 -11
- 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/package.json +1 -1
- package/packages/pi-ai/dist/index.d.ts +1 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +1 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/utils/overflow.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/overflow.js +12 -0
- package/packages/pi-ai/dist/utils/overflow.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts +2 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.js +50 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.js.map +1 -0
- package/packages/pi-ai/src/index.ts +4 -0
- package/packages/pi-ai/src/utils/overflow.ts +14 -1
- package/packages/pi-ai/src/utils/tests/overflow.test.ts +58 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +313 -8
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/utils.js +5 -5
- package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.js +45 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +12 -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 +51 -26
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -0
- 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 +94 -16
- 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.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +11 -3
- 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/core/chat-controller-ordering.test.ts +355 -8
- package/packages/pi-coding-agent/src/core/compaction/utils.ts +5 -5
- package/packages/pi-coding-agent/src/core/compaction-utils.test.ts +50 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +62 -26
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +113 -21
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +11 -3
- package/packages/pi-tui/dist/__tests__/tui.test.js +60 -1
- package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts +8 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +32 -3
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/__tests__/tui.test.ts +76 -1
- package/packages/pi-tui/src/tui.ts +31 -3
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +12 -4
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +23 -2
- package/src/resources/extensions/gsd/auto/phases.ts +22 -9
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
- package/src/resources/extensions/gsd/auto-model-selection.ts +85 -11
- package/src/resources/extensions/gsd/auto-start.ts +30 -6
- package/src/resources/extensions/gsd/auto.ts +10 -0
- package/src/resources/extensions/gsd/bootstrap/crash-log.ts +32 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -7
- package/src/resources/extensions/gsd/commands-handlers.ts +8 -2
- package/src/resources/extensions/gsd/crash-recovery.ts +59 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/src/resources/extensions/gsd/gsd-db.ts +52 -2
- package/src/resources/extensions/gsd/milestone-actions.ts +19 -1
- package/src/resources/extensions/gsd/notification-widget.ts +2 -2
- package/src/resources/extensions/gsd/preferences-models.ts +41 -0
- package/src/resources/extensions/gsd/preferences-types.ts +12 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +23 -0
- package/src/resources/extensions/gsd/state.ts +46 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +51 -2
- package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +235 -0
- package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +137 -1
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +59 -1
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +91 -2
- package/src/resources/extensions/gsd/tests/park-milestone.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
- /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → nwYTvJZ1-hZIfw98d9Wfg}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → nwYTvJZ1-hZIfw98d9Wfg}/_ssgManifest.js +0 -0
|
@@ -94,7 +94,7 @@ function createHost() {
|
|
|
94
94
|
return host;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
test("chat-controller
|
|
97
|
+
test("chat-controller renders content blocks in content[] index order (tool-first stream)", async () => {
|
|
98
98
|
// ToolExecutionComponent uses the global theme singleton.
|
|
99
99
|
// Install a minimal no-op theme implementation for this unit test.
|
|
100
100
|
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
@@ -116,7 +116,6 @@ test("chat-controller keeps tool output ahead of delayed assistant text for exte
|
|
|
116
116
|
|
|
117
117
|
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
118
118
|
|
|
119
|
-
assert.equal(host.streamingComponent, undefined, "assistant component should be deferred at message_start");
|
|
120
119
|
assert.equal(host.chatContainer.children.length, 0, "nothing should render before content arrives");
|
|
121
120
|
|
|
122
121
|
await handleAgentEvent(
|
|
@@ -140,11 +139,10 @@ test("chat-controller keeps tool output ahead of delayed assistant text for exte
|
|
|
140
139
|
} as any,
|
|
141
140
|
);
|
|
142
141
|
|
|
143
|
-
|
|
142
|
+
// content[0] = toolCall → ToolExecutionComponent renders first
|
|
144
143
|
assert.equal(host.chatContainer.children.length, 1, "tool execution block should render immediately");
|
|
145
144
|
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
146
145
|
|
|
147
|
-
// Re-assert required host method before the text-bearing update path.
|
|
148
146
|
host.getMarkdownThemeWithSettings = () => ({});
|
|
149
147
|
|
|
150
148
|
await handleAgentEvent(
|
|
@@ -161,12 +159,13 @@ test("chat-controller keeps tool output ahead of delayed assistant text for exte
|
|
|
161
159
|
} as any,
|
|
162
160
|
);
|
|
163
161
|
|
|
164
|
-
|
|
162
|
+
// content[0]=toolCall, content[1]=text → order: tool, then text
|
|
163
|
+
assert.equal(host.chatContainer.children.length, 2, "text run should render after tool in content[] order");
|
|
165
164
|
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
166
165
|
assert.equal(host.chatContainer.children[1]?.constructor?.name, "AssistantMessageComponent");
|
|
167
166
|
});
|
|
168
167
|
|
|
169
|
-
test("chat-controller
|
|
168
|
+
test("chat-controller renders serverToolUse before trailing text matching content[] index order", async () => {
|
|
170
169
|
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
171
170
|
fg: (_key: string, text: string) => text,
|
|
172
171
|
bg: (_key: string, text: string) => text,
|
|
@@ -199,7 +198,7 @@ test("chat-controller keeps serverToolUse output ahead of assistant text when ex
|
|
|
199
198
|
} as any,
|
|
200
199
|
);
|
|
201
200
|
|
|
202
|
-
|
|
201
|
+
// content[0] = serverToolUse → ToolExecutionComponent renders first
|
|
203
202
|
assert.equal(host.chatContainer.children.length, 1, "server tool block should render immediately");
|
|
204
203
|
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
205
204
|
|
|
@@ -229,7 +228,8 @@ test("chat-controller keeps serverToolUse output ahead of assistant text when ex
|
|
|
229
228
|
} as any,
|
|
230
229
|
);
|
|
231
230
|
|
|
232
|
-
|
|
231
|
+
// content[0]=serverToolUse, content[1]=text → order: tool, then text
|
|
232
|
+
assert.equal(host.chatContainer.children.length, 2, "text run should render after server tool in content[] order");
|
|
233
233
|
assert.equal(host.chatContainer.children[0]?.constructor?.name, "ToolExecutionComponent");
|
|
234
234
|
assert.equal(host.chatContainer.children[1]?.constructor?.name, "AssistantMessageComponent");
|
|
235
235
|
});
|
|
@@ -466,3 +466,350 @@ test("chat-controller does not pin when there are no tool calls", async () => {
|
|
|
466
466
|
|
|
467
467
|
assert.equal(host.pinnedMessageContainer.children.length, 0, "pinned zone should stay empty without tool calls");
|
|
468
468
|
});
|
|
469
|
+
|
|
470
|
+
// Regression test for issue #4144: interleaved text/tool content must render in content[] index order.
|
|
471
|
+
// Stream: [text "A", toolCall T1, text "B", toolCall T2, text "C"]
|
|
472
|
+
// Expected chatContainer order: textRun(A), toolExec(T1), textRun(B), toolExec(T2), textRun(C)
|
|
473
|
+
// Each AssistantMessageComponent must render ONLY its own text — no duplication after message_end.
|
|
474
|
+
test("chat-controller renders interleaved text and tool blocks in content[] index order (#4144)", async () => {
|
|
475
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
476
|
+
fg: (_key: string, text: string) => text,
|
|
477
|
+
bg: (_key: string, text: string) => text,
|
|
478
|
+
bold: (text: string) => text,
|
|
479
|
+
italic: (text: string) => text,
|
|
480
|
+
truncate: (text: string) => text,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const host = createHost();
|
|
484
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
485
|
+
|
|
486
|
+
const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} };
|
|
487
|
+
const t2 = { type: "toolCall", id: "t2", name: "tool_two", arguments: {} };
|
|
488
|
+
|
|
489
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
490
|
+
|
|
491
|
+
// Stream text "A" at index 0
|
|
492
|
+
await handleAgentEvent(host, {
|
|
493
|
+
type: "message_update",
|
|
494
|
+
message: makeAssistant([{ type: "text", text: "A" }]),
|
|
495
|
+
assistantMessageEvent: {
|
|
496
|
+
type: "text_delta",
|
|
497
|
+
contentIndex: 0,
|
|
498
|
+
delta: "A",
|
|
499
|
+
partial: makeAssistant([{ type: "text", text: "A" }]),
|
|
500
|
+
},
|
|
501
|
+
} as any);
|
|
502
|
+
|
|
503
|
+
// Stream toolCall T1 at index 1
|
|
504
|
+
await handleAgentEvent(host, {
|
|
505
|
+
type: "message_update",
|
|
506
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
507
|
+
assistantMessageEvent: {
|
|
508
|
+
type: "toolcall_end",
|
|
509
|
+
contentIndex: 1,
|
|
510
|
+
toolCall: {
|
|
511
|
+
...t1,
|
|
512
|
+
externalResult: { content: [{ type: "text", text: "result1" }], details: {}, isError: false },
|
|
513
|
+
},
|
|
514
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
515
|
+
},
|
|
516
|
+
} as any);
|
|
517
|
+
|
|
518
|
+
// Stream text "B" at index 2
|
|
519
|
+
await handleAgentEvent(host, {
|
|
520
|
+
type: "message_update",
|
|
521
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }]),
|
|
522
|
+
assistantMessageEvent: {
|
|
523
|
+
type: "text_delta",
|
|
524
|
+
contentIndex: 2,
|
|
525
|
+
delta: "B",
|
|
526
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }]),
|
|
527
|
+
},
|
|
528
|
+
} as any);
|
|
529
|
+
|
|
530
|
+
// Stream toolCall T2 at index 3
|
|
531
|
+
await handleAgentEvent(host, {
|
|
532
|
+
type: "message_update",
|
|
533
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }, t2]),
|
|
534
|
+
assistantMessageEvent: {
|
|
535
|
+
type: "toolcall_end",
|
|
536
|
+
contentIndex: 3,
|
|
537
|
+
toolCall: {
|
|
538
|
+
...t2,
|
|
539
|
+
externalResult: { content: [{ type: "text", text: "result2" }], details: {}, isError: false },
|
|
540
|
+
},
|
|
541
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }, t2]),
|
|
542
|
+
},
|
|
543
|
+
} as any);
|
|
544
|
+
|
|
545
|
+
// Stream text "C" at index 4
|
|
546
|
+
const finalContent = [
|
|
547
|
+
{ type: "text", text: "A" }, t1, { type: "text", text: "B" }, t2, { type: "text", text: "C" },
|
|
548
|
+
];
|
|
549
|
+
await handleAgentEvent(host, {
|
|
550
|
+
type: "message_update",
|
|
551
|
+
message: makeAssistant(finalContent),
|
|
552
|
+
assistantMessageEvent: {
|
|
553
|
+
type: "text_delta",
|
|
554
|
+
contentIndex: 4,
|
|
555
|
+
delta: "C",
|
|
556
|
+
partial: makeAssistant(finalContent),
|
|
557
|
+
},
|
|
558
|
+
} as any);
|
|
559
|
+
|
|
560
|
+
// Finalize — exercises the message_end path where a buggy setRange(undefined) would cause duplication
|
|
561
|
+
await handleAgentEvent(host, { type: "message_end", message: makeAssistant(finalContent) } as any);
|
|
562
|
+
|
|
563
|
+
// Assert interleaved order: textRun(A), toolExec(T1), textRun(B), toolExec(T2), textRun(C)
|
|
564
|
+
assert.equal(host.chatContainer.children.length, 5, "should have 5 children in interleaved order");
|
|
565
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "AssistantMessageComponent", "index 0: text run A");
|
|
566
|
+
assert.equal(host.chatContainer.children[1]?.constructor?.name, "ToolExecutionComponent", "index 1: tool T1");
|
|
567
|
+
assert.equal(host.chatContainer.children[2]?.constructor?.name, "AssistantMessageComponent", "index 2: text run B");
|
|
568
|
+
assert.equal(host.chatContainer.children[3]?.constructor?.name, "ToolExecutionComponent", "index 3: tool T2");
|
|
569
|
+
assert.equal(host.chatContainer.children[4]?.constructor?.name, "AssistantMessageComponent", "index 4: text run C");
|
|
570
|
+
|
|
571
|
+
// Helper: collect the text of all Markdown children inside an AssistantMessageComponent.
|
|
572
|
+
// Structure: AssistantMessageComponent (Container) -> contentContainer (children[0]) -> Markdown nodes.
|
|
573
|
+
function getRenderedTexts(comp: any): string[] {
|
|
574
|
+
const contentContainer = comp.children?.[0];
|
|
575
|
+
if (!contentContainer) return [];
|
|
576
|
+
return (contentContainer.children ?? [])
|
|
577
|
+
.filter((c: any) => c.constructor?.name === "Markdown")
|
|
578
|
+
.map((c: any) => (c as any).text as string);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Each text-run component must contain only its own text — no cross-contamination after message_end
|
|
582
|
+
assert.deepEqual(getRenderedTexts(host.chatContainer.children[0]), ["A"], "text run A must contain only 'A'");
|
|
583
|
+
assert.deepEqual(getRenderedTexts(host.chatContainer.children[2]), ["B"], "text run B must contain only 'B'");
|
|
584
|
+
assert.deepEqual(getRenderedTexts(host.chatContainer.children[4]), ["C"], "text run C must contain only 'C'");
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test("chat-controller does not duplicate text when content is [text, tool, text] (interleaved stream)", async () => {
|
|
588
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
589
|
+
fg: (_key: string, text: string) => text,
|
|
590
|
+
bg: (_key: string, text: string) => text,
|
|
591
|
+
bold: (text: string) => text,
|
|
592
|
+
italic: (text: string) => text,
|
|
593
|
+
truncate: (text: string) => text,
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const host = createHost();
|
|
597
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
598
|
+
|
|
599
|
+
const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} };
|
|
600
|
+
|
|
601
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
602
|
+
|
|
603
|
+
// Step 1: text "A" at index 0
|
|
604
|
+
await handleAgentEvent(host, {
|
|
605
|
+
type: "message_update",
|
|
606
|
+
message: makeAssistant([{ type: "text", text: "A" }]),
|
|
607
|
+
assistantMessageEvent: {
|
|
608
|
+
type: "text_delta",
|
|
609
|
+
contentIndex: 0,
|
|
610
|
+
delta: "A",
|
|
611
|
+
partial: makeAssistant([{ type: "text", text: "A" }]),
|
|
612
|
+
},
|
|
613
|
+
} as any);
|
|
614
|
+
|
|
615
|
+
// Step 2: toolCall at index 1
|
|
616
|
+
await handleAgentEvent(host, {
|
|
617
|
+
type: "message_update",
|
|
618
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
619
|
+
assistantMessageEvent: {
|
|
620
|
+
type: "toolcall_end",
|
|
621
|
+
contentIndex: 1,
|
|
622
|
+
toolCall: {
|
|
623
|
+
...t1,
|
|
624
|
+
externalResult: { content: [{ type: "text", text: "result1" }], details: {}, isError: false },
|
|
625
|
+
},
|
|
626
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
627
|
+
},
|
|
628
|
+
} as any);
|
|
629
|
+
|
|
630
|
+
// Step 3: text "B" at index 2
|
|
631
|
+
const finalContent = [{ type: "text", text: "A" }, t1, { type: "text", text: "B" }];
|
|
632
|
+
await handleAgentEvent(host, {
|
|
633
|
+
type: "message_update",
|
|
634
|
+
message: makeAssistant(finalContent),
|
|
635
|
+
assistantMessageEvent: {
|
|
636
|
+
type: "text_delta",
|
|
637
|
+
contentIndex: 2,
|
|
638
|
+
delta: "B",
|
|
639
|
+
partial: makeAssistant(finalContent),
|
|
640
|
+
},
|
|
641
|
+
} as any);
|
|
642
|
+
|
|
643
|
+
assert.equal(host.chatContainer.children.length, 3);
|
|
644
|
+
assert.equal(host.chatContainer.children[0]?.constructor?.name, "AssistantMessageComponent");
|
|
645
|
+
assert.equal(host.chatContainer.children[1]?.constructor?.name, "ToolExecutionComponent");
|
|
646
|
+
assert.equal(host.chatContainer.children[2]?.constructor?.name, "AssistantMessageComponent");
|
|
647
|
+
|
|
648
|
+
const firstText = host.chatContainer.children[0];
|
|
649
|
+
const secondText = host.chatContainer.children[2];
|
|
650
|
+
assert.notEqual(firstText, secondText, "text-before-tool and text-after-tool must be separate component instances");
|
|
651
|
+
assert.deepEqual((firstText as any).range, { startIndex: 0, endIndex: 0 }, "first text-run covers only content[0]");
|
|
652
|
+
assert.deepEqual((secondText as any).range, { startIndex: 2, endIndex: 2 }, "second text-run covers only content[2]");
|
|
653
|
+
|
|
654
|
+
// Finalize — regression guard: range must NOT be cleared on message_end (would cause duplication)
|
|
655
|
+
await handleAgentEvent(host, { type: "message_end", message: makeAssistant(finalContent) } as any);
|
|
656
|
+
|
|
657
|
+
assert.deepEqual((secondText as any).range, { startIndex: 2, endIndex: 2 }, "range must not be cleared on message_end (would cause duplication)");
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// Regression for the claude-code sub-turn bug that followed #4144:
|
|
661
|
+
// an adapter can reset content[] back to 0/1 mid-lifecycle when a new provider
|
|
662
|
+
// sub-turn begins. The segment walker must NOT update prior-sub-turn text-run
|
|
663
|
+
// components in place (which would destroy earlier history) and must NOT reuse
|
|
664
|
+
// stale tool registrations for a new tool at the same contentIndex. Prior
|
|
665
|
+
// sub-turn children must stay frozen; new sub-turn segments must append after
|
|
666
|
+
// them, and the pinned "Latest Output" mirror must re-evaluate for the new sub-turn.
|
|
667
|
+
test("chat-controller freezes prior sub-turn and appends new segments when content shrinks", async () => {
|
|
668
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
669
|
+
fg: (_key: string, text: string) => text,
|
|
670
|
+
bg: (_key: string, text: string) => text,
|
|
671
|
+
bold: (text: string) => text,
|
|
672
|
+
italic: (text: string) => text,
|
|
673
|
+
truncate: (text: string) => text,
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const host = createHost();
|
|
677
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
678
|
+
|
|
679
|
+
const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} };
|
|
680
|
+
const t2 = { type: "toolCall", id: "t2", name: "tool_two", arguments: {} };
|
|
681
|
+
|
|
682
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
683
|
+
|
|
684
|
+
// Sub-turn 1: grow to [A, T1, B]
|
|
685
|
+
await handleAgentEvent(host, {
|
|
686
|
+
type: "message_update",
|
|
687
|
+
message: makeAssistant([{ type: "text", text: "A" }]),
|
|
688
|
+
assistantMessageEvent: {
|
|
689
|
+
type: "text_delta", contentIndex: 0, delta: "A",
|
|
690
|
+
partial: makeAssistant([{ type: "text", text: "A" }]),
|
|
691
|
+
},
|
|
692
|
+
} as any);
|
|
693
|
+
await handleAgentEvent(host, {
|
|
694
|
+
type: "message_update",
|
|
695
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
696
|
+
assistantMessageEvent: {
|
|
697
|
+
type: "toolcall_end", contentIndex: 1,
|
|
698
|
+
toolCall: { ...t1, externalResult: { content: [{ type: "text", text: "r1" }], details: {}, isError: false } },
|
|
699
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1]),
|
|
700
|
+
},
|
|
701
|
+
} as any);
|
|
702
|
+
await handleAgentEvent(host, {
|
|
703
|
+
type: "message_update",
|
|
704
|
+
message: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }]),
|
|
705
|
+
assistantMessageEvent: {
|
|
706
|
+
type: "text_delta", contentIndex: 2, delta: "B",
|
|
707
|
+
partial: makeAssistant([{ type: "text", text: "A" }, t1, { type: "text", text: "B" }]),
|
|
708
|
+
},
|
|
709
|
+
} as any);
|
|
710
|
+
|
|
711
|
+
assert.equal(host.chatContainer.children.length, 3, "sub-turn 1 renders 3 children");
|
|
712
|
+
const priorA = host.chatContainer.children[0];
|
|
713
|
+
const priorT1 = host.chatContainer.children[1];
|
|
714
|
+
const priorB = host.chatContainer.children[2];
|
|
715
|
+
|
|
716
|
+
// Sub-turn boundary: adapter resets content[] to [C]
|
|
717
|
+
await handleAgentEvent(host, {
|
|
718
|
+
type: "message_update",
|
|
719
|
+
message: makeAssistant([{ type: "text", text: "C" }]),
|
|
720
|
+
assistantMessageEvent: {
|
|
721
|
+
type: "text_delta", contentIndex: 0, delta: "C",
|
|
722
|
+
partial: makeAssistant([{ type: "text", text: "C" }]),
|
|
723
|
+
},
|
|
724
|
+
} as any);
|
|
725
|
+
|
|
726
|
+
// Prior 3 children must still exist in DOM — and a NEW text-run for "C" appended after them.
|
|
727
|
+
assert.equal(host.chatContainer.children.length, 4, "shrink must append new segment, not replace prior history");
|
|
728
|
+
assert.equal(host.chatContainer.children[0], priorA, "prior A component stays at index 0");
|
|
729
|
+
assert.equal(host.chatContainer.children[1], priorT1, "prior T1 component stays at index 1");
|
|
730
|
+
assert.equal(host.chatContainer.children[2], priorB, "prior B component stays at index 2");
|
|
731
|
+
assert.notEqual(host.chatContainer.children[3], priorA, "new C text-run must be a different component from prior A");
|
|
732
|
+
assert.equal(host.chatContainer.children[3]?.constructor?.name, "AssistantMessageComponent");
|
|
733
|
+
|
|
734
|
+
// Prior A component must still render "A", not be overwritten with "C".
|
|
735
|
+
function getRenderedTexts(comp: any): string[] {
|
|
736
|
+
const contentContainer = comp.children?.[0];
|
|
737
|
+
if (!contentContainer) return [];
|
|
738
|
+
return (contentContainer.children ?? [])
|
|
739
|
+
.filter((c: any) => c.constructor?.name === "Markdown")
|
|
740
|
+
.map((c: any) => (c as any).text as string);
|
|
741
|
+
}
|
|
742
|
+
assert.deepEqual(getRenderedTexts(priorA), ["A"], "prior A text-run must still contain 'A' after shrink");
|
|
743
|
+
assert.deepEqual(getRenderedTexts(priorB), ["B"], "prior B text-run must still contain 'B' after shrink");
|
|
744
|
+
assert.deepEqual(getRenderedTexts(host.chatContainer.children[3]), ["C"], "new text-run must contain only 'C'");
|
|
745
|
+
|
|
746
|
+
// Sub-turn 2 grows with a new tool T2 at contentIndex=1.
|
|
747
|
+
await handleAgentEvent(host, {
|
|
748
|
+
type: "message_update",
|
|
749
|
+
message: makeAssistant([{ type: "text", text: "C" }, t2]),
|
|
750
|
+
assistantMessageEvent: {
|
|
751
|
+
type: "toolcall_end", contentIndex: 1,
|
|
752
|
+
toolCall: { ...t2, externalResult: { content: [{ type: "text", text: "r2" }], details: {}, isError: false } },
|
|
753
|
+
partial: makeAssistant([{ type: "text", text: "C" }, t2]),
|
|
754
|
+
},
|
|
755
|
+
} as any);
|
|
756
|
+
|
|
757
|
+
// T2 must be appended after the new C text-run, not conflated with the stale T1 registration.
|
|
758
|
+
assert.equal(host.chatContainer.children.length, 5, "new tool appends after new text-run");
|
|
759
|
+
assert.equal(host.chatContainer.children[4]?.constructor?.name, "ToolExecutionComponent");
|
|
760
|
+
assert.notEqual(host.chatContainer.children[4], priorT1, "new T2 must be a different component from prior T1");
|
|
761
|
+
|
|
762
|
+
// Finalize so the module-level pinned spinner (setInterval) is torn down and the test process can exit.
|
|
763
|
+
await handleAgentEvent(host, { type: "message_end", message: makeAssistant([{ type: "text", text: "C" }, t2]) } as any);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// Regression: after a sub-turn shrink, lastPinnedText must be cleared so the
|
|
767
|
+
// pinned "Latest Output" mirror can display text from the new sub-turn instead
|
|
768
|
+
// of staying frozen on a stale snapshot (the "bottom green stays" symptom).
|
|
769
|
+
test("chat-controller updates pinned zone after sub-turn shrink", async () => {
|
|
770
|
+
(globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = {
|
|
771
|
+
fg: (_key: string, text: string) => text,
|
|
772
|
+
bg: (_key: string, text: string) => text,
|
|
773
|
+
bold: (text: string) => text,
|
|
774
|
+
italic: (text: string) => text,
|
|
775
|
+
truncate: (text: string) => text,
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
const host = createHost();
|
|
779
|
+
host.getMarkdownThemeWithSettings = () => ({});
|
|
780
|
+
|
|
781
|
+
const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} };
|
|
782
|
+
const t2 = { type: "toolCall", id: "t2", name: "tool_two", arguments: {} };
|
|
783
|
+
|
|
784
|
+
await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any);
|
|
785
|
+
|
|
786
|
+
// Sub-turn 1 with pinnable text before a tool → populates pinned zone with "first".
|
|
787
|
+
await handleAgentEvent(host, {
|
|
788
|
+
type: "message_update",
|
|
789
|
+
message: makeAssistant([{ type: "text", text: "first" }, t1]),
|
|
790
|
+
assistantMessageEvent: {
|
|
791
|
+
type: "toolcall_end", contentIndex: 1,
|
|
792
|
+
toolCall: { ...t1, externalResult: { content: [{ type: "text", text: "r1" }], details: {}, isError: false } },
|
|
793
|
+
partial: makeAssistant([{ type: "text", text: "first" }, t1]),
|
|
794
|
+
},
|
|
795
|
+
} as any);
|
|
796
|
+
const pinnedMarkdown = host.pinnedMessageContainer.children[1];
|
|
797
|
+
assert.equal((pinnedMarkdown as any)?.text, "first", "pinned zone seeded with sub-turn 1 text");
|
|
798
|
+
|
|
799
|
+
// Sub-turn boundary: content resets to [second, t2].
|
|
800
|
+
await handleAgentEvent(host, {
|
|
801
|
+
type: "message_update",
|
|
802
|
+
message: makeAssistant([{ type: "text", text: "second" }, t2]),
|
|
803
|
+
assistantMessageEvent: {
|
|
804
|
+
type: "toolcall_end", contentIndex: 1,
|
|
805
|
+
toolCall: { ...t2, externalResult: { content: [{ type: "text", text: "r2" }], details: {}, isError: false } },
|
|
806
|
+
partial: makeAssistant([{ type: "text", text: "second" }, t2]),
|
|
807
|
+
},
|
|
808
|
+
} as any);
|
|
809
|
+
|
|
810
|
+
// Pinned markdown must now reflect the new sub-turn's text, not stay frozen on "first".
|
|
811
|
+
assert.equal((pinnedMarkdown as any)?.text, "second", "pinned zone must update after sub-turn shrink (#4144 regression)");
|
|
812
|
+
|
|
813
|
+
// Finalize so the module-level pinned spinner (setInterval) is torn down and the test process can exit.
|
|
814
|
+
await handleAgentEvent(host, { type: "message_end", message: makeAssistant([{ type: "text", text: "second" }, t2]) } as any);
|
|
815
|
+
});
|
|
@@ -218,7 +218,7 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
218
218
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
219
219
|
.map((c) => c.text)
|
|
220
220
|
.join("");
|
|
221
|
-
if (content) parts.push(
|
|
221
|
+
if (content) parts.push(`**User said:** ${content}`);
|
|
222
222
|
} else if (msg.role === "assistant") {
|
|
223
223
|
const textParts: string[] = [];
|
|
224
224
|
const thinkingParts: string[] = [];
|
|
@@ -239,13 +239,13 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
if (thinkingParts.length > 0) {
|
|
242
|
-
parts.push(
|
|
242
|
+
parts.push(`**Assistant thinking:** ${thinkingParts.join("\n")}`);
|
|
243
243
|
}
|
|
244
244
|
if (textParts.length > 0) {
|
|
245
|
-
parts.push(
|
|
245
|
+
parts.push(`**Assistant responded:** ${textParts.join("\n")}`);
|
|
246
246
|
}
|
|
247
247
|
if (toolCalls.length > 0) {
|
|
248
|
-
parts.push(
|
|
248
|
+
parts.push(`**Assistant tool calls:** ${toolCalls.join("; ")}`);
|
|
249
249
|
}
|
|
250
250
|
} else if (msg.role === "toolResult") {
|
|
251
251
|
const content = msg.content
|
|
@@ -253,7 +253,7 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
253
253
|
.map((c) => c.text)
|
|
254
254
|
.join("");
|
|
255
255
|
if (content) {
|
|
256
|
-
parts.push(
|
|
256
|
+
parts.push(`**Tool result:** ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import type { Message } from "@gsd/pi-ai";
|
|
5
|
+
|
|
6
|
+
import { serializeConversation } from "./compaction/index.js";
|
|
7
|
+
|
|
8
|
+
test("serializeConversation uses narrative role markers instead of chat-style delimiters (#4054)", () => {
|
|
9
|
+
const messages: Message[] = [
|
|
10
|
+
{ role: "user", content: "Please refactor the parser." } as Message,
|
|
11
|
+
{
|
|
12
|
+
role: "assistant",
|
|
13
|
+
content: [
|
|
14
|
+
{ type: "thinking", thinking: "I should inspect the parser entry points first." },
|
|
15
|
+
{ type: "text", text: "I'll start with the parser entry points." },
|
|
16
|
+
{ type: "toolCall", id: "tool-1", name: "Read", arguments: { path: "src/parser.ts" } },
|
|
17
|
+
],
|
|
18
|
+
api: "anthropic-messages",
|
|
19
|
+
provider: "anthropic",
|
|
20
|
+
model: "claude-sonnet-4-6",
|
|
21
|
+
usage: {
|
|
22
|
+
input: 0,
|
|
23
|
+
output: 0,
|
|
24
|
+
cacheRead: 0,
|
|
25
|
+
cacheWrite: 0,
|
|
26
|
+
totalTokens: 0,
|
|
27
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
28
|
+
},
|
|
29
|
+
stopReason: "stop",
|
|
30
|
+
timestamp: Date.now(),
|
|
31
|
+
} as Message,
|
|
32
|
+
{
|
|
33
|
+
role: "toolResult",
|
|
34
|
+
content: [{ type: "text", text: "parser contents" }],
|
|
35
|
+
toolName: "Read",
|
|
36
|
+
toolCallId: "tool-1",
|
|
37
|
+
} as Message,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const serialized = serializeConversation(messages);
|
|
41
|
+
|
|
42
|
+
assert.match(serialized, /\*\*User said:\*\* Please refactor the parser\./);
|
|
43
|
+
assert.match(serialized, /\*\*Assistant thinking:\*\* I should inspect the parser entry points first\./);
|
|
44
|
+
assert.match(serialized, /\*\*Assistant responded:\*\* I'll start with the parser entry points\./);
|
|
45
|
+
assert.match(serialized, /\*\*Assistant tool calls:\*\* Read\(path="src\/parser\.ts"\)/);
|
|
46
|
+
assert.match(serialized, /\*\*Tool result:\*\* parser contents/);
|
|
47
|
+
assert.ok(!serialized.includes("[User]:"), "chat-style [User]: markers should not remain");
|
|
48
|
+
assert.ok(!serialized.includes("[Assistant]:"), "chat-style [Assistant]: markers should not remain");
|
|
49
|
+
assert.ok(!serialized.includes("[Tool result]:"), "chat-style [Tool result]: markers should not remain");
|
|
50
|
+
});
|
|
@@ -3,8 +3,15 @@ import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-t
|
|
|
3
3
|
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|
4
4
|
import { formatTimestamp, type TimestampFormat } from "./timestamp.js";
|
|
5
5
|
|
|
6
|
+
export interface ContentRange {
|
|
7
|
+
startIndex: number;
|
|
8
|
+
endIndex: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
|
-
* Component that renders a complete assistant message
|
|
12
|
+
* Component that renders a complete assistant message, or a sub-range of its content[].
|
|
13
|
+
* When `range` is provided, only content[startIndex..endIndex] (inclusive) is rendered.
|
|
14
|
+
* Non-text/thinking blocks within the range are silently skipped.
|
|
8
15
|
*/
|
|
9
16
|
export class AssistantMessageComponent extends Container {
|
|
10
17
|
private contentContainer: Container;
|
|
@@ -12,18 +19,26 @@ export class AssistantMessageComponent extends Container {
|
|
|
12
19
|
private markdownTheme: MarkdownTheme;
|
|
13
20
|
private lastMessage?: AssistantMessage;
|
|
14
21
|
private timestampFormat: TimestampFormat;
|
|
22
|
+
private range?: ContentRange;
|
|
23
|
+
private showMetadata: boolean;
|
|
15
24
|
|
|
16
25
|
constructor(
|
|
17
26
|
message?: AssistantMessage,
|
|
18
27
|
hideThinkingBlock = false,
|
|
19
28
|
markdownTheme: MarkdownTheme = getMarkdownTheme(),
|
|
20
29
|
timestampFormat: TimestampFormat = "date-time-iso",
|
|
30
|
+
range?: ContentRange,
|
|
21
31
|
) {
|
|
22
32
|
super();
|
|
23
33
|
|
|
24
34
|
this.hideThinkingBlock = hideThinkingBlock;
|
|
25
35
|
this.markdownTheme = markdownTheme;
|
|
26
36
|
this.timestampFormat = timestampFormat;
|
|
37
|
+
this.range = range;
|
|
38
|
+
// No range = legacy full-message rendering; show metadata by default.
|
|
39
|
+
// Ranged (interleaved) instances start with metadata hidden; chat-controller
|
|
40
|
+
// calls setShowMetadata(true) on the last segment at message_end.
|
|
41
|
+
this.showMetadata = !range;
|
|
27
42
|
|
|
28
43
|
// Container for text/thinking content
|
|
29
44
|
this.contentContainer = new Container();
|
|
@@ -34,6 +49,20 @@ export class AssistantMessageComponent extends Container {
|
|
|
34
49
|
}
|
|
35
50
|
}
|
|
36
51
|
|
|
52
|
+
setRange(range: ContentRange | undefined): void {
|
|
53
|
+
this.range = range;
|
|
54
|
+
if (this.lastMessage) {
|
|
55
|
+
this.updateContent(this.lastMessage);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setShowMetadata(show: boolean): void {
|
|
60
|
+
this.showMetadata = show;
|
|
61
|
+
if (this.lastMessage) {
|
|
62
|
+
this.updateContent(this.lastMessage);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
override invalidate(): void {
|
|
38
67
|
super.invalidate();
|
|
39
68
|
if (this.lastMessage) {
|
|
@@ -51,7 +80,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
51
80
|
// Clear content container
|
|
52
81
|
this.contentContainer.clear();
|
|
53
82
|
|
|
54
|
-
const
|
|
83
|
+
const start = this.range?.startIndex ?? 0;
|
|
84
|
+
const end = this.range?.endIndex ?? message.content.length - 1;
|
|
85
|
+
const slice = message.content.slice(start, end + 1);
|
|
86
|
+
|
|
87
|
+
const hasVisibleContent = slice.some(
|
|
55
88
|
(c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
|
|
56
89
|
);
|
|
57
90
|
|
|
@@ -59,9 +92,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
59
92
|
this.contentContainer.addChild(new Spacer(1));
|
|
60
93
|
}
|
|
61
94
|
|
|
62
|
-
// Render content in order
|
|
63
|
-
for (let i = 0; i <
|
|
64
|
-
const content =
|
|
95
|
+
// Render content in order; non-text/thinking blocks are silently skipped
|
|
96
|
+
for (let i = 0; i < slice.length; i++) {
|
|
97
|
+
const content = slice[i];
|
|
65
98
|
if (content.type === "text" && content.text.trim()) {
|
|
66
99
|
// Assistant text messages with no background - trim the text
|
|
67
100
|
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
@@ -69,7 +102,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
69
102
|
} else if (content.type === "thinking" && content.thinking.trim()) {
|
|
70
103
|
// Add spacing only when another visible assistant content block follows.
|
|
71
104
|
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
72
|
-
const hasVisibleContentAfter =
|
|
105
|
+
const hasVisibleContentAfter = slice
|
|
73
106
|
.slice(i + 1)
|
|
74
107
|
.some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
|
|
75
108
|
|
|
@@ -94,30 +127,33 @@ export class AssistantMessageComponent extends Container {
|
|
|
94
127
|
}
|
|
95
128
|
}
|
|
96
129
|
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
// Metadata (errors, timestamp): gated on showMetadata so ranged instances stay clean
|
|
131
|
+
// until chat-controller explicitly enables it on the last segment at message_end.
|
|
132
|
+
if (this.showMetadata) {
|
|
133
|
+
// Check if aborted - show after partial content
|
|
134
|
+
// But only if there are no tool calls (tool execution components will show the error)
|
|
135
|
+
const hasToolCalls = message.content.some((c) => c.type === "toolCall");
|
|
136
|
+
if (!hasToolCalls) {
|
|
137
|
+
if (message.stopReason === "aborted") {
|
|
138
|
+
const abortMessage =
|
|
139
|
+
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
140
|
+
? message.errorMessage
|
|
141
|
+
: "Operation aborted";
|
|
142
|
+
if (hasVisibleContent) {
|
|
143
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
144
|
+
}
|
|
145
|
+
this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
|
|
146
|
+
} else if (message.stopReason === "error") {
|
|
147
|
+
const errorMsg = message.errorMessage || "Unknown error";
|
|
107
148
|
this.contentContainer.addChild(new Spacer(1));
|
|
149
|
+
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
108
150
|
}
|
|
109
|
-
this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
|
|
110
|
-
} else if (message.stopReason === "error") {
|
|
111
|
-
const errorMsg = message.errorMessage || "Unknown error";
|
|
112
|
-
this.contentContainer.addChild(new Spacer(1));
|
|
113
|
-
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
114
151
|
}
|
|
115
|
-
}
|
|
116
152
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
153
|
+
if (message.stopReason && message.timestamp) {
|
|
154
|
+
const timeStr = formatTimestamp(message.timestamp, this.timestampFormat);
|
|
155
|
+
this.contentContainer.addChild(new Text(theme.fg("dim", timeStr), 1, 0));
|
|
156
|
+
}
|
|
121
157
|
}
|
|
122
158
|
}
|
|
123
159
|
}
|