@pi-archimedes/core 2.8.0 → 2.9.1

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.
@@ -1,26 +0,0 @@
1
- const NPM_REGISTRY_URL = "https://registry.npmjs.org/@earendil-works/pi-coding-agent/latest";
2
- const FETCH_TIMEOUT_MS = 4000;
3
-
4
- export async function fetchLatestVersion(): Promise<string | undefined> {
5
- try {
6
- const controller = new AbortController();
7
- const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
8
- const res = await fetch(NPM_REGISTRY_URL, { signal: controller.signal });
9
- clearTimeout(timeout);
10
- if (!res.ok) return undefined;
11
- const data = (await res.json()) as { version?: string };
12
- return data.version;
13
- } catch {
14
- return undefined;
15
- }
16
- }
17
-
18
- export function compareVersions(a: string, b: string): number {
19
- const pa = a.replace(/^v/, "").split(".").map(Number);
20
- const pb = b.replace(/^v/, "").split(".").map(Number);
21
- for (let i = 0; i < 3; i++) {
22
- if ((pa[i] ?? 0) > (pb[i] ?? 0)) return 1;
23
- if ((pa[i] ?? 0) < (pb[i] ?? 0)) return -1;
24
- }
25
- return 0;
26
- }
@@ -1,389 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from "vitest";
2
-
3
- // Symbols used by patch.ts to mark patched prototypes
4
- const PATCHED_KEY = Symbol.for("archimedes:thinkingPatched");
5
- const PATCH_VERSION_KEY = Symbol.for("archimedes:thinkingPatchVersion");
6
-
7
- // Dynamic import after mocking the pi-coding-agent module
8
- async function importPatch() {
9
- vi.resetModules();
10
- const mod = await import("./patch.js");
11
- return mod.patchThinkingRenderer;
12
- }
13
-
14
- describe("patchThinkingRenderer", () => {
15
- afterEach(() => {
16
- vi.resetModules();
17
- });
18
-
19
- it("returns early when AssistantMessageComponent is undefined", async () => {
20
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
21
- AssistantMessageComponent: undefined,
22
- VERSION: "1.0.0",
23
- highlightCode: vi.fn(),
24
- }));
25
-
26
- const patch = await importPatch();
27
- // Should not throw
28
- patch(() => ({} as any));
29
- });
30
-
31
- it("returns early when prototype is null", async () => {
32
- const MockClass = function AssistantMessageComponent() {};
33
- MockClass.prototype = null;
34
-
35
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
36
- AssistantMessageComponent: MockClass,
37
- VERSION: "1.0.0",
38
- highlightCode: vi.fn(),
39
- }));
40
-
41
- const patch = await importPatch();
42
- patch(() => ({} as any));
43
- // Prototype must still be null — no patching occurred
44
- expect(MockClass.prototype).toBeNull();
45
- });
46
-
47
- it("returns early when updateContent is not a function", async () => {
48
- const MockClass = function AssistantMessageComponent() {};
49
- MockClass.prototype.updateContent = "not a function";
50
-
51
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
52
- AssistantMessageComponent: MockClass,
53
- VERSION: "1.0.0",
54
- highlightCode: vi.fn(),
55
- }));
56
-
57
- const patch = await importPatch();
58
- patch(() => ({} as any));
59
- // PATCHED_KEY must NOT be set — early return before patching
60
- expect(MockClass.prototype[PATCHED_KEY]).toBeUndefined();
61
- });
62
-
63
- it("returns early when class name does not match", async () => {
64
- const WrongName = function () {};
65
- WrongName.prototype.updateContent = function () {};
66
-
67
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
68
- AssistantMessageComponent: WrongName,
69
- VERSION: "1.0.0",
70
- highlightCode: vi.fn(),
71
- }));
72
-
73
- const patch = await importPatch();
74
- patch(() => ({} as any));
75
- // PATCHED_KEY must NOT be set — name mismatch causes early return
76
- expect(WrongName.prototype[PATCHED_KEY]).toBeUndefined();
77
- });
78
-
79
- it("returns early when source lacks thinking check", async () => {
80
- const MockClass = function AssistantMessageComponent() {};
81
- MockClass.prototype.updateContent = function updateContent() {
82
- // Does NOT contain: content.type === "thinking"
83
- this.doSomething();
84
- };
85
-
86
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
87
- AssistantMessageComponent: MockClass,
88
- VERSION: "1.0.0",
89
- highlightCode: vi.fn(),
90
- }));
91
-
92
- const patch = await importPatch();
93
- patch(() => ({} as any));
94
- // PATCHED_KEY must NOT be set — signature mismatch causes early return
95
- expect(MockClass.prototype[PATCHED_KEY]).toBeUndefined();
96
- });
97
-
98
- it("returns early when source lacks markdownTheme reference", async () => {
99
- const MockClass = function AssistantMessageComponent() {};
100
- MockClass.prototype.updateContent = function updateContent() {
101
- // Has thinking check but no markdownTheme
102
- if (this.content.type === "thinking") {
103
- this.render();
104
- }
105
- };
106
-
107
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
108
- AssistantMessageComponent: MockClass,
109
- VERSION: "1.0.0",
110
- highlightCode: vi.fn(),
111
- }));
112
-
113
- const patch = await importPatch();
114
- patch(() => ({} as any));
115
- // PATCHED_KEY must NOT be set — signature mismatch causes early return
116
- expect(MockClass.prototype[PATCHED_KEY]).toBeUndefined();
117
- });
118
-
119
- it("patches successfully when signature matches", async () => {
120
- const MockClass = function AssistantMessageComponent() {};
121
- MockClass.prototype.updateContent = function updateContent() {
122
- if (this.content.type === "thinking") {
123
- this.markdownTheme.codeBlockIndent = "";
124
- }
125
- };
126
-
127
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
128
- AssistantMessageComponent: MockClass,
129
- VERSION: "1.0.0",
130
- highlightCode: vi.fn(),
131
- }));
132
-
133
- const patch = await importPatch();
134
- patch(() => ({} as any));
135
-
136
- // The prototype should be marked as patched
137
- expect(MockClass.prototype[PATCHED_KEY]).toBe(true);
138
- expect(MockClass.prototype[PATCH_VERSION_KEY]).toBe("1.0.0");
139
- });
140
-
141
- it("marks prototype with correct version", async () => {
142
- const MockClass = function AssistantMessageComponent() {};
143
- MockClass.prototype.updateContent = function updateContent() {
144
- if (this.content.type === "thinking") {
145
- this.markdownTheme.codeBlockIndent = "";
146
- }
147
- };
148
-
149
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
150
- AssistantMessageComponent: MockClass,
151
- VERSION: "2.5.0",
152
- highlightCode: vi.fn(),
153
- }));
154
-
155
- const patch = await importPatch();
156
- patch(() => ({} as any));
157
-
158
- expect(MockClass.prototype[PATCH_VERSION_KEY]).toBe("2.5.0");
159
- });
160
-
161
- it("re-patches when version changes", async () => {
162
- // First patch with version 1.0.0
163
- const MockClassV1 = function AssistantMessageComponent() {};
164
- MockClassV1.prototype.updateContent = function updateContent() {
165
- if (this.content.type === "thinking") {
166
- this.markdownTheme.codeBlockIndent = "";
167
- }
168
- };
169
-
170
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
171
- AssistantMessageComponent: MockClassV1,
172
- VERSION: "1.0.0",
173
- highlightCode: vi.fn(),
174
- }));
175
-
176
- const patchV1 = await importPatch();
177
- patchV1(() => ({} as any));
178
- expect(MockClassV1.prototype[PATCH_VERSION_KEY]).toBe("1.0.0");
179
-
180
- // Now simulate a version change with a fresh class
181
- const MockClassV2 = function AssistantMessageComponent() {};
182
- MockClassV2.prototype.updateContent = function updateContent() {
183
- if (this.content.type === "thinking") {
184
- this.markdownTheme.codeBlockIndent = "";
185
- }
186
- };
187
-
188
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
189
- AssistantMessageComponent: MockClassV2,
190
- VERSION: "2.0.0",
191
- highlightCode: vi.fn(),
192
- }));
193
-
194
- const patchV2 = await importPatch();
195
- patchV2(() => ({} as any));
196
- expect(MockClassV2.prototype[PATCH_VERSION_KEY]).toBe("2.0.0");
197
- });
198
-
199
- it("accepts a minified thinking-check variant (no whitespace around ===)", async () => {
200
- const MockClass = function AssistantMessageComponent() {};
201
- MockClass.prototype.updateContent = function updateContent() {
202
- // Minified dist-chunk shape: no whitespace around ===, single quotes
203
- const content = { type: "thinking" };
204
- if (content.type==="thinking") {
205
- this.markdownTheme.codeBlockIndent="";
206
- }
207
- };
208
-
209
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
210
- AssistantMessageComponent: MockClass,
211
- VERSION: "1.0.0",
212
- highlightCode: vi.fn(),
213
- }));
214
-
215
- const patch = await importPatch();
216
- patch(() => ({} as any));
217
-
218
- // The minification-safe regex probe must accept the minified variant
219
- expect(MockClass.prototype[PATCHED_KEY]).toBe(true);
220
- });
221
-
222
- it("rejects a negated thinking check", async () => {
223
- const MockClass = function AssistantMessageComponent() {};
224
- MockClass.prototype.updateContent = function updateContent() {
225
- // pi 0.84.3's own minified chunk contains
226
- // `thinkingContent.type!=="thinking"` (inner batch-loop break). A source whose
227
- // ONLY thinking-relations are negations must NOT pass the probe.
228
- const content = { type: "thinking" };
229
- if (content.type!=="thinking") {
230
- this.markdownTheme.codeBlockIndent="";
231
- }
232
- };
233
-
234
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
235
- AssistantMessageComponent: MockClass,
236
- VERSION: "1.0.0",
237
- highlightCode: vi.fn(),
238
- }));
239
-
240
- const patch = await importPatch();
241
- patch(() => ({} as any));
242
-
243
- // PATCHED_KEY must NOT be set — the probe must not match `!==`
244
- expect(MockClass.prototype[PATCHED_KEY]).toBeUndefined();
245
- });
246
-
247
- it("re-patches on same version to update getTheme closure", async () => {
248
- const MockClass = function AssistantMessageComponent() {};
249
- MockClass.prototype.updateContent = function updateContent() {
250
- if (this.content.type === "thinking") {
251
- this.markdownTheme.codeBlockIndent = "";
252
- }
253
- };
254
-
255
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
256
- AssistantMessageComponent: MockClass,
257
- VERSION: "1.0.0",
258
- highlightCode: vi.fn(),
259
- }));
260
-
261
- const patch = await importPatch();
262
-
263
- // First patch
264
- patch(() => ({} as any));
265
- expect(MockClass.prototype[PATCHED_KEY]).toBe(true);
266
- const first = MockClass.prototype.updateContent;
267
-
268
- // Second patch (same version) — must produce a new function
269
- patch(() => ({} as any));
270
- expect(MockClass.prototype[PATCHED_KEY]).toBe(true);
271
- expect(MockClass.prototype[PATCH_VERSION_KEY]).toBe("1.0.0");
272
- // The patched function must be a new closure (not the same reference)
273
- expect(MockClass.prototype.updateContent).not.toBe(first);
274
- });
275
-
276
- // ── Configurable thinking label (issue #36) ────────────────────────────
277
-
278
- // Patches with a valid AssistantMessageComponent + mocked pi-tui so the
279
- // Markdown content rendered for a thinking block can be inspected.
280
- async function patchAndRender(
281
- config?: { labelText?: string; labelColor?: string },
282
- thinkingText = "Let me consider this carefully.",
283
- ) {
284
- const MockClass = function AssistantMessageComponent() {};
285
- MockClass.prototype.updateContent = function updateContent() {
286
- if (this.content.type === "thinking") {
287
- this.markdownTheme.codeBlockIndent = "";
288
- }
289
- };
290
-
291
- vi.doMock("@earendil-works/pi-coding-agent", () => ({
292
- AssistantMessageComponent: MockClass,
293
- VERSION: "1.0.0",
294
- highlightCode: vi.fn(),
295
- }));
296
-
297
- const capturedMarkdown: any[] = [];
298
- class MockMarkdown {
299
- content: string;
300
- constructor(content: string, ..._rest: any[]) {
301
- this.content = content;
302
- capturedMarkdown.push(this);
303
- }
304
- }
305
- class MockSpacer {}
306
- class MockText {}
307
- vi.doMock("@earendil-works/pi-tui", () => ({
308
- Markdown: MockMarkdown,
309
- Spacer: MockSpacer,
310
- Text: MockText,
311
- }));
312
-
313
- vi.resetModules();
314
- const mod = await import("./patch.js");
315
- mod.patchThinkingRenderer(
316
- () => ({ getFgAnsi: () => "", fg: (_t: string, text: string) => text }) as any,
317
- config,
318
- );
319
-
320
- const instance = {
321
- contentContainer: { clear: vi.fn(), addChild: vi.fn() },
322
- isStreaming: false,
323
- markdownTheme: { codeBlockIndent: "" },
324
- markdownTransformers: [],
325
- hideThinkingBlock: false,
326
- outputPad: 1,
327
- };
328
- const message = {
329
- content: [{ type: "thinking", thinking: thinkingText }],
330
- stopReason: undefined,
331
- };
332
- MockClass.prototype.updateContent.call(instance, message, false);
333
- return capturedMarkdown;
334
- }
335
-
336
- it("uses configured labelText/labelColor for the thinking label", async () => {
337
- const captured = await patchAndRender({
338
- labelText: "Yapping...",
339
- labelColor: "255,215,0",
340
- });
341
-
342
- expect(captured).toHaveLength(1);
343
- expect(
344
- captured[0]!.content.startsWith("\x1b[1m\x1b[38;2;255;215;0mYapping...\x1b[39m\x1b[22m\n\n"),
345
- ).toBe(true);
346
- });
347
-
348
- it("defaults to the original Thinking... label when no config is given", async () => {
349
- const captured = await patchAndRender();
350
-
351
- expect(captured).toHaveLength(1);
352
- // Byte-identical to the previous hardcoded THINKING_LABEL
353
- expect(
354
- captured[0]!.content.startsWith("\x1b[1m\x1b[38;2;255;215;0mThinking...\x1b[39m\x1b[22m\n\n"),
355
- ).toBe(true);
356
- });
357
-
358
- it("trims whitespace from configured labelText and labelColor", async () => {
359
- const captured = await patchAndRender({
360
- labelText: " Yapping... ",
361
- labelColor: " 255, 215, 0 ",
362
- });
363
-
364
- expect(
365
- captured[0]!.content.startsWith("\x1b[1m\x1b[38;2;255;215;0mYapping...\x1b[39m\x1b[22m\n\n"),
366
- ).toBe(true);
367
- });
368
-
369
- it("falls back to 255,215,0 when labelColor is not a valid RGB triple", async () => {
370
- const captured = await patchAndRender({
371
- labelText: "Hmm",
372
- labelColor: "not-a-color",
373
- });
374
-
375
- expect(
376
- captured[0]!.content.startsWith("\x1b[1m\x1b[38;2;255;215;0mHmm\x1b[39m\x1b[22m\n\n"),
377
- ).toBe(true);
378
- });
379
-
380
- it("does not double-prepend the label when content already starts with it", async () => {
381
- const label = "\x1b[1m\x1b[38;2;255;215;0mYapping...\x1b[39m\x1b[22m";
382
- const captured = await patchAndRender(
383
- { labelText: "Yapping...", labelColor: "255,215,0" },
384
- `${label}\n\nAlready labelled body.`,
385
- );
386
-
387
- expect(captured[0]!.content).toBe(`${label}\n\nAlready labelled body.`);
388
- });
389
- });
@@ -1,288 +0,0 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { AssistantMessageComponent, VERSION } from "@earendil-works/pi-coding-agent";
3
- import { Markdown, type MarkdownOptions, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui";
4
- import { buildMutedMarkdownTheme } from "./theme.js";
5
-
6
- // Track which pi version we patched against to detect incompatibility
7
- const PATCHED_KEY = Symbol.for("archimedes:thinkingPatched");
8
- const PATCH_VERSION_KEY = Symbol.for("archimedes:thinkingPatchVersion");
9
-
10
- /**
11
- * Patches `AssistantMessageComponent.prototype.updateContent` so thinking
12
- * blocks render with a muted `MarkdownTheme`. Called on every session_start
13
- * to capture a fresh `getTheme` closure (required for /resume).
14
- *
15
- * Re-patches when pi version changes to catch breaking upstream changes.
16
- *
17
- * @param config Optional labelText/labelColor overrides for the thinking
18
- * block header. When omitted (or empty/invalid), the original defaults
19
- * ("Thinking..." / "255,215,0") are used, producing byte-identical output.
20
- */
21
- export function patchThinkingRenderer(
22
- getTheme: () => Theme,
23
- config?: { labelText?: string; labelColor?: string },
24
- ): void {
25
- if (!AssistantMessageComponent) return;
26
-
27
- const proto = AssistantMessageComponent.prototype;
28
- if (
29
- !proto ||
30
- typeof proto.updateContent !== "function" ||
31
- AssistantMessageComponent.name !== "AssistantMessageComponent"
32
- ) {
33
- return;
34
- }
35
-
36
- const src = proto.updateContent.toString();
37
- // NOTE: pi ships the interactive TUI in a minified bundle chunk at runtime, so
38
- // the source we see via .toString() can be `content.type==="thinking"` (no
39
- // spaces) even where dist is readable. The probe below must therefore be
40
- // minification-safe: a whitespace-tolerant regex rather than an exact
41
- // substring. A bare `space === "thinking"` (or any other field) still does
42
- // not match, as required.
43
- const hasThinkingCheck = /content\.type\s*===\s*["']thinking["']/.test(src);
44
- const hasMarkdownTheme = src.includes("this.markdownTheme");
45
- if (!hasThinkingCheck || !hasMarkdownTheme) {
46
- console.warn(
47
- `[archimedes] Skipping thinking renderer patch — signature mismatch
48
- hasThinkingCheck: ${hasThinkingCheck}, hasMarkdownTheme: ${hasMarkdownTheme}
49
- This likely means pi's AssistantMessageComponent changed. The muted theme
50
- for thinking blocks will not be applied.`,
51
- );
52
- return;
53
- }
54
-
55
- // Check if already patched against the current pi version
56
- const currentVersion = VERSION ?? "unknown";
57
- const patchVersion = (proto as any)[PATCH_VERSION_KEY] as string | undefined;
58
- if ((proto as any)[PATCHED_KEY] && patchVersion === currentVersion) {
59
- // Already patched for this version — just update the closure by re-patching
60
- // (needed for /resume to get fresh getTheme)
61
- } else {
62
- // First patch or version changed — warn if version mismatch
63
- if ((proto as any)[PATCHED_KEY] && patchVersion && patchVersion !== currentVersion) {
64
- console.warn(`[archimedes] Re-patching thinking renderer: pi version changed ${patchVersion} → ${currentVersion}`);
65
- }
66
- }
67
-
68
- // Re-patched every session_start — /resume needs a fresh getTheme closure.
69
- //
70
- // Shape: 0.84.3 pi native updateContent:
71
- // updateContent(message, isStreaming = this.isStreaming) {
72
- // this.lastMessage = message;
73
- // this.isStreaming = isStreaming;
74
- // this.contentContainer.clear();
75
- // ...
76
- // // batches consecutive "thinking" parts into thinkingBlocks
77
- // // (skipping empties), i-- after the inner loop,
78
- // // renders as ONE Markdown section of thinkingBlocks.join("\n\n")
79
- // // or ONE static Text label when hidden.
80
- // // stop-reason: const hasToolCalls = content.some(...);
81
- // // this.hasToolCalls = hasToolCalls; (render() uses for OSC-133 zones)
82
- // // stopReason === "length" → Spacer + "truncated" Text
83
- // // else if (!hasToolCalls) { aborted / error branches }
84
- (proto as any).updateContent = function (this: any, message: any, isStreaming?: boolean): void {
85
- this.lastMessage = message;
86
- if (isStreaming !== undefined) this.isStreaming = isStreaming;
87
-
88
- this.markdownTheme.codeBlockIndent = "";
89
- this.contentContainer.clear();
90
-
91
- // Build the thinking-block header from the closure config once per render.
92
- // Defaults preserve the original byte-identical output ("Thinking..." in
93
- // bold truecolor 255,215,0).
94
- const buildThinkingLabel = (): string => {
95
- const label = config?.labelText?.trim() ? config.labelText.trim() : "Thinking...";
96
- const color = config?.labelColor?.trim() ? config.labelColor.trim() : "255,215,0";
97
- const parts = color.split(",").map((p) => p.trim());
98
- // Valid iff exactly three 0..255 components (an "R,G,B" triple).
99
- const valid =
100
- parts.length === 3 &&
101
- parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
102
- const [r, g, b] = valid ? parts : ["255", "215", "0"];
103
- return `\x1b[1m\x1b[38;2;${r};${g};${b}m${label}\x1b[39m\x1b[22m`;
104
- };
105
-
106
- const hasVisibleContent = message.content.some(
107
- (c: any) =>
108
- (c.type === "text" && c.text.trim()) ||
109
- (c.type === "thinking" && c.thinking.trim()),
110
- );
111
-
112
- if (hasVisibleContent) {
113
- this.contentContainer.addChild(new Spacer(1));
114
- }
115
-
116
- // Lazy muted theme: built once per updateContent call.
117
- let mutedTheme: ReturnType<typeof buildMutedMarkdownTheme> | undefined;
118
- let theme: Theme | undefined;
119
- let themeFailed = false;
120
-
121
- const ensureTheme = (): Theme | undefined => {
122
- if (themeFailed) return undefined;
123
- if (!theme) {
124
- try {
125
- theme = getTheme();
126
- } catch {
127
- themeFailed = true;
128
- return undefined;
129
- }
130
- }
131
- return theme;
132
- };
133
-
134
- const ensureMuted = (): MarkdownTheme | undefined => {
135
- if (!mutedTheme) {
136
- const t = ensureTheme();
137
- if (!t) return undefined;
138
- mutedTheme = buildMutedMarkdownTheme(t);
139
- }
140
- return mutedTheme;
141
- };
142
-
143
- // Pi's native updateContent passes a `transform` (createMarkdownTransform)
144
- // to Markdown so the markdown-transformer pipeline runs — that is what
145
- // renders Mermaid blocks to ASCII and applies any extension transformers.
146
- // We must preserve it here, otherwise those transformers are silently
147
- // dropped when this patch replaces updateContent.
148
- //
149
- // NOTE: `createMarkdownTransform` is not exported from pi-coding-agent, so
150
- // we inline an equivalent pipeline over `this.markdownTransformers`.
151
- //
152
- // The `transform` option was added to @earendil-works/pi-tui in 0.84.1.
153
- // At runtime older pi-tui ignores the field, 0.84.1+ honors it.
154
- type MarkdownOptionsWithTransform = MarkdownOptions & {
155
- transform?: (markdown: string, availableWidth: number) => string;
156
- };
157
- const transformFor =
158
- (messageType: "assistant" | "assistant-thinking") =>
159
- (markdown: string, availableWidth: number): string => {
160
- let out = markdown;
161
- for (const transformer of (this as any).markdownTransformers ?? []) {
162
- try {
163
- const transformed = transformer(out, {
164
- messageType,
165
- isStreaming: this.isStreaming,
166
- availableWidth,
167
- });
168
- if (typeof transformed === "string") out = transformed;
169
- } catch {
170
- // Keep the current markdown and continue with the next transformer.
171
- }
172
- }
173
- return out;
174
- };
175
-
176
- // Render content in order.
177
- for (let i = 0; i < message.content.length; i++) {
178
- const content = message.content[i];
179
- if (content.type === "text" && content.text.trim()) {
180
- this.contentContainer.addChild(
181
- new Markdown(
182
- content.text.trim(),
183
- this.outputPad ?? 1,
184
- 0,
185
- this.markdownTheme,
186
- undefined,
187
- { transform: transformFor("assistant") } as MarkdownOptionsWithTransform,
188
- ),
189
- );
190
- } else if (content.type === "thinking") {
191
- // Batch a consecutive run of thinking parts into one section
192
- // (mirrors 0.84.3 pi native behaviour: thinkBlocks, i-- on the
193
- // inner loop, early continue on zero-length runs).
194
- const thinkBlocks: string[] = [];
195
- for (; i < message.content.length; i++) {
196
- const thinkingPart = message.content[i];
197
- if (thinkingPart.type !== "thinking") break;
198
- const trimmed = thinkingPart.thinking.trim();
199
- if (trimmed) thinkBlocks.push(trimmed);
200
- }
201
- i--;
202
- if (thinkBlocks.length === 0) continue;
203
-
204
- const hasVisibleContentAfter = message.content
205
- .slice(i + 1)
206
- .some(
207
- (c: any) =>
208
- (c.type === "text" && c.text.trim()) ||
209
- (c.type === "thinking" && c.thinking.trim()),
210
- );
211
-
212
- if (this.hideThinkingBlock) {
213
- // One static label for the whole run when hidden.
214
- const t = ensureTheme();
215
- if (!t) continue;
216
- this.contentContainer.addChild(
217
- new Text(t.italic(t.fg("thinkingText", this.hiddenThinkingLabel)), this.outputPad ?? 1, 0),
218
- );
219
- if (hasVisibleContentAfter) {
220
- this.contentContainer.addChild(new Spacer(1));
221
- }
222
- } else {
223
- let thinkingContent = thinkBlocks.join("\n\n");
224
- const label = buildThinkingLabel();
225
- if (!thinkingContent.startsWith(label)) {
226
- thinkingContent = `${label}\n\n${thinkingContent}`;
227
- }
228
- const t = ensureTheme();
229
- if (!t) continue;
230
- const muted = ensureMuted();
231
- this.contentContainer.addChild(
232
- new Markdown(
233
- thinkingContent,
234
- this.outputPad ?? 1,
235
- 0,
236
- muted ?? this.markdownTheme,
237
- {
238
- color: (text: string) => t.fg("thinkingText", text),
239
- italic: true,
240
- },
241
- { transform: transformFor("assistant-thinking") } as MarkdownOptionsWithTransform,
242
- ),
243
- );
244
- if (hasVisibleContentAfter) {
245
- this.contentContainer.addChild(new Spacer(1));
246
- }
247
- }
248
- }
249
- }
250
-
251
- // Stop-reason handling — 0.84.3 pi shape. `hasToolCalls` is required by
252
- // the component's render() for OSC-133 prompt zones, so it must be set.
253
- const hasToolCalls = message.content.some((c: any) => c.type === "toolCall");
254
- this.hasToolCalls = hasToolCalls;
255
-
256
- if (message.stopReason === "length") {
257
- this.contentContainer.addChild(new Spacer(1));
258
- const t = ensureTheme();
259
- if (t)
260
- this.contentContainer.addChild(
261
- new Text(t.fg("error", "Response was truncated before completion."), this.outputPad ?? 1, 0),
262
- );
263
- } else if (!hasToolCalls) {
264
- if (message.stopReason === "aborted") {
265
- const abortMessage =
266
- message.errorMessage && message.errorMessage !== "Request was aborted"
267
- ? message.errorMessage
268
- : "Operation aborted";
269
- this.contentContainer.addChild(new Spacer(1));
270
- const t = ensureTheme();
271
- if (t) this.contentContainer.addChild(new Text(t.fg("error", abortMessage), this.outputPad ?? 1, 0));
272
- } else if (message.stopReason === "error") {
273
- const errorMsg = message.errorMessage || "Unknown error";
274
- this.contentContainer.addChild(new Spacer(1));
275
- const t = ensureTheme();
276
- if (t) {
277
- this.contentContainer.addChild(
278
- new Text(t.fg("error", `Error: ${errorMsg}`), this.outputPad ?? 1, 0),
279
- );
280
- }
281
- }
282
- }
283
- };
284
-
285
- // Mark as patched with version for incompatibility detection
286
- (proto as any)[PATCHED_KEY] = true;
287
- (proto as any)[PATCH_VERSION_KEY] = currentVersion;
288
- }