@pi-kaush/pi-tool-call-markers 0.3.5 → 0.3.7
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/CHANGELOG.md +80 -0
- package/README.md +35 -8
- package/package.json +1 -1
- package/src/bash-block.ts +33 -137
- package/src/container-hooks.ts +6 -0
- package/src/index.ts +108 -164
- package/src/info-visibility-state.ts +16 -2
- package/src/info-visibility.ts +15 -3
- package/src/muted.ts +31 -16
- package/src/prompt-shell.ts +174 -0
- package/src/question-block.ts +328 -0
- package/src/sanitize.ts +20 -0
- package/src/thinking-block-merger.ts +141 -64
|
@@ -49,56 +49,60 @@ type ThemeDetail = {
|
|
|
49
49
|
// styles current across mid-session theme switches, which fire no event.
|
|
50
50
|
let themeProvider: (() => ThemeDetail | undefined) | undefined;
|
|
51
51
|
let activeLevel: (() => string) | undefined;
|
|
52
|
-
//
|
|
53
|
-
|
|
52
|
+
// Pi builds both thinking labels italic. The default treatment replaces that
|
|
53
|
+
// with a plain row: italic-off always leads — the TUI diff renderer can skip
|
|
54
|
+
// bytes shared with the previous italic frame, which would leave the terminal
|
|
55
|
+
// italic — plus the resolved color when the theme supplies one. The `inherit`
|
|
56
|
+
// variant is the only choice that keeps Pi's native italic styling.
|
|
57
|
+
const ITALIC_OFF = "\x1b[23m";
|
|
54
58
|
|
|
55
59
|
type LabelStyle = { prefix: string; suffix: string };
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
|
|
61
|
+
// The package styles labels only inside a session it can read the theme from.
|
|
62
|
+
// `inherit` is the other native case: it keeps Pi's italic labels.
|
|
63
|
+
function stylesThinkingLabels(): boolean {
|
|
64
|
+
return themeProvider !== undefined && thoughtLabelColorChoice() !== "inherit";
|
|
65
|
+
}
|
|
58
66
|
|
|
59
67
|
function styledWith(
|
|
60
68
|
token: string,
|
|
61
|
-
theme: ThemeDetail | undefined
|
|
69
|
+
theme: ThemeDetail | undefined,
|
|
62
70
|
): LabelStyle | undefined {
|
|
63
|
-
|
|
71
|
+
// Pi's Theme throws on a token it does not know, so probe the way muted.ts
|
|
72
|
+
// does: an unresolvable color costs the label its tint, never the row.
|
|
73
|
+
let ansi: string | undefined;
|
|
74
|
+
try {
|
|
75
|
+
const resolved = theme?.getFgAnsi?.(token);
|
|
76
|
+
if (typeof resolved === "string") ansi = resolved;
|
|
77
|
+
} catch {
|
|
78
|
+
ansi = undefined;
|
|
79
|
+
}
|
|
64
80
|
if (!ansi) return undefined;
|
|
65
|
-
|
|
66
|
-
// restyleHiddenThinkingLabel). The leading italic-off still matters: the
|
|
67
|
-
// TUI's diff renderer can skip bytes shared with the previously drawn
|
|
68
|
-
// italic line, leaving the terminal in italic state otherwise.
|
|
69
|
-
return { prefix: `\x1b[23m${ansi}`, suffix: "\x1b[39m" };
|
|
81
|
+
return { prefix: `${ITALIC_OFF}${ansi}`, suffix: "\x1b[39m" };
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
// The theme's collapsedThinkingCall override when defined, else its comment
|
|
73
85
|
// color, else the muted token.
|
|
74
|
-
function settledMutedStyle(
|
|
75
|
-
|
|
76
|
-
):
|
|
77
|
-
const ansi = theme ? collapsedThinkingAnsi(theme) : null;
|
|
78
|
-
if (ansi) return { prefix: `\x1b[23m${ansi}`, suffix: "\x1b[39m" };
|
|
86
|
+
function settledMutedStyle(theme: ThemeDetail): LabelStyle | undefined {
|
|
87
|
+
const ansi = collapsedThinkingAnsi(theme);
|
|
88
|
+
if (ansi) return { prefix: `${ITALIC_OFF}${ansi}`, suffix: "\x1b[39m" };
|
|
79
89
|
return styledWith("muted", theme);
|
|
80
90
|
}
|
|
81
|
-
function updateThoughtLabelStyle(): void {
|
|
82
|
-
liveLabelStyle = undefined;
|
|
83
|
-
settledLabelStyle = undefined;
|
|
84
|
-
const theme = themeProvider?.();
|
|
85
|
-
stylesTheme = theme;
|
|
86
|
-
const choice = thoughtLabelColorChoice();
|
|
87
|
-
if (choice === "inherit" || !theme) return;
|
|
88
91
|
|
|
92
|
+
// Resolved per label, never cached: Pi swaps the colors behind a stable theme
|
|
93
|
+
// Proxy, so a cached style would survive a theme switch and repaint the
|
|
94
|
+
// transcript in the old palette.
|
|
95
|
+
function labelStyle(
|
|
96
|
+
settled: boolean,
|
|
97
|
+
theme: ThemeDetail,
|
|
98
|
+
): LabelStyle | undefined {
|
|
99
|
+
const choice = thoughtLabelColorChoice();
|
|
100
|
+
if (settled && choice !== "mdheading") return settledMutedStyle(theme);
|
|
89
101
|
const token =
|
|
90
102
|
choice === "mdheading"
|
|
91
103
|
? "mdHeading"
|
|
92
104
|
: (LEVEL_TOKEN[activeLevel?.() ?? "off"] ?? "thinkingOff");
|
|
93
|
-
|
|
94
|
-
settledLabelStyle =
|
|
95
|
-
choice === "mdheading" ? liveLabelStyle : settledMutedStyle(theme);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Recompute label styles whenever the theme object has swapped (theme switch
|
|
99
|
-
// or /reload); those transitions fire no extension event.
|
|
100
|
-
function ensureLabelStyles(): void {
|
|
101
|
-
if (themeProvider?.() !== stylesTheme) updateThoughtLabelStyle();
|
|
105
|
+
return styledWith(token, theme);
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
// The settled label is the finalized "+ Thought" row; every other label is
|
|
@@ -108,13 +112,14 @@ function isSettledThoughtLabel(label: string): boolean {
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
export function visibleThoughtLabel(label: string): string {
|
|
111
|
-
|
|
112
|
-
const style = isSettledThoughtLabel(label)
|
|
113
|
-
? settledLabelStyle
|
|
114
|
-
: liveLabelStyle;
|
|
115
|
-
if (!style) return label;
|
|
115
|
+
if (!stylesThinkingLabels()) return label;
|
|
116
116
|
const raw = label.replace(/\x1b\[[0-9;]*m/g, "");
|
|
117
|
-
|
|
117
|
+
const theme = themeProvider?.();
|
|
118
|
+
const style = theme
|
|
119
|
+
? labelStyle(isSettledThoughtLabel(label), theme)
|
|
120
|
+
: undefined;
|
|
121
|
+
// A theme that cannot resolve a color still drops Pi's italics.
|
|
122
|
+
return style ? `${style.prefix}${raw}${style.suffix}` : `${ITALIC_OFF}${raw}`;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
type AssistantMessageLike = {
|
|
@@ -133,6 +138,28 @@ type TextLikeChild = {
|
|
|
133
138
|
setText?(text: string): void;
|
|
134
139
|
};
|
|
135
140
|
|
|
141
|
+
type MarkdownLikeChild = {
|
|
142
|
+
defaultTextStyle?: { italic?: boolean };
|
|
143
|
+
invalidate?(): void;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Pi renders a visible thinking trace as italic markdown. Italic is the live
|
|
147
|
+
// cue — a settled trace drops it and reads as ordinary transcript text — and
|
|
148
|
+
// the italic default style is what identifies the trace: Pi passes it for
|
|
149
|
+
// thinking content and for nothing else. Callers gate on the row actually
|
|
150
|
+
// carrying thinking, so no other italic block is touched.
|
|
151
|
+
function unitalicizeSettledThinking(row: AssistantMessageRow): void {
|
|
152
|
+
const children = row.contentContainer?.children;
|
|
153
|
+
if (!Array.isArray(children)) return;
|
|
154
|
+
for (const child of children) {
|
|
155
|
+
const markdown = child as MarkdownLikeChild | undefined;
|
|
156
|
+
const style = markdown?.defaultTextStyle;
|
|
157
|
+
if (style?.italic !== true) continue;
|
|
158
|
+
style.italic = false;
|
|
159
|
+
markdown?.invalidate?.();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
// Pi renders the hidden-thinking label as an italic Text node built from the
|
|
137
164
|
// plain label field. Embedding style codes in the field itself is not
|
|
138
165
|
// enough: the TUI diff renderer reuses the byte prefix shared with the
|
|
@@ -149,7 +176,7 @@ function restyleHiddenThinkingLabel(row: AssistantMessageRow): void {
|
|
|
149
176
|
const label = row.hiddenThinkingLabel;
|
|
150
177
|
const children = row.contentContainer?.children;
|
|
151
178
|
if (!Array.isArray(children)) return;
|
|
152
|
-
if (!
|
|
179
|
+
if (!stylesThinkingLabels()) return;
|
|
153
180
|
const styled = visibleThoughtLabel(label);
|
|
154
181
|
for (const child of children) {
|
|
155
182
|
const textChild = child as TextLikeChild | undefined;
|
|
@@ -171,6 +198,10 @@ type ThinkingTiming = {
|
|
|
171
198
|
|
|
172
199
|
type ThinkingGroupingPatchState = {
|
|
173
200
|
owners: number;
|
|
201
|
+
// Set at the final owner's shutdown. A wrapper another extension has buried
|
|
202
|
+
// cannot be uninstalled, so it delegates instead of outliving its owner; a
|
|
203
|
+
// later install re-enables it.
|
|
204
|
+
disabled?: boolean;
|
|
174
205
|
originalUpdateContent: (
|
|
175
206
|
message: AssistantMessageLike,
|
|
176
207
|
...args: unknown[]
|
|
@@ -180,14 +211,39 @@ type ThinkingGroupingPatchState = {
|
|
|
180
211
|
...args: unknown[]
|
|
181
212
|
) => void;
|
|
182
213
|
timings: WeakMap<AssistantMessageRow, ThinkingTiming>;
|
|
183
|
-
// Last update per row, so /toggle-info can replay rows without
|
|
184
|
-
// new content.
|
|
214
|
+
// Last update per tracked row, so /toggle-info can replay rows without
|
|
215
|
+
// waiting for new content. Only rows that carry thinking change under the
|
|
216
|
+
// toggle, and the map is capped so a long session or an abandoned branch
|
|
217
|
+
// cannot pin every message the transcript ever showed.
|
|
185
218
|
rows: Map<
|
|
186
219
|
AssistantMessageRow,
|
|
187
220
|
{ message: AssistantMessageLike; args: unknown[] }
|
|
188
221
|
>;
|
|
189
222
|
};
|
|
190
223
|
|
|
224
|
+
const MAX_REPLAY_ROWS = 400;
|
|
225
|
+
|
|
226
|
+
// Bookkeeping only, so it fails open: a message whose content shape throws
|
|
227
|
+
// must not take the row's update pass down with it.
|
|
228
|
+
function trackReplayRow(
|
|
229
|
+
state: ThinkingGroupingPatchState,
|
|
230
|
+
row: AssistantMessageRow,
|
|
231
|
+
message: AssistantMessageLike,
|
|
232
|
+
args: unknown[],
|
|
233
|
+
): void {
|
|
234
|
+
try {
|
|
235
|
+
if (!hasThinkingContent(message)) return;
|
|
236
|
+
state.rows.set(row, { message, args });
|
|
237
|
+
while (state.rows.size > MAX_REPLAY_ROWS) {
|
|
238
|
+
const oldest = state.rows.keys().next().value;
|
|
239
|
+
if (oldest === undefined) return;
|
|
240
|
+
state.rows.delete(oldest);
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
// The row keeps its native rendering; only /toggle-info replay misses it.
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
191
247
|
type ThinkingContentLike = {
|
|
192
248
|
type: "thinking";
|
|
193
249
|
thinking: string;
|
|
@@ -246,30 +302,40 @@ function thinkingSpinner(startedAt: number, now: number): string {
|
|
|
246
302
|
return PI_SPINNER_FRAMES[frame % PI_SPINNER_FRAMES.length]!;
|
|
247
303
|
}
|
|
248
304
|
|
|
305
|
+
// A row is live while it streams, and stays live through un-flagged rebuilds
|
|
306
|
+
// (resize, theme switch, /reload) until an explicit final update arrives.
|
|
307
|
+
// Rows that never carry the flag — restored history, older runtimes — count
|
|
308
|
+
// as settled.
|
|
309
|
+
function thinkingStillLive(
|
|
310
|
+
streaming: boolean | undefined,
|
|
311
|
+
timing: ThinkingTiming | undefined,
|
|
312
|
+
): boolean {
|
|
313
|
+
if (streaming === true) return true;
|
|
314
|
+
return (
|
|
315
|
+
streaming === undefined &&
|
|
316
|
+
timing !== undefined &&
|
|
317
|
+
timing.finishedAt === undefined
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
249
321
|
function lifecycleLabel(
|
|
250
322
|
row: AssistantMessageRow,
|
|
251
323
|
streaming: boolean | undefined,
|
|
252
324
|
timings: WeakMap<AssistantMessageRow, ThinkingTiming>,
|
|
253
325
|
): string {
|
|
254
326
|
const now = Date.now();
|
|
255
|
-
|
|
256
|
-
if (streaming
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
327
|
+
const previous = timings.get(row);
|
|
328
|
+
if (thinkingStillLive(streaming, previous)) {
|
|
329
|
+
const current =
|
|
330
|
+
previous && previous.finishedAt === undefined
|
|
331
|
+
? previous
|
|
332
|
+
: { startedAt: now };
|
|
333
|
+
timings.set(row, current);
|
|
261
334
|
return `${thinkingSpinner(current.startedAt, now)} Thinking…`;
|
|
262
335
|
}
|
|
263
336
|
|
|
264
|
-
if (streaming ===
|
|
265
|
-
|
|
266
|
-
// the optional streaming flag. Keep its active label until an explicit
|
|
267
|
-
// final update arrives.
|
|
268
|
-
return `${thinkingSpinner(current.startedAt, now)} Thinking…`;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (streaming === false && current?.finishedAt === undefined) {
|
|
272
|
-
if (current) current.finishedAt = now;
|
|
337
|
+
if (streaming === false && previous && previous.finishedAt === undefined) {
|
|
338
|
+
previous.finishedAt = now;
|
|
273
339
|
}
|
|
274
340
|
|
|
275
341
|
const settled = timings.get(row);
|
|
@@ -353,6 +419,7 @@ function installThinkingGroupingPatch():
|
|
|
353
419
|
if (existing) {
|
|
354
420
|
existing.owners++;
|
|
355
421
|
existing.rows ??= new Map();
|
|
422
|
+
existing.disabled = false;
|
|
356
423
|
return existing;
|
|
357
424
|
}
|
|
358
425
|
|
|
@@ -367,9 +434,14 @@ function installThinkingGroupingPatch():
|
|
|
367
434
|
message: AssistantMessageLike,
|
|
368
435
|
...args: unknown[]
|
|
369
436
|
): void {
|
|
370
|
-
// Grouping and
|
|
437
|
+
// Grouping and styling are cosmetic. Each fails open independently, and
|
|
371
438
|
// the original renderer is still called exactly once with every arg.
|
|
372
|
-
state.
|
|
439
|
+
if (state.disabled) {
|
|
440
|
+
Reflect.apply(state.originalUpdateContent, this, [message, ...args]);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
trackReplayRow(state, this, message, args);
|
|
444
|
+
const streaming = typeof args[0] === "boolean" ? args[0] : undefined;
|
|
373
445
|
let combined = message;
|
|
374
446
|
try {
|
|
375
447
|
// Pi builds the hidden label and the visible block only when thinking
|
|
@@ -381,7 +453,6 @@ function installThinkingGroupingPatch():
|
|
|
381
453
|
// Preserve the original message intact.
|
|
382
454
|
}
|
|
383
455
|
try {
|
|
384
|
-
const streaming = typeof args[0] === "boolean" ? args[0] : undefined;
|
|
385
456
|
applyHiddenThinkingLabel(this, combined, streaming, state.timings);
|
|
386
457
|
} catch {
|
|
387
458
|
// Preserve Pi's native label if its private row shape changes.
|
|
@@ -392,6 +463,16 @@ function installThinkingGroupingPatch():
|
|
|
392
463
|
} catch {
|
|
393
464
|
// Keep Pi's native label styling if the row shape changes.
|
|
394
465
|
}
|
|
466
|
+
try {
|
|
467
|
+
if (
|
|
468
|
+
hasThinkingContent(combined) &&
|
|
469
|
+
!thinkingStillLive(streaming, state.timings.get(this))
|
|
470
|
+
) {
|
|
471
|
+
unitalicizeSettledThinking(this);
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
// Keep Pi's native italic trace if the row shape changes.
|
|
475
|
+
}
|
|
395
476
|
};
|
|
396
477
|
|
|
397
478
|
state.patchedUpdateContent = patchedUpdateContent;
|
|
@@ -412,6 +493,7 @@ function uninstallThinkingGroupingPatch(
|
|
|
412
493
|
if (!state || state.owners <= 0) return false;
|
|
413
494
|
state.owners--;
|
|
414
495
|
if (state.owners > 0) return false;
|
|
496
|
+
state.disabled = true;
|
|
415
497
|
const proto =
|
|
416
498
|
AssistantMessageComponent?.prototype as unknown as AssistantMessageRow & {
|
|
417
499
|
[THINKING_GROUPING_PATCHED]?: ThinkingGroupingPatchState;
|
|
@@ -453,17 +535,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
453
535
|
return "off";
|
|
454
536
|
}
|
|
455
537
|
};
|
|
456
|
-
updateThoughtLabelStyle();
|
|
457
538
|
});
|
|
458
|
-
pi.on("thinking_level_select", () => updateThoughtLabelStyle());
|
|
459
539
|
pi.on("session_shutdown", () => {
|
|
460
540
|
if (released) return;
|
|
461
541
|
released = true;
|
|
462
542
|
if (patch && !uninstallThinkingGroupingPatch(patch)) return;
|
|
463
543
|
themeProvider = undefined;
|
|
464
544
|
activeLevel = undefined;
|
|
465
|
-
stylesTheme = undefined;
|
|
466
|
-
liveLabelStyle = undefined;
|
|
467
|
-
settledLabelStyle = undefined;
|
|
468
545
|
});
|
|
469
546
|
}
|