@fyeeme/pi-thinking-ui 1.0.0 → 1.1.0

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/internal-patch.ts DELETED
@@ -1,441 +0,0 @@
1
- import { dirname, join } from "node:path";
2
- import { fileURLToPath, pathToFileURL } from "node:url";
3
- import type { AssistantMessage, ThinkingContent } from "@earendil-works/pi-ai";
4
- import { Markdown, Spacer, Text } from "@earendil-works/pi-tui";
5
- import type { MarkdownTheme } from "@earendil-works/pi-tui";
6
- import { decrementPatchRefCount, getPatchCleanup, getPatchInstallPromise, incrementPatchRefCount, resolveThinkingMessageScope, setPatchCleanup, setPatchInstallPromise } from "./state.ts";
7
- import { ThinkingUIComponent } from "./render.ts";
8
- import type { ThinkingSourceBlock, ThinkingThemeLike } from "./types.ts";
9
-
10
- export const PI_CODING_AGENT_INTERNAL_MODULES = {
11
- assistantMessageComponent: "dist/modes/interactive/components/assistant-message.js",
12
- theme: "dist/modes/interactive/theme/theme.js",
13
- } as const;
14
-
15
- interface AssistantMessageComponentPrototype {
16
- updateContent(message: AssistantMessage): void;
17
- setHideThinkingBlock(hide: boolean): void;
18
- setHiddenThinkingLabel(label: string): void;
19
- contentContainer: {
20
- clear(): void;
21
- addChild(component: unknown): void;
22
- };
23
- lastMessage?: AssistantMessage;
24
- hideThinkingBlock: boolean;
25
- markdownTheme: MarkdownTheme;
26
- hiddenThinkingLabel: string;
27
- }
28
-
29
- export function assertPatchableAssistantMessageComponent(value: unknown): { prototype: AssistantMessageComponentPrototype } {
30
- if (!value || (typeof value !== "function" && typeof value !== "object")) {
31
- throw new Error("Thinking UI patch failed: AssistantMessageComponent export is missing or invalid.");
32
- }
33
-
34
- const prototype = (value as { prototype?: unknown }).prototype;
35
- if (!prototype || typeof prototype !== "object") {
36
- throw new Error("Thinking UI patch failed: AssistantMessageComponent.prototype is missing.");
37
- }
38
-
39
- const candidate = prototype as Record<string, unknown>;
40
- const missingMethods = ["updateContent", "setHideThinkingBlock", "setHiddenThinkingLabel"].filter((name) => typeof candidate[name] !== "function");
41
- if (missingMethods.length > 0) {
42
- throw new Error(`Thinking UI patch failed: AssistantMessageComponent prototype is incompatible (missing ${missingMethods.join(", ")}).`);
43
- }
44
-
45
- return value as { prototype: AssistantMessageComponentPrototype };
46
- }
47
-
48
- export function assertThinkingUITheme(value: unknown): ThinkingThemeLike {
49
- if (!value || typeof value !== "object") {
50
- throw new Error("Thinking UI patch failed: interactive theme export is missing or invalid.");
51
- }
52
-
53
- try {
54
- const candidate = value as Record<string, unknown>;
55
- if (typeof candidate.fg !== "function" || typeof candidate.bold !== "function") {
56
- throw new Error("Thinking UI patch failed: interactive theme export is incompatible.");
57
- }
58
- } catch (error) {
59
- if (error instanceof Error && /Theme not initialized/.test(error.message)) {
60
- return value as ThinkingThemeLike;
61
- }
62
- throw error;
63
- }
64
-
65
- return value as ThinkingThemeLike;
66
- }
67
-
68
- function hasPatchableContentContainer(value: AssistantMessageComponentPrototype): boolean {
69
- return Boolean(
70
- value.contentContainer
71
- && typeof value.contentContainer.clear === "function"
72
- && typeof value.contentContainer.addChild === "function",
73
- );
74
- }
75
-
76
- function getPackageRoot(packageName: string): string {
77
- let entryUrl: string;
78
- try {
79
- entryUrl = import.meta.resolve(packageName);
80
- } catch (error) {
81
- throw new Error(`Thinking UI patch failed: could not resolve ${packageName} package root. Pi internals may be unavailable or moved.`, {
82
- cause: error,
83
- });
84
- }
85
-
86
- try {
87
- const entryPath = fileURLToPath(entryUrl);
88
- return dirname(dirname(entryPath));
89
- } catch (error) {
90
- throw new Error(`Thinking UI patch failed: could not derive ${packageName} package root from ${entryUrl}.`, {
91
- cause: error,
92
- });
93
- }
94
- }
95
-
96
- export function resolvePiCodingAgentInternalModuleUrl(relativePath: string): string {
97
- const packageRoot = getPackageRoot("@earendil-works/pi-coding-agent");
98
- return pathToFileURL(join(packageRoot, relativePath)).href;
99
- }
100
-
101
- export async function importPiCodingAgentInternal<TModule>(relativePath: string): Promise<TModule> {
102
- const moduleUrl = resolvePiCodingAgentInternalModuleUrl(relativePath);
103
- try {
104
- return (await import(moduleUrl)) as TModule;
105
- } catch (error) {
106
- throw new Error(`Thinking UI patch failed: could not import internal module "@earendil-works/pi-coding-agent/${relativePath}". Pi internals may have moved.`, {
107
- cause: error,
108
- });
109
- }
110
- }
111
-
112
- function hasVisibleThinking(content: ThinkingContent): boolean {
113
- return content.redacted === true || content.thinking.trim().length > 0;
114
- }
115
-
116
- function collectThinkingBlocks(message: AssistantMessage): ThinkingSourceBlock[] {
117
- const blocks: ThinkingSourceBlock[] = [];
118
- message.content.forEach((content, index) => {
119
- if (content.type !== "thinking") return;
120
- if (!hasVisibleThinking(content)) return;
121
- blocks.push({
122
- contentIndex: index,
123
- text: content.thinking,
124
- redacted: content.redacted,
125
- });
126
- });
127
- return blocks;
128
- }
129
-
130
- function hasVisibleTextContent(message: AssistantMessage): boolean {
131
- return message.content.some((content) => content.type === "text" && content.text.trim().length > 0);
132
- }
133
-
134
- function hasVisibleThinkingContent(message: AssistantMessage): boolean {
135
- return message.content.some((content) => content.type === "thinking" && hasVisibleThinking(content));
136
- }
137
-
138
- async function installPatch(): Promise<() => void> {
139
- const [{ AssistantMessageComponent: rawAssistantMessageComponent }, { theme: rawTheme }] = await Promise.all([
140
- importPiCodingAgentInternal<{ AssistantMessageComponent: unknown }>(
141
- PI_CODING_AGENT_INTERNAL_MODULES.assistantMessageComponent,
142
- ),
143
- importPiCodingAgentInternal<{ theme: unknown }>(
144
- PI_CODING_AGENT_INTERNAL_MODULES.theme,
145
- ),
146
- ]);
147
-
148
- const AssistantMessageComponent = assertPatchableAssistantMessageComponent(rawAssistantMessageComponent);
149
- const theme = assertThinkingUITheme(rawTheme);
150
- const prototype = AssistantMessageComponent.prototype;
151
- const originalUpdateContent = prototype.updateContent;
152
- const originalSetHideThinkingBlock = prototype.setHideThinkingBlock;
153
- const originalSetHiddenThinkingLabel = prototype.setHiddenThinkingLabel;
154
-
155
- const normalizeHiddenThinkingLabel = (label: string): string => label.replace(/\u2060+$/gu, "");
156
-
157
- const restoreOriginalMethods = (): void => {
158
- if (prototype.updateContent !== originalUpdateContent) {
159
- prototype.updateContent = originalUpdateContent;
160
- }
161
- if (prototype.setHideThinkingBlock !== originalSetHideThinkingBlock) {
162
- prototype.setHideThinkingBlock = originalSetHideThinkingBlock;
163
- }
164
- if (prototype.setHiddenThinkingLabel !== originalSetHiddenThinkingLabel) {
165
- prototype.setHiddenThinkingLabel = originalSetHiddenThinkingLabel;
166
- }
167
- };
168
-
169
- const withOriginalInstanceMethods = <T>(instance: AssistantMessageComponentPrototype, callback: () => T): T => {
170
- const ownUpdateContent = Object.prototype.hasOwnProperty.call(instance, "updateContent");
171
- const ownSetHideThinkingBlock = Object.prototype.hasOwnProperty.call(instance, "setHideThinkingBlock");
172
- const ownSetHiddenThinkingLabel = Object.prototype.hasOwnProperty.call(instance, "setHiddenThinkingLabel");
173
- const previousUpdateContent = instance.updateContent;
174
- const previousSetHideThinkingBlock = instance.setHideThinkingBlock;
175
- const previousSetHiddenThinkingLabel = instance.setHiddenThinkingLabel;
176
-
177
- instance.updateContent = originalUpdateContent;
178
- instance.setHideThinkingBlock = originalSetHideThinkingBlock;
179
- instance.setHiddenThinkingLabel = originalSetHiddenThinkingLabel;
180
-
181
- try {
182
- return callback();
183
- } finally {
184
- if (ownUpdateContent) {
185
- instance.updateContent = previousUpdateContent;
186
- } else {
187
- delete (instance as unknown as Record<string, unknown>).updateContent;
188
- }
189
-
190
- if (ownSetHideThinkingBlock) {
191
- instance.setHideThinkingBlock = previousSetHideThinkingBlock;
192
- } else {
193
- delete (instance as unknown as Record<string, unknown>).setHideThinkingBlock;
194
- }
195
-
196
- if (ownSetHiddenThinkingLabel) {
197
- instance.setHiddenThinkingLabel = previousSetHiddenThinkingLabel;
198
- } else {
199
- delete (instance as unknown as Record<string, unknown>).setHiddenThinkingLabel;
200
- }
201
- }
202
- };
203
-
204
- const reportFallback = (stage: string, error: unknown): void => {
205
- console.warn(`Thinking UI patch warning: falling back to Pi renderer during ${stage}.`, error);
206
- };
207
-
208
- const fallbackErrorMessage = "Thinking UI patch failed: Pi internals are incompatible and fallback rendering also failed.";
209
-
210
- const fallbackToOriginalUpdateContent = (
211
- instance: AssistantMessageComponentPrototype,
212
- message: AssistantMessage,
213
- stage: string,
214
- originalError?: unknown,
215
- ): void => {
216
- try {
217
- withOriginalInstanceMethods(instance, () => {
218
- originalUpdateContent.call(instance, message);
219
- });
220
- } catch (fallbackError) {
221
- throw new Error(fallbackErrorMessage, {
222
- cause: originalError ? { patchError: originalError, fallbackError } : fallbackError,
223
- });
224
- }
225
-
226
- if (originalError) {
227
- reportFallback(stage, originalError);
228
- }
229
- };
230
-
231
- const fallbackToOriginalSetHideThinkingBlock = (
232
- instance: AssistantMessageComponentPrototype,
233
- hide: boolean,
234
- originalError?: unknown,
235
- ): void => {
236
- try {
237
- withOriginalInstanceMethods(instance, () => {
238
- originalSetHideThinkingBlock.call(instance, hide);
239
- });
240
- } catch (fallbackError) {
241
- throw new Error(fallbackErrorMessage, {
242
- cause: originalError ? { patchError: originalError, fallbackError } : fallbackError,
243
- });
244
- }
245
-
246
- if (originalError) {
247
- reportFallback("setHideThinkingBlock", originalError);
248
- }
249
- };
250
-
251
- const fallbackToOriginalSetHiddenThinkingLabel = (
252
- instance: AssistantMessageComponentPrototype,
253
- label: string,
254
- originalError?: unknown,
255
- ): void => {
256
- const normalizedLabel = normalizeHiddenThinkingLabel(label);
257
- try {
258
- withOriginalInstanceMethods(instance, () => {
259
- originalSetHiddenThinkingLabel.call(instance, normalizedLabel);
260
- });
261
- } catch (fallbackError) {
262
- throw new Error(fallbackErrorMessage, {
263
- cause: originalError ? { patchError: originalError, fallbackError } : fallbackError,
264
- });
265
- }
266
-
267
- if (originalError) {
268
- reportFallback("setHiddenThinkingLabel", originalError);
269
- }
270
- };
271
-
272
- const patchedUpdateContent = function patchedUpdateContent(this: AssistantMessageComponentPrototype, message: AssistantMessage): void {
273
- this.lastMessage = message;
274
- if (!hasPatchableContentContainer(this)) {
275
- fallbackToOriginalUpdateContent(this, message, "updateContent");
276
- return;
277
- }
278
-
279
- try {
280
- this.contentContainer.clear();
281
-
282
- const thinkingBlocks = collectThinkingBlocks(message);
283
- const hasVisibleContent = hasVisibleTextContent(message) || thinkingBlocks.length > 0;
284
- if (hasVisibleContent) {
285
- this.contentContainer.addChild(new Spacer(1));
286
- }
287
-
288
- let renderedThinking = false;
289
- const hasVisibleTextAfterThinking = (() => {
290
- const firstThinkingIndex = thinkingBlocks[0]?.contentIndex;
291
- if (firstThinkingIndex === undefined) return false;
292
- return message.content.slice(firstThinkingIndex + 1).some((content) => content.type === "text" && content.text.trim().length > 0);
293
- })();
294
-
295
- for (const content of message.content) {
296
- if (content.type === "text" && content.text.trim()) {
297
- this.contentContainer.addChild(new Markdown(content.text.trim(), 1, 0, this.markdownTheme));
298
- continue;
299
- }
300
-
301
- if (content.type === "thinking" && thinkingBlocks.length > 0 && !renderedThinking) {
302
- this.contentContainer.addChild(new ThinkingUIComponent(theme, message.timestamp, thinkingBlocks, resolveThinkingMessageScope(message)));
303
- renderedThinking = true;
304
- if (hasVisibleTextAfterThinking) {
305
- this.contentContainer.addChild(new Spacer(1));
306
- }
307
- }
308
- }
309
-
310
- const hasToolCalls = message.content.some((content) => content.type === "toolCall");
311
- if (!hasToolCalls) {
312
- if (message.stopReason === "aborted") {
313
- const abortMessage =
314
- message.errorMessage && message.errorMessage !== "Request was aborted"
315
- ? message.errorMessage
316
- : "Operation aborted";
317
- this.contentContainer.addChild(new Spacer(1));
318
- this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
319
- } else if (message.stopReason === "error") {
320
- const errorMessage = message.errorMessage || "Unknown error";
321
- this.contentContainer.addChild(new Spacer(1));
322
- this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
323
- }
324
- }
325
- } catch (error) {
326
- fallbackToOriginalUpdateContent(this, message, "updateContent", error);
327
- }
328
- };
329
-
330
- const patchedSetHideThinkingBlock = function patchedSetHideThinkingBlock(this: AssistantMessageComponentPrototype, hide: boolean): void {
331
- if (!hasPatchableContentContainer(this)) {
332
- fallbackToOriginalSetHideThinkingBlock(this, hide);
333
- return;
334
- }
335
-
336
- this.hideThinkingBlock = false;
337
- if (!this.lastMessage) return;
338
- try {
339
- this.updateContent(this.lastMessage);
340
- } catch (error) {
341
- fallbackToOriginalSetHideThinkingBlock(this, hide, error);
342
- }
343
- };
344
-
345
- const patchedSetHiddenThinkingLabel = function patchedSetHiddenThinkingLabel(
346
- this: AssistantMessageComponentPrototype,
347
- label: string,
348
- ): void {
349
- const normalizedLabel = normalizeHiddenThinkingLabel(label);
350
- if (!hasPatchableContentContainer(this)) {
351
- fallbackToOriginalSetHiddenThinkingLabel(this, normalizedLabel);
352
- return;
353
- }
354
-
355
- this.hiddenThinkingLabel = normalizedLabel;
356
- if (!this.lastMessage) return;
357
- try {
358
- this.updateContent(this.lastMessage);
359
- } catch (error) {
360
- fallbackToOriginalSetHiddenThinkingLabel(this, normalizedLabel, error);
361
- }
362
- };
363
-
364
- try {
365
- prototype.updateContent = patchedUpdateContent;
366
- prototype.setHideThinkingBlock = patchedSetHideThinkingBlock;
367
- prototype.setHiddenThinkingLabel = patchedSetHiddenThinkingLabel;
368
- } catch (error) {
369
- try {
370
- restoreOriginalMethods();
371
- } catch (rollbackError) {
372
- throw new Error("Thinking UI patch failed: AssistantMessageComponent prototype patching failed and rollback was incomplete.", {
373
- cause: { installError: error, rollbackError },
374
- });
375
- }
376
-
377
- throw new Error("Thinking UI patch failed: AssistantMessageComponent prototype is incompatible with thinking-ui patching.", { cause: error });
378
- }
379
-
380
- return () => {
381
- restoreOriginalMethods();
382
- };
383
- }
384
-
385
- export async function retainThinkingUIPatch(): Promise<() => Promise<void>> {
386
- incrementPatchRefCount();
387
- let cleanup = getPatchCleanup();
388
- if (!cleanup) {
389
- const existingInstallPromise = getPatchInstallPromise();
390
- const installPromise = existingInstallPromise ?? installPatch();
391
- if (!existingInstallPromise) {
392
- setPatchInstallPromise(installPromise);
393
- }
394
-
395
- try {
396
- cleanup = await installPromise;
397
- if (!getPatchCleanup()) {
398
- setPatchCleanup(cleanup);
399
- }
400
- } catch (error) {
401
- decrementPatchRefCount();
402
- throw error;
403
- } finally {
404
- if (getPatchInstallPromise() === installPromise) {
405
- setPatchInstallPromise(undefined);
406
- }
407
- }
408
- }
409
-
410
- let released = false;
411
- return async () => {
412
- if (released) return;
413
-
414
- const refCount = decrementPatchRefCount();
415
- if (refCount > 0) {
416
- released = true;
417
- return;
418
- }
419
-
420
- const currentCleanup = getPatchCleanup();
421
- if (!currentCleanup) {
422
- released = true;
423
- return;
424
- }
425
-
426
- if (getPatchCleanup() === currentCleanup) {
427
- setPatchCleanup(undefined);
428
- }
429
-
430
- try {
431
- await currentCleanup();
432
- released = true;
433
- } catch (error) {
434
- incrementPatchRefCount();
435
- if (!getPatchCleanup()) {
436
- setPatchCleanup(currentCleanup);
437
- }
438
- throw error;
439
- }
440
- };
441
- }