@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
package/src/index.ts
CHANGED
|
@@ -15,15 +15,21 @@ import {
|
|
|
15
15
|
uninstallBashBlockPatch,
|
|
16
16
|
} from "./bash-block.ts";
|
|
17
17
|
import { runChatContainerHooks } from "./container-hooks.ts";
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
installInfoVisibility,
|
|
20
|
+
uninstallInfoVisibility,
|
|
21
|
+
} from "./info-visibility.ts";
|
|
22
|
+
import { fgCollapsed, fgCollapsedRail, paletteSample } from "./muted.ts";
|
|
23
|
+
import { isQuestionToolCall, renderQuestionBlock } from "./question-block.ts";
|
|
24
|
+
import { sanitizeInline } from "./sanitize.ts";
|
|
20
25
|
|
|
21
26
|
const OUTER_INSET = 2;
|
|
22
27
|
const SUBAGENT_MARKER = "↪";
|
|
23
28
|
const GROUP_CALL_MARKER = "│";
|
|
24
29
|
// Collapsed rows anchor with a glyph instead of repeating the tool name;
|
|
25
30
|
// tools that read as one family share a glyph, and unmapped tools fall back
|
|
26
|
-
// to `*`.
|
|
31
|
+
// to `*`. A single space separates the glyph from the call content: the
|
|
32
|
+
// anchor is punctuation enough without a joining colon.
|
|
27
33
|
const TOOL_CALL_GLYPHS: ReadonlyMap<string, string> = new Map([
|
|
28
34
|
["read", "●"],
|
|
29
35
|
["write", "+"],
|
|
@@ -87,13 +93,16 @@ type ToolExecutionRow = {
|
|
|
87
93
|
|
|
88
94
|
type PresentationPatchState = {
|
|
89
95
|
owners: number;
|
|
96
|
+
// Set at the final owner's shutdown. A wrapper that another extension has
|
|
97
|
+
// buried cannot be uninstalled, so it delegates instead of outliving its
|
|
98
|
+
// owner; a later install re-enables it.
|
|
99
|
+
disabled?: boolean;
|
|
90
100
|
theme?: ThemeLike;
|
|
91
101
|
collapseParallel: boolean;
|
|
92
102
|
expandedTools: ReadonlySet<string>;
|
|
93
103
|
groupCache: WeakMap<ToolExecutionRow, GroupRenderCache>;
|
|
94
104
|
collapsedCache: WeakMap<ToolExecutionRow, CollapsedRenderCache>;
|
|
95
105
|
rowVersions: WeakMap<ToolExecutionRow, number>;
|
|
96
|
-
rowGroups: WeakMap<ToolExecutionRow, ToolExecutionRow[]>;
|
|
97
106
|
originalRender: (width: number) => string[];
|
|
98
107
|
originalUpdateDisplay: () => void;
|
|
99
108
|
originalHandleMouse?: (this: ToolExecutionRow, event: unknown) => unknown;
|
|
@@ -153,24 +162,6 @@ function stripAnsi(text: string): string {
|
|
|
153
162
|
return text.replace(ANSI_RE, "");
|
|
154
163
|
}
|
|
155
164
|
|
|
156
|
-
// Command output can carry cursor-moving control bytes, raw terminal
|
|
157
|
-
// sequences, line feeds, and tabs. Collapsed rows are single-line, so result
|
|
158
|
-
// and scraped text is stripped of display sequences and all C0 controls before
|
|
159
|
-
// it can reach a row; otherwise control bytes could split or overwrite it.
|
|
160
|
-
const INLINE_CONTROL_RE = /[\x00-\x1f\x7f]/g;
|
|
161
|
-
// OSC first: the two-byte alternative would otherwise consume `\x1b]` and
|
|
162
|
-
// leave the hyperlink payload behind as visible text. OSC sequences (OSC 8
|
|
163
|
-
// hyperlinks, titles) end at their first BEL/ST terminator; a payload match
|
|
164
|
-
// must stop there too, because the visible text sits BETWEEN two OSC
|
|
165
|
-
// sequences — Pi wraps read paths as `ESC]8;;url ESC\ <path> ESC]8;; ESC\`,
|
|
166
|
-
// and a greedy `[^\x07]*` would swallow that path along with the sequences.
|
|
167
|
-
const DISPLAY_ANSI_RE =
|
|
168
|
-
/\x1b(?:\][^\x07\x1b]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g;
|
|
169
|
-
|
|
170
|
-
function sanitizeInline(text: string): string {
|
|
171
|
-
return text.replace(DISPLAY_ANSI_RE, "").replace(INLINE_CONTROL_RE, " ");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
165
|
function hasVisibleContent(line: string): boolean {
|
|
175
166
|
return stripAnsi(line).trim().length > 0;
|
|
176
167
|
}
|
|
@@ -182,22 +173,12 @@ function rowSignatureOf(row: ToolExecutionRow): string {
|
|
|
182
173
|
return `${row.isPartial}|${row.expanded}|${row.result ? 1 : 0}|${row.result?.isError ? 1 : 0}`;
|
|
183
174
|
}
|
|
184
175
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
176
|
+
// Render caches hold lines with resolved colors baked in, so they key on the
|
|
177
|
+
// palette's values rather than the theme object: Pi swaps the colors behind a
|
|
178
|
+
// stable Proxy, which made an identity key survive a theme switch. Probing is
|
|
179
|
+
// a handful of map lookups per row per frame.
|
|
189
180
|
function themeSampleFor(theme: ThemeLike): string {
|
|
190
|
-
|
|
191
|
-
if (!sample) {
|
|
192
|
-
sample =
|
|
193
|
-
theme.fg("toolTitle", "x") +
|
|
194
|
-
theme.fg("muted", "x") +
|
|
195
|
-
theme.fg("dim", "x") +
|
|
196
|
-
theme.fg("warning", "x") +
|
|
197
|
-
theme.fg("error", "x");
|
|
198
|
-
themeSamples.set(theme, sample);
|
|
199
|
-
}
|
|
200
|
-
return sample;
|
|
181
|
+
return paletteSample(theme);
|
|
201
182
|
}
|
|
202
183
|
|
|
203
184
|
type InsetLayout = {
|
|
@@ -461,10 +442,6 @@ function selfRenderedCallLabel(row: ToolExecutionRow): string {
|
|
|
461
442
|
return title ?? token;
|
|
462
443
|
}
|
|
463
444
|
|
|
464
|
-
function selfRenderedSummary(row: ToolExecutionRow, width: number): string {
|
|
465
|
-
return fitSummary(selfRenderedCallLabel(row), width);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
445
|
function isToolExecutionRow(
|
|
469
446
|
component: unknown,
|
|
470
447
|
): component is ToolExecutionRow & ComponentLike {
|
|
@@ -558,6 +535,16 @@ function hasToolSiblingInAssistantBatch(
|
|
|
558
535
|
return false;
|
|
559
536
|
}
|
|
560
537
|
|
|
538
|
+
// A self-rendered tool opts out of the transcript by drawing no lines. The
|
|
539
|
+
// singleton and grouped paths must agree on that, or grouping resurrects a
|
|
540
|
+
// row the tool deliberately hid.
|
|
541
|
+
function drawsNoRowLines(
|
|
542
|
+
row: ToolExecutionRow,
|
|
543
|
+
nativeLines: string[],
|
|
544
|
+
): boolean {
|
|
545
|
+
return nativeLines.length === 0 && (row.imageComponents?.length ?? 0) === 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
561
548
|
function isGroupableToolRow(
|
|
562
549
|
row: ToolExecutionRow,
|
|
563
550
|
children: unknown[],
|
|
@@ -566,6 +553,8 @@ function isGroupableToolRow(
|
|
|
566
553
|
state: PresentationPatchState,
|
|
567
554
|
): boolean {
|
|
568
555
|
if (row.toolName === "subagent") return false;
|
|
556
|
+
if (isQuestionToolCall(row)) return false;
|
|
557
|
+
if (drawsNoRowLines(row, renderAt(index))) return false;
|
|
569
558
|
if (
|
|
570
559
|
!isLiveRow(row) &&
|
|
571
560
|
!isCollapsibleSuccess(row) &&
|
|
@@ -796,11 +785,6 @@ function renderedCallSummary(
|
|
|
796
785
|
width: number,
|
|
797
786
|
theme: ThemeLike,
|
|
798
787
|
): string {
|
|
799
|
-
// Self-rendered call components change shape across the live/settled
|
|
800
|
-
// boundary (the adapter's call disappears once settled), so always build
|
|
801
|
-
// the summary from the stable args label instead of scraping the preview.
|
|
802
|
-
if (row.getRenderShell?.() === "self") return selfRenderedSummary(row, width);
|
|
803
|
-
|
|
804
788
|
let component = row.callRendererComponent;
|
|
805
789
|
if (!component && Array.isArray(row.contentBox?.children)) {
|
|
806
790
|
component = row.contentBox.children[0] as ComponentLike | undefined;
|
|
@@ -851,56 +835,60 @@ function renderedCallSummary(
|
|
|
851
835
|
: fgCollapsed(theme, "muted", "(no arguments)");
|
|
852
836
|
}
|
|
853
837
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const match = /^(\S+)(.*)$/s.exec(plain);
|
|
861
|
-
if (!match) return fgCollapsed(theme, color, plain);
|
|
862
|
-
const rest = match[2] ?? "";
|
|
863
|
-
// The colon joins the bolded name to its content. Failed rows stay
|
|
864
|
-
// uniformly error-colored; settled rows use the collapsed mute — same
|
|
865
|
-
// color for the bolded name and the call content.
|
|
866
|
-
return (
|
|
867
|
-
fgCollapsed(theme, color, match[1] ?? "", true) +
|
|
868
|
-
(rest ? fgCollapsed(theme, color, `:${rest}`) : "")
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function collapsedCallLabel(
|
|
838
|
+
// The collapsed-row seam lives here and nowhere else: which anchor a row
|
|
839
|
+
// takes, how that anchor is styled, and the call text that follows it.
|
|
840
|
+
// Anchors always render bold and take no joining colon — `│ ● src/a.ts`,
|
|
841
|
+
// `│ $ npm test` — and subagent fallbacks keep their `↪ subagent …`
|
|
842
|
+
// reading instead of a glyph. Callers append the outcome tail.
|
|
843
|
+
function collapsedSeam(
|
|
873
844
|
row: ToolExecutionRow,
|
|
874
845
|
width: number,
|
|
875
846
|
theme: ThemeLike,
|
|
876
|
-
color
|
|
877
|
-
): string {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
847
|
+
color: string,
|
|
848
|
+
): { anchor: string; content: string } {
|
|
849
|
+
const isBash = row.toolName === "bash";
|
|
850
|
+
const isSubagent = row.toolName === "subagent";
|
|
851
|
+
const glyph = isBash
|
|
852
|
+
? "$"
|
|
853
|
+
: (TOOL_CALL_GLYPHS.get(row.toolName ?? "") ?? "*");
|
|
854
|
+
const anchor = isSubagent
|
|
855
|
+
? `${fgCollapsed(theme, color, SUBAGENT_MARKER, true)} `
|
|
856
|
+
: `${fgCollapsedRail(theme, GROUP_CALL_MARKER)} ${fgCollapsed(
|
|
857
|
+
theme,
|
|
858
|
+
color,
|
|
859
|
+
glyph,
|
|
860
|
+
true,
|
|
861
|
+
)} `;
|
|
862
|
+
const budget = Math.max(1, width - visibleWidth(anchor));
|
|
863
|
+
// Self-rendered rows build their own `tool {args}` label — Pi's call title
|
|
864
|
+
// is gone once settled — while ordinary rows scrape the rendered call
|
|
865
|
+
// line, which already drops its heading token.
|
|
866
|
+
const selfRendered = row.getRenderShell?.() === "self";
|
|
867
|
+
const source = selfRendered
|
|
868
|
+
? selfRenderedCallLabel(row)
|
|
869
|
+
: renderedCallSummary(row, budget, theme);
|
|
870
|
+
const plain = sanitizeInline(stripAnsi(source)).trim();
|
|
871
|
+
// The anchor stands in for the label's leading token. Only self-rendered
|
|
872
|
+
// labels still carry it; a subagent fallback keeps `subagent` as its own
|
|
873
|
+
// heading, which the scrape dropped.
|
|
874
|
+
const text = isSubagent
|
|
875
|
+
? `subagent${plain ? ` ${plain}` : ""}`
|
|
876
|
+
: selfRendered
|
|
877
|
+
? dropCallToken(plain, row.toolName ?? "tool")
|
|
878
|
+
: plain;
|
|
879
|
+
return {
|
|
880
|
+
anchor,
|
|
881
|
+
content: fgCollapsed(theme, color, text || "(no arguments)"),
|
|
882
|
+
};
|
|
887
883
|
}
|
|
888
884
|
|
|
889
|
-
//
|
|
890
|
-
// the
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
color: string,
|
|
897
|
-
): string {
|
|
898
|
-
if (row.toolName === "bash" || row.toolName === "subagent") return "";
|
|
899
|
-
return fgCollapsed(
|
|
900
|
-
theme,
|
|
901
|
-
color,
|
|
902
|
-
`${TOOL_CALL_GLYPHS.get(row.toolName ?? "") ?? "*"}: `,
|
|
903
|
-
);
|
|
885
|
+
// Drops the leading token an anchor now stands for, with whichever separator
|
|
886
|
+
// the label joined it with.
|
|
887
|
+
function dropCallToken(plain: string, token: string): string {
|
|
888
|
+
if (!plain.startsWith(token)) return plain;
|
|
889
|
+
const rest = plain.slice(token.length);
|
|
890
|
+
if (!/^:?\s|^:$/.test(rest)) return plain;
|
|
891
|
+
return rest.replace(/^:?\s*/, "");
|
|
904
892
|
}
|
|
905
893
|
|
|
906
894
|
function collapsedOutcome(
|
|
@@ -927,31 +915,28 @@ function collapsedOutcome(
|
|
|
927
915
|
return renderedOutcome(row, theme);
|
|
928
916
|
}
|
|
929
917
|
|
|
930
|
-
|
|
918
|
+
// One collapsed row — head and outcome tail — at the given width. Singleton
|
|
919
|
+
// and grouped rows differ only in what surrounds the line.
|
|
920
|
+
function collapsedRowLine(
|
|
931
921
|
row: ToolExecutionRow,
|
|
932
922
|
width: number,
|
|
933
923
|
theme: ThemeLike,
|
|
934
924
|
): string {
|
|
935
925
|
// Failed rows render entirely in error so the row reads as the one that
|
|
936
926
|
// failed, not just its outcome tail. Only the row's anchor stays bold.
|
|
937
|
-
const
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
? `${fgCollapsed(theme, tone, SUBAGENT_MARKER)} `
|
|
941
|
-
: `${fgCollapsedRail(theme, GROUP_CALL_MARKER)} `;
|
|
942
|
-
const glyph = collapsedCallMarker(row, theme, tone);
|
|
943
|
-
const budget = Math.max(1, width - visibleWidth(marker));
|
|
944
|
-
const label = glyph + collapsedChildLabel(row, budget, theme, tone);
|
|
927
|
+
const color = rowHasFailed(row) ? "error" : "muted";
|
|
928
|
+
const seam = collapsedSeam(row, width, theme, color);
|
|
929
|
+
const budget = Math.max(1, width - visibleWidth(seam.anchor));
|
|
945
930
|
const outcome = collapsedOutcome(row, budget, theme);
|
|
946
931
|
// The truncation suffix inherits the row tone; pi-tui's truncation resets
|
|
947
932
|
// around a plain suffix, which would render it in the terminal default
|
|
948
933
|
// foreground instead of the row color.
|
|
949
|
-
const suffix = fgCollapsed(theme,
|
|
934
|
+
const suffix = fgCollapsed(theme, color, "…");
|
|
950
935
|
return (
|
|
951
|
-
|
|
936
|
+
seam.anchor +
|
|
952
937
|
(outcome
|
|
953
|
-
? fitSummaryTail(
|
|
954
|
-
: fitSummary(
|
|
938
|
+
? fitSummaryTail(seam.content, outcome, budget, suffix)
|
|
939
|
+
: fitSummary(seam.content, budget, suffix))
|
|
955
940
|
);
|
|
956
941
|
}
|
|
957
942
|
|
|
@@ -1246,45 +1231,20 @@ function renderCollapsedToolRow(
|
|
|
1246
1231
|
width: number,
|
|
1247
1232
|
theme: ThemeLike,
|
|
1248
1233
|
): string[] {
|
|
1234
|
+
// An asked question is a user-input moment, not an execution row: it takes
|
|
1235
|
+
// the submitted-prompt shell, which carries its own inset.
|
|
1236
|
+
const questionBlock = renderQuestionBlock(row, width, theme);
|
|
1237
|
+
if (questionBlock) return ["", ...questionBlock];
|
|
1249
1238
|
const layout = insetLayout(width);
|
|
1250
1239
|
const plan =
|
|
1251
1240
|
row.toolName === "subagent"
|
|
1252
1241
|
? renderSubagentPlan(row, layout.contentWidth, theme)
|
|
1253
1242
|
: undefined;
|
|
1254
|
-
const body = plan ?? [
|
|
1243
|
+
const body = plan ?? [collapsedRowLine(row, layout.contentWidth, theme)];
|
|
1255
1244
|
body.push(...imageResultLines(row, layout.contentWidth, theme));
|
|
1256
1245
|
return ["", ...insetLines(body, width)];
|
|
1257
1246
|
}
|
|
1258
1247
|
|
|
1259
|
-
// Drops the call label's tool-name token: the row's glyph anchors the call
|
|
1260
|
-
// instead. The `↪` subagent fallback keeps its own heading.
|
|
1261
|
-
function collapsedChildLabel(
|
|
1262
|
-
row: ToolExecutionRow,
|
|
1263
|
-
width: number,
|
|
1264
|
-
theme: ThemeLike,
|
|
1265
|
-
color: string = "muted",
|
|
1266
|
-
): string {
|
|
1267
|
-
const label = collapsedCallLabel(row, width, theme, color);
|
|
1268
|
-
if (row.toolName === "subagent") return label;
|
|
1269
|
-
const plain = stripAnsi(label);
|
|
1270
|
-
const toolName = row.toolName ?? "tool";
|
|
1271
|
-
let prefix = "";
|
|
1272
|
-
if (plain === toolName) {
|
|
1273
|
-
prefix = toolName;
|
|
1274
|
-
} else if (plain.startsWith(`${toolName}:`)) {
|
|
1275
|
-
const whitespace = /^\s*/.exec(plain.slice(toolName.length + 1))?.[0] ?? "";
|
|
1276
|
-
prefix = `${toolName}:${whitespace}`;
|
|
1277
|
-
}
|
|
1278
|
-
const prefixWidth = visibleWidth(prefix);
|
|
1279
|
-
const child = sliceByColumn(
|
|
1280
|
-
label,
|
|
1281
|
-
prefixWidth,
|
|
1282
|
-
Math.max(0, visibleWidth(label) - prefixWidth),
|
|
1283
|
-
true,
|
|
1284
|
-
);
|
|
1285
|
-
return child || fgCollapsed(theme, color, "(no arguments)");
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
1248
|
function renderGroupedCallLines(
|
|
1289
1249
|
rows: ToolExecutionRow[],
|
|
1290
1250
|
width: number,
|
|
@@ -1292,26 +1252,7 @@ function renderGroupedCallLines(
|
|
|
1292
1252
|
): Array<{ row: ToolExecutionRow; lines: string[] }> {
|
|
1293
1253
|
const segments: Array<{ row: ToolExecutionRow; lines: string[] }> = [];
|
|
1294
1254
|
for (const row of rows) {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
const color = rowHasFailed(row) ? "error" : "muted";
|
|
1298
|
-
const prefix = `${fgCollapsedRail(theme, GROUP_CALL_MARKER)} `;
|
|
1299
|
-
const marker = collapsedCallMarker(row, theme, color);
|
|
1300
|
-
const budget = Math.max(
|
|
1301
|
-
1,
|
|
1302
|
-
width - visibleWidth(prefix) - visibleWidth(marker),
|
|
1303
|
-
);
|
|
1304
|
-
const label = collapsedChildLabel(row, budget, theme, color);
|
|
1305
|
-
const outcome = collapsedOutcome(row, budget, theme);
|
|
1306
|
-
const suffix = fgCollapsed(theme, color, "…");
|
|
1307
|
-
lines.push(
|
|
1308
|
-
prefix +
|
|
1309
|
-
marker +
|
|
1310
|
-
(outcome
|
|
1311
|
-
? fitSummaryTail(label, outcome, budget, suffix)
|
|
1312
|
-
: fitSummary(label, budget, suffix)),
|
|
1313
|
-
);
|
|
1314
|
-
segments.push({ row, lines });
|
|
1255
|
+
segments.push({ row, lines: [collapsedRowLine(row, width, theme)] });
|
|
1315
1256
|
}
|
|
1316
1257
|
return segments;
|
|
1317
1258
|
}
|
|
@@ -1471,7 +1412,6 @@ function renderContainerWithToolGroups(
|
|
|
1471
1412
|
continue;
|
|
1472
1413
|
}
|
|
1473
1414
|
|
|
1474
|
-
for (const member of group) presentation.rowGroups.set(member, group);
|
|
1475
1415
|
// Use a live member while any call is pending, then the first member once
|
|
1476
1416
|
// settled. Either way, the grouped row keeps the same number of lines.
|
|
1477
1417
|
const shellRow = group.find(isLiveRow) ?? child;
|
|
@@ -1592,6 +1532,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1592
1532
|
const existing = proto[PRESENTATION_PATCHED];
|
|
1593
1533
|
if (existing) {
|
|
1594
1534
|
existing.owners++;
|
|
1535
|
+
existing.disabled = false;
|
|
1595
1536
|
return existing;
|
|
1596
1537
|
}
|
|
1597
1538
|
const legacy = proto[LEGACY_PRESENTATION_PATCHED];
|
|
@@ -1612,13 +1553,16 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1612
1553
|
groupCache: new WeakMap(),
|
|
1613
1554
|
collapsedCache: new WeakMap(),
|
|
1614
1555
|
rowVersions: new WeakMap(),
|
|
1615
|
-
rowGroups: new WeakMap(),
|
|
1616
1556
|
originalRender: proto.render,
|
|
1617
1557
|
originalUpdateDisplay: proto.updateDisplay,
|
|
1618
1558
|
};
|
|
1619
1559
|
const patchedUpdateDisplay = function updateDisplayWithToolPresentation(
|
|
1620
1560
|
this: ToolExecutionRow,
|
|
1621
1561
|
): void {
|
|
1562
|
+
if (state.disabled) {
|
|
1563
|
+
state.originalUpdateDisplay.call(this);
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1622
1566
|
// Transcript rerenders reuse settled caches, but a real component
|
|
1623
1567
|
// display update can change args, results, renderer output, or images
|
|
1624
1568
|
// without changing the row's shape signature.
|
|
@@ -1651,6 +1595,9 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1651
1595
|
event: unknown,
|
|
1652
1596
|
): unknown {
|
|
1653
1597
|
const mouseEvent = event as { type?: unknown; button?: unknown };
|
|
1598
|
+
if (state.disabled) {
|
|
1599
|
+
return state.originalHandleMouse?.call(this, event);
|
|
1600
|
+
}
|
|
1654
1601
|
if (
|
|
1655
1602
|
state.theme &&
|
|
1656
1603
|
this.expanded === false &&
|
|
@@ -1672,6 +1619,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1672
1619
|
): string[] {
|
|
1673
1620
|
const theme = state.theme;
|
|
1674
1621
|
if (
|
|
1622
|
+
state.disabled ||
|
|
1675
1623
|
typeof this.expanded !== "boolean" ||
|
|
1676
1624
|
typeof this.isPartial !== "boolean" ||
|
|
1677
1625
|
this.expanded ||
|
|
@@ -1709,7 +1657,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1709
1657
|
return cached.lines;
|
|
1710
1658
|
}
|
|
1711
1659
|
const lines = state.originalRender.call(this, width);
|
|
1712
|
-
if (
|
|
1660
|
+
if (drawsNoRowLines(this, lines)) {
|
|
1713
1661
|
return lines;
|
|
1714
1662
|
}
|
|
1715
1663
|
try {
|
|
@@ -1730,7 +1678,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1730
1678
|
// Partial (streaming) rows keep the collapsed presentation but are
|
|
1731
1679
|
// never cached: their content changes with every streamed chunk.
|
|
1732
1680
|
const lines = state.originalRender.call(this, width);
|
|
1733
|
-
if (
|
|
1681
|
+
if (drawsNoRowLines(this, lines)) {
|
|
1734
1682
|
return lines;
|
|
1735
1683
|
}
|
|
1736
1684
|
try {
|
|
@@ -1767,6 +1715,7 @@ function uninstallPresentationPatch(
|
|
|
1767
1715
|
if (!state || state.owners <= 0) return;
|
|
1768
1716
|
state.owners--;
|
|
1769
1717
|
if (state.owners > 0) return;
|
|
1718
|
+
state.disabled = true;
|
|
1770
1719
|
const proto =
|
|
1771
1720
|
ToolExecutionComponent?.prototype as unknown as ToolExecutionRow & {
|
|
1772
1721
|
[PRESENTATION_PATCHED]?: PresentationPatchState;
|
|
@@ -1817,17 +1766,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1817
1766
|
ctx.ui.setToolsExpanded(false);
|
|
1818
1767
|
});
|
|
1819
1768
|
|
|
1820
|
-
pi.on("session_start", (_event, ctx) => {
|
|
1821
|
-
if (patch) patch.theme = ctx.ui.theme;
|
|
1822
|
-
if (bashPatch) bashPatch.theme = ctx.ui.theme;
|
|
1823
|
-
ctx.ui.setToolsExpanded(false);
|
|
1824
|
-
});
|
|
1825
|
-
|
|
1826
1769
|
pi.on("session_shutdown", () => {
|
|
1827
1770
|
if (released) return;
|
|
1828
1771
|
released = true;
|
|
1829
1772
|
uninstallGroupingPatch(grouping);
|
|
1830
1773
|
uninstallPresentationPatch(patch);
|
|
1831
1774
|
uninstallBashBlockPatch(bashPatch);
|
|
1775
|
+
uninstallInfoVisibility();
|
|
1832
1776
|
});
|
|
1833
1777
|
}
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
// duplicated between the main entrypoint (the /toggle-info command) and the
|
|
5
5
|
// thinking-block-merger entrypoint (the strip). globalThis keeps it singular.
|
|
6
6
|
|
|
7
|
-
type InfoVisibilityState = { hidden: boolean };
|
|
7
|
+
type InfoVisibilityState = { hidden: boolean; owners: number };
|
|
8
8
|
|
|
9
9
|
const STATE_KEY = Symbol.for("kg.pi.toolCallMarkers.infoVisibility.v1");
|
|
10
10
|
|
|
11
11
|
function infoVisibilityState(): InfoVisibilityState {
|
|
12
12
|
const root = globalThis as Record<symbol, InfoVisibilityState | undefined>;
|
|
13
|
-
return (root[STATE_KEY] ??= { hidden: false });
|
|
13
|
+
return (root[STATE_KEY] ??= { hidden: false, owners: 0 });
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function infoVisibilityHidden(): boolean {
|
|
@@ -20,3 +20,17 @@ export function infoVisibilityHidden(): boolean {
|
|
|
20
20
|
export function setInfoVisibilityHidden(hidden: boolean): void {
|
|
21
21
|
infoVisibilityState().hidden = hidden;
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
// The filter hook lives in a process-global registry, so it is owned like the
|
|
25
|
+
// prototype patches: it outlives every install that loaded this module and
|
|
26
|
+
// disappears with the last one.
|
|
27
|
+
export function retainInfoVisibilityOwner(): void {
|
|
28
|
+
infoVisibilityState().owners += 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Returns the owners left after this release.
|
|
32
|
+
export function releaseInfoVisibilityOwner(): number {
|
|
33
|
+
const state = infoVisibilityState();
|
|
34
|
+
state.owners = Math.max(0, state.owners - 1);
|
|
35
|
+
return state.owners;
|
|
36
|
+
}
|
package/src/info-visibility.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
import { chatContainerHooks } from "./container-hooks.ts";
|
|
7
7
|
import {
|
|
8
8
|
infoVisibilityHidden,
|
|
9
|
+
releaseInfoVisibilityOwner,
|
|
10
|
+
retainInfoVisibilityOwner,
|
|
9
11
|
setInfoVisibilityHidden,
|
|
10
12
|
} from "./info-visibility-state.ts";
|
|
11
13
|
import { refreshThinkingVisibility } from "./thinking-block-merger.ts";
|
|
@@ -42,6 +44,7 @@ function infoFilterHook(
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export function installInfoVisibility(pi: ExtensionAPI): void {
|
|
47
|
+
retainInfoVisibilityOwner();
|
|
45
48
|
// The hook registry is process-global; dedupe by name so hot reloads swap
|
|
46
49
|
// rather than stack.
|
|
47
50
|
const hooks = chatContainerHooks();
|
|
@@ -67,8 +70,17 @@ export function installInfoVisibility(pi: ExtensionAPI): void {
|
|
|
67
70
|
},
|
|
68
71
|
});
|
|
69
72
|
|
|
70
|
-
// Every session starts in the default collapsed-but-visible state.
|
|
71
|
-
// shutdown reset: extra shutdown handlers would sit between this package's
|
|
72
|
-
// owner-counted patch releases.
|
|
73
|
+
// Every session starts in the default collapsed-but-visible state.
|
|
73
74
|
pi.on("session_start", () => setInfoVisibilityHidden(false));
|
|
74
75
|
}
|
|
76
|
+
|
|
77
|
+
// Released from the entrypoint's own shutdown handler, so a session still
|
|
78
|
+
// tears down with one handler per install. The registry and the toggle are
|
|
79
|
+
// process-global: while any owner remains, the hook stays; the last owner
|
|
80
|
+
// removes it, so execution rows cannot keep disappearing from a transcript
|
|
81
|
+
// this package no longer renders.
|
|
82
|
+
export function uninstallInfoVisibility(): void {
|
|
83
|
+
if (releaseInfoVisibilityOwner() > 0) return;
|
|
84
|
+
chatContainerHooks().delete(infoFilterHook);
|
|
85
|
+
setInfoVisibilityHidden(false);
|
|
86
|
+
}
|
package/src/muted.ts
CHANGED
|
@@ -19,27 +19,42 @@ type CollapsedTheme = {
|
|
|
19
19
|
getFgAnsi?(color: string): string;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
// Probed live, never cached: Pi swaps the colors behind a stable theme
|
|
23
|
+
// Proxy, so a value cached against the theme object outlives a theme switch.
|
|
24
|
+
// A probe is one map lookup, and the Theme throws on tokens it does not know
|
|
25
|
+
// (themes without getFgAnsi have no overrides at all).
|
|
27
26
|
function tokenAnsi(theme: CollapsedTheme, token: string): string | null {
|
|
28
|
-
let cache = ansiCache.get(theme);
|
|
29
|
-
if (!cache) {
|
|
30
|
-
cache = new Map();
|
|
31
|
-
ansiCache.set(theme, cache);
|
|
32
|
-
}
|
|
33
|
-
if (cache.has(token)) return cache.get(token)!;
|
|
34
|
-
let ansi: string | null = null;
|
|
35
27
|
try {
|
|
36
28
|
const resolved = theme.getFgAnsi?.(token);
|
|
37
|
-
|
|
29
|
+
return typeof resolved === "string" ? resolved : null;
|
|
38
30
|
} catch {
|
|
39
|
-
|
|
31
|
+
return null;
|
|
40
32
|
}
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The tokens this package resolves from a palette.
|
|
36
|
+
const PALETTE_TOKENS = [
|
|
37
|
+
TOOL_OVERRIDE_TOKEN,
|
|
38
|
+
THINKING_OVERRIDE_TOKEN,
|
|
39
|
+
DEFAULT_TOKEN,
|
|
40
|
+
"muted",
|
|
41
|
+
"toolOutput",
|
|
42
|
+
"accent",
|
|
43
|
+
"warning",
|
|
44
|
+
"error",
|
|
45
|
+
// Asked-question blocks paint with the prompt-shell tokens: the rail, the
|
|
46
|
+
// question text, and the surface behind both.
|
|
47
|
+
"borderAccent",
|
|
48
|
+
"text",
|
|
49
|
+
"userMessageBg",
|
|
50
|
+
"userMessageText",
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
// Revision key for rendered output that bakes resolved colors in. Compares
|
|
54
|
+
// the colors themselves, so a mid-session theme switch is visible to every
|
|
55
|
+
// cache that keys on it.
|
|
56
|
+
export function paletteSample(theme: CollapsedTheme): string {
|
|
57
|
+
return PALETTE_TOKENS.map((token) => tokenAnsi(theme, token) ?? "").join(",");
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
// Override token when defined, else the theme's syntaxComment color, else
|