@pi-kaush/pi-tool-call-markers 0.2.8 → 0.2.10
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 +29 -0
- package/README.md +27 -4
- package/package.json +1 -1
- package/src/bash-block.ts +303 -0
- package/src/index.ts +229 -65
- package/src/info-visibility-state.ts +22 -0
- package/src/info-visibility.ts +74 -0
- package/src/thinking-block-merger.ts +111 -136
|
@@ -1,48 +1,39 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { infoVisibilityHidden } from "./info-visibility-state.ts";
|
|
3
4
|
|
|
4
|
-
const THINKING_GROUPING_PATCHED = Symbol.for("kg.pi.thinkingGrouping.
|
|
5
|
+
const THINKING_GROUPING_PATCHED = Symbol.for("kg.pi.thinkingGrouping.v2");
|
|
5
6
|
const PI_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
6
7
|
const SPINNER_INTERVAL_MS = 80;
|
|
7
8
|
|
|
8
9
|
// Visual experiment for the settled "+ Thought" label. "inherit" keeps Pi's
|
|
9
|
-
// native styling (italic + thinkingText); "
|
|
10
|
-
//
|
|
11
|
-
//
|
|
10
|
+
// native styling (italic + thinkingText); "mdheading" rides the theme's
|
|
11
|
+
// mdHeading token; the default tints the label with the session's active
|
|
12
|
+
// thinking-level color (thinkingOff…thinkingMax), so the collapsed label
|
|
13
|
+
// quietly advertises the level Pi is reasoning at. Only the collapsed label
|
|
14
|
+
// is restyled — the expanded thinking block keeps Pi's thinkingText. The env
|
|
15
|
+
// var overrides the default so variants can be compared without republishing.
|
|
12
16
|
const THOUGHT_LABEL_COLOR_ENV = "PI_TOOL_CALL_MARKERS_THOUGHT_COLOR";
|
|
13
|
-
type ThoughtLabelColor = "inherit" | "
|
|
14
|
-
const DEFAULT_THOUGHT_LABEL_COLOR: ThoughtLabelColor = "
|
|
15
|
-
// cobalt2's orange token (#ffb86c).
|
|
16
|
-
const THOUGHT_LABEL_ORANGE = { r: 255, g: 184, b: 108 };
|
|
17
|
+
type ThoughtLabelColor = "inherit" | "level" | "mdheading";
|
|
18
|
+
const DEFAULT_THOUGHT_LABEL_COLOR: ThoughtLabelColor = "level";
|
|
17
19
|
|
|
18
20
|
function thoughtLabelColorChoice(): ThoughtLabelColor {
|
|
19
21
|
const raw = process.env[THOUGHT_LABEL_COLOR_ENV];
|
|
20
|
-
return raw === "inherit" || raw === "
|
|
22
|
+
return raw === "inherit" || raw === "level" || raw === "mdheading"
|
|
21
23
|
? raw
|
|
22
24
|
: DEFAULT_THOUGHT_LABEL_COLOR;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
[8, "#808080"],
|
|
36
|
-
[9, "#ff0000"],
|
|
37
|
-
[10, "#00ff00"],
|
|
38
|
-
[11, "#ffff00"],
|
|
39
|
-
[12, "#0000ff"],
|
|
40
|
-
[13, "#ff00ff"],
|
|
41
|
-
[14, "#00ffff"],
|
|
42
|
-
[15, "#ffffff"],
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
type Rgb = { r: number; g: number; b: number };
|
|
27
|
+
// thinkingLevel values map onto theme tokens: "xhigh" → thinkingXhigh etc.
|
|
28
|
+
const LEVEL_TOKEN: Record<string, string> = {
|
|
29
|
+
off: "thinkingOff",
|
|
30
|
+
minimal: "thinkingMinimal",
|
|
31
|
+
low: "thinkingLow",
|
|
32
|
+
medium: "thinkingMedium",
|
|
33
|
+
high: "thinkingHigh",
|
|
34
|
+
xhigh: "thinkingXhigh",
|
|
35
|
+
max: "thinkingMax",
|
|
36
|
+
};
|
|
46
37
|
|
|
47
38
|
type ThemeDetail = {
|
|
48
39
|
fg(color: string, text: string): string;
|
|
@@ -51,96 +42,7 @@ type ThemeDetail = {
|
|
|
51
42
|
};
|
|
52
43
|
|
|
53
44
|
let activeTheme: ThemeDetail | undefined;
|
|
54
|
-
|
|
55
|
-
function hexToRgb(hex: string): Rgb | undefined {
|
|
56
|
-
const match = /^#([0-9a-f]{6})$/i.exec(hex.trim());
|
|
57
|
-
if (!match) return undefined;
|
|
58
|
-
const value = parseInt(match[1]!, 16);
|
|
59
|
-
return { r: value >> 16, g: (value >> 8) & 255, b: value & 255 };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Mirrors Pi's own mapping so a named color can round-trip through a
|
|
63
|
-
// 256-color terminal (e.g. when COLORFGBG forces 256color mode).
|
|
64
|
-
function ansi256ToRgb(index: number): Rgb | undefined {
|
|
65
|
-
if (index < 16) {
|
|
66
|
-
const hex = BASIC_ANSI_RGB[index]?.[1];
|
|
67
|
-
return hex ? hexToRgb(hex) : undefined;
|
|
68
|
-
}
|
|
69
|
-
if (index < 232) {
|
|
70
|
-
const cube = index - 16;
|
|
71
|
-
const channel = (n: number) => (n === 0 ? 0 : 55 + n * 40);
|
|
72
|
-
return {
|
|
73
|
-
r: channel(Math.floor(cube / 36)),
|
|
74
|
-
g: channel(Math.floor((cube % 36) / 6)),
|
|
75
|
-
b: channel(cube % 6),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
const gray = 8 + (index - 232) * 10;
|
|
79
|
-
return { r: gray, g: gray, b: gray };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function rgbToAnsi256({ r, g, b }: Rgb): number {
|
|
83
|
-
const nearest = (value: number) =>
|
|
84
|
-
CUBE_VALUES.reduce(
|
|
85
|
-
(best, candidate) =>
|
|
86
|
-
Math.abs(candidate - value) < Math.abs(best - value) ? candidate : best,
|
|
87
|
-
0,
|
|
88
|
-
);
|
|
89
|
-
const cubeR = nearest(r);
|
|
90
|
-
const cubeG = nearest(g);
|
|
91
|
-
const cubeB = nearest(b);
|
|
92
|
-
const cubeDistance =
|
|
93
|
-
(r - cubeR) ** 2 * 0.299 +
|
|
94
|
-
(g - cubeG) ** 2 * 0.587 +
|
|
95
|
-
(b - cubeB) ** 2 * 0.114;
|
|
96
|
-
const luminance = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
|
97
|
-
const grayStep = Math.max(0, Math.min(23, Math.round((luminance - 8) / 10)));
|
|
98
|
-
const grayValue = 8 + grayStep * 10;
|
|
99
|
-
const grayDistance = (luminance - grayValue) ** 2;
|
|
100
|
-
const spread = Math.max(r, g, b) - Math.min(r, g, b);
|
|
101
|
-
if (spread < 10 && grayDistance < cubeDistance) return 232 + grayStep;
|
|
102
|
-
return (
|
|
103
|
-
16 +
|
|
104
|
-
36 * CUBE_VALUES.indexOf(cubeR) +
|
|
105
|
-
6 * CUBE_VALUES.indexOf(cubeG) +
|
|
106
|
-
CUBE_VALUES.indexOf(cubeB)
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function parseAnsiFgRgb(ansi: string): Rgb | undefined {
|
|
111
|
-
if (/^\x1b\[39m$/.test(ansi)) return undefined;
|
|
112
|
-
const truecolor = /^\x1b\[38;2;(\d+);(\d+);(\d+)m$/.exec(ansi);
|
|
113
|
-
if (truecolor) {
|
|
114
|
-
return {
|
|
115
|
-
r: Number(truecolor[1]),
|
|
116
|
-
g: Number(truecolor[2]),
|
|
117
|
-
b: Number(truecolor[3]),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
const indexed = /^\x1b\[38;5;(\d+)m$/.exec(ansi);
|
|
121
|
-
if (indexed) return ansi256ToRgb(Number(indexed[1]));
|
|
122
|
-
const basic = /^\x1b\[(9[0-7]|3[0-7])m$/.exec(ansi);
|
|
123
|
-
if (basic) {
|
|
124
|
-
const code = Number(basic[1]);
|
|
125
|
-
return ansi256ToRgb(code >= 90 ? code - 90 + 8 : code - 30);
|
|
126
|
-
}
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function fgRgbAnsi({ r, g, b }: Rgb): string {
|
|
131
|
-
if (activeTheme?.getColorMode?.() === "256color") {
|
|
132
|
-
return `\x1b[38;5;${rgbToAnsi256({ r, g, b })}m`;
|
|
133
|
-
}
|
|
134
|
-
return `\x1b[38;2;${r};${g};${b}m`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function midpointRgb(a: Rgb, b: Rgb): Rgb {
|
|
138
|
-
return {
|
|
139
|
-
r: Math.round((a.r + b.r) / 2),
|
|
140
|
-
g: Math.round((a.g + b.g) / 2),
|
|
141
|
-
b: Math.round((a.b + b.b) / 2),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
45
|
+
let activeLevel: (() => string) | undefined;
|
|
144
46
|
|
|
145
47
|
let thoughtLabelPrefix = "";
|
|
146
48
|
let thoughtLabelSuffix = "";
|
|
@@ -151,21 +53,19 @@ function updateThoughtLabelStyle(): void {
|
|
|
151
53
|
const choice = thoughtLabelColorChoice();
|
|
152
54
|
if (choice === "inherit" || !activeTheme) return;
|
|
153
55
|
|
|
154
|
-
let
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
if (!rgb) return;
|
|
56
|
+
let ansi: string | undefined;
|
|
57
|
+
const token =
|
|
58
|
+
choice === "mdheading"
|
|
59
|
+
? "mdHeading"
|
|
60
|
+
: (LEVEL_TOKEN[activeLevel?.() ?? "off"] ?? "thinkingOff");
|
|
61
|
+
ansi = activeTheme.getFgAnsi?.(token);
|
|
62
|
+
if (!ansi) return;
|
|
163
63
|
|
|
164
64
|
// The styled label replaces Pi's italicized Text node wholesale (see
|
|
165
65
|
// restyleHiddenThinkingLabel). The leading italic-off still matters: the
|
|
166
66
|
// TUI's diff renderer can skip bytes shared with the previously drawn
|
|
167
67
|
// italic line, leaving the terminal in italic state otherwise.
|
|
168
|
-
thoughtLabelPrefix = `\x1b[23m${
|
|
68
|
+
thoughtLabelPrefix = `\x1b[23m${ansi}`;
|
|
169
69
|
thoughtLabelSuffix = "\x1b[39m";
|
|
170
70
|
}
|
|
171
71
|
|
|
@@ -228,6 +128,7 @@ type ThinkingTiming = {
|
|
|
228
128
|
};
|
|
229
129
|
|
|
230
130
|
type ThinkingGroupingPatchState = {
|
|
131
|
+
owners: number;
|
|
231
132
|
originalUpdateContent: (
|
|
232
133
|
message: AssistantMessageLike,
|
|
233
134
|
...args: unknown[]
|
|
@@ -237,6 +138,12 @@ type ThinkingGroupingPatchState = {
|
|
|
237
138
|
...args: unknown[]
|
|
238
139
|
) => void;
|
|
239
140
|
timings: WeakMap<AssistantMessageRow, ThinkingTiming>;
|
|
141
|
+
// Last update per row, so /toggle-info can replay rows without waiting for
|
|
142
|
+
// new content. Cleared on session start; rows are session-lived anyway.
|
|
143
|
+
rows: Map<
|
|
144
|
+
AssistantMessageRow,
|
|
145
|
+
{ message: AssistantMessageLike; args: unknown[] }
|
|
146
|
+
>;
|
|
240
147
|
};
|
|
241
148
|
|
|
242
149
|
type ThinkingContentLike = {
|
|
@@ -330,6 +237,40 @@ function lifecycleLabel(
|
|
|
330
237
|
return "+ Thought";
|
|
331
238
|
}
|
|
332
239
|
|
|
240
|
+
function stripThinkingBlocks(
|
|
241
|
+
message: AssistantMessageLike,
|
|
242
|
+
): AssistantMessageLike {
|
|
243
|
+
const content = message.content;
|
|
244
|
+
if (!Array.isArray(content) || !content.some(isThinkingContent)) {
|
|
245
|
+
return message;
|
|
246
|
+
}
|
|
247
|
+
return { ...message, content: content.filter((c) => !isThinkingContent(c)) };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Replays the last update on every tracked assistant row so a /toggle-info
|
|
251
|
+
// flip applies immediately instead of waiting for the next content pass.
|
|
252
|
+
export function refreshThinkingVisibility(): void {
|
|
253
|
+
try {
|
|
254
|
+
const proto = AssistantMessageComponent?.prototype as unknown as {
|
|
255
|
+
[THINKING_GROUPING_PATCHED]?: ThinkingGroupingPatchState;
|
|
256
|
+
};
|
|
257
|
+
const state = proto?.[THINKING_GROUPING_PATCHED];
|
|
258
|
+
if (!state?.patchedUpdateContent) return;
|
|
259
|
+
for (const [row, tracked] of state.rows) {
|
|
260
|
+
try {
|
|
261
|
+
Reflect.apply(state.patchedUpdateContent, row, [
|
|
262
|
+
tracked.message,
|
|
263
|
+
...tracked.args,
|
|
264
|
+
]);
|
|
265
|
+
} catch {
|
|
266
|
+
// A row that changed shape mid-flight keeps its last render.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// Cosmetic replay; never break the toggle.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
333
274
|
function applyHiddenThinkingLabel(
|
|
334
275
|
row: AssistantMessageRow,
|
|
335
276
|
message: AssistantMessageLike,
|
|
@@ -367,11 +308,17 @@ function installThinkingGroupingPatch():
|
|
|
367
308
|
if (!proto || typeof proto.updateContent !== "function") return undefined;
|
|
368
309
|
|
|
369
310
|
const existing = proto[THINKING_GROUPING_PATCHED];
|
|
370
|
-
if (existing)
|
|
311
|
+
if (existing) {
|
|
312
|
+
existing.owners++;
|
|
313
|
+
existing.rows ??= new Map();
|
|
314
|
+
return existing;
|
|
315
|
+
}
|
|
371
316
|
|
|
372
317
|
const state: ThinkingGroupingPatchState = {
|
|
318
|
+
owners: 1,
|
|
373
319
|
originalUpdateContent: proto.updateContent,
|
|
374
320
|
timings: new WeakMap(),
|
|
321
|
+
rows: new Map(),
|
|
375
322
|
};
|
|
376
323
|
const patchedUpdateContent = function updateContentWithCombinedThinking(
|
|
377
324
|
this: AssistantMessageRow,
|
|
@@ -380,9 +327,14 @@ function installThinkingGroupingPatch():
|
|
|
380
327
|
): void {
|
|
381
328
|
// Grouping and labels are cosmetic. Each fails open independently, and
|
|
382
329
|
// the original renderer is still called exactly once with every arg.
|
|
330
|
+
state.rows.set(this, { message, args });
|
|
383
331
|
let combined = message;
|
|
384
332
|
try {
|
|
385
|
-
|
|
333
|
+
// Pi builds the hidden label and the visible block only when thinking
|
|
334
|
+
// blocks exist, so stripping them hides both at once.
|
|
335
|
+
combined = infoVisibilityHidden()
|
|
336
|
+
? stripThinkingBlocks(message)
|
|
337
|
+
: combineAdjacentThinking(message);
|
|
386
338
|
} catch {
|
|
387
339
|
// Preserve the original message intact.
|
|
388
340
|
}
|
|
@@ -414,8 +366,10 @@ function installThinkingGroupingPatch():
|
|
|
414
366
|
|
|
415
367
|
function uninstallThinkingGroupingPatch(
|
|
416
368
|
state: ThinkingGroupingPatchState | undefined,
|
|
417
|
-
):
|
|
418
|
-
if (!state) return;
|
|
369
|
+
): boolean {
|
|
370
|
+
if (!state || state.owners <= 0) return false;
|
|
371
|
+
state.owners--;
|
|
372
|
+
if (state.owners > 0) return false;
|
|
419
373
|
const proto =
|
|
420
374
|
AssistantMessageComponent?.prototype as unknown as AssistantMessageRow & {
|
|
421
375
|
[THINKING_GROUPING_PATCHED]?: ThinkingGroupingPatchState;
|
|
@@ -428,23 +382,44 @@ function uninstallThinkingGroupingPatch(
|
|
|
428
382
|
proto[THINKING_GROUPING_PATCHED] !== state ||
|
|
429
383
|
proto.updateContent !== state.patchedUpdateContent
|
|
430
384
|
) {
|
|
431
|
-
return;
|
|
385
|
+
return true;
|
|
432
386
|
}
|
|
433
387
|
proto.updateContent = state.originalUpdateContent;
|
|
434
388
|
delete proto[THINKING_GROUPING_PATCHED];
|
|
389
|
+
return true;
|
|
435
390
|
}
|
|
436
391
|
|
|
437
392
|
export default function (pi: ExtensionAPI) {
|
|
438
393
|
const patch = installThinkingGroupingPatch();
|
|
394
|
+
let released = false;
|
|
439
395
|
pi.on("session_start", (_event, ctx) => {
|
|
440
396
|
if (ctx.mode !== "tui") return;
|
|
397
|
+
try {
|
|
398
|
+
const proto = AssistantMessageComponent?.prototype as unknown as {
|
|
399
|
+
[THINKING_GROUPING_PATCHED]?: ThinkingGroupingPatchState;
|
|
400
|
+
};
|
|
401
|
+
proto?.[THINKING_GROUPING_PATCHED]?.rows.clear();
|
|
402
|
+
} catch {
|
|
403
|
+
// Best effort; stale rows would only linger until the process exits.
|
|
404
|
+
}
|
|
441
405
|
activeTheme = ctx.ui.theme as unknown as ThemeDetail;
|
|
406
|
+
activeLevel = () => {
|
|
407
|
+
try {
|
|
408
|
+
return pi.getThinkingLevel();
|
|
409
|
+
} catch {
|
|
410
|
+
return "off";
|
|
411
|
+
}
|
|
412
|
+
};
|
|
442
413
|
updateThoughtLabelStyle();
|
|
443
414
|
});
|
|
415
|
+
pi.on("thinking_level_select", () => updateThoughtLabelStyle());
|
|
444
416
|
pi.on("session_shutdown", () => {
|
|
417
|
+
if (released) return;
|
|
418
|
+
released = true;
|
|
419
|
+
if (patch && !uninstallThinkingGroupingPatch(patch)) return;
|
|
445
420
|
activeTheme = undefined;
|
|
421
|
+
activeLevel = undefined;
|
|
446
422
|
thoughtLabelPrefix = "";
|
|
447
423
|
thoughtLabelSuffix = "";
|
|
448
|
-
uninstallThinkingGroupingPatch(patch);
|
|
449
424
|
});
|
|
450
425
|
}
|