@pi-kaush/pi-tool-call-markers 0.3.4 → 0.3.6
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 +56 -0
- package/README.md +26 -14
- package/package.json +1 -1
- package/src/bash-block.ts +22 -3
- package/src/container-hooks.ts +6 -0
- package/src/index.ts +168 -209
- package/src/info-visibility-state.ts +16 -2
- package/src/info-visibility.ts +15 -3
- package/src/muted.ts +29 -24
- package/src/thinking-block-merger.ts +141 -64
package/src/index.ts
CHANGED
|
@@ -15,17 +15,38 @@ 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";
|
|
20
23
|
|
|
21
24
|
const OUTER_INSET = 2;
|
|
22
25
|
const SUBAGENT_MARKER = "↪";
|
|
23
26
|
const GROUP_CALL_MARKER = "│";
|
|
27
|
+
// Collapsed rows anchor with a glyph instead of repeating the tool name;
|
|
28
|
+
// tools that read as one family share a glyph, and unmapped tools fall back
|
|
29
|
+
// to `*`. A single space separates the glyph from the call content: the
|
|
30
|
+
// anchor is punctuation enough without a joining colon.
|
|
31
|
+
const TOOL_CALL_GLYPHS: ReadonlyMap<string, string> = new Map([
|
|
32
|
+
["read", "●"],
|
|
33
|
+
["write", "+"],
|
|
34
|
+
["edit", "±"],
|
|
35
|
+
["grep", "○"],
|
|
36
|
+
["ffgrep", "○"],
|
|
37
|
+
["find", "○"],
|
|
38
|
+
["fffind", "○"],
|
|
39
|
+
["ls", "≡"],
|
|
40
|
+
["web_search", "↗"],
|
|
41
|
+
["fetch_content", "↗"],
|
|
42
|
+
["get_search_content", "↗"],
|
|
43
|
+
]);
|
|
24
44
|
const PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v3");
|
|
25
45
|
const LEGACY_PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v2");
|
|
26
46
|
const GROUPING_PATCHED = Symbol.for("kg.pi.toolGrouping.v1");
|
|
27
47
|
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
28
48
|
const COLLAPSE_PARALLEL_ENV = "PI_TOOL_CALL_MARKERS_COLLAPSE_PARALLEL";
|
|
49
|
+
const EXPANDED_TOOLS_ENV = "PI_ALWAYS_EXPANDED_TOOL_CALL_MARKERS";
|
|
29
50
|
|
|
30
51
|
type ThemeLike = {
|
|
31
52
|
bold(text: string): string;
|
|
@@ -70,12 +91,16 @@ type ToolExecutionRow = {
|
|
|
70
91
|
|
|
71
92
|
type PresentationPatchState = {
|
|
72
93
|
owners: number;
|
|
94
|
+
// Set at the final owner's shutdown. A wrapper that another extension has
|
|
95
|
+
// buried cannot be uninstalled, so it delegates instead of outliving its
|
|
96
|
+
// owner; a later install re-enables it.
|
|
97
|
+
disabled?: boolean;
|
|
73
98
|
theme?: ThemeLike;
|
|
74
99
|
collapseParallel: boolean;
|
|
100
|
+
expandedTools: ReadonlySet<string>;
|
|
75
101
|
groupCache: WeakMap<ToolExecutionRow, GroupRenderCache>;
|
|
76
102
|
collapsedCache: WeakMap<ToolExecutionRow, CollapsedRenderCache>;
|
|
77
103
|
rowVersions: WeakMap<ToolExecutionRow, number>;
|
|
78
|
-
rowGroups: WeakMap<ToolExecutionRow, ToolExecutionRow[]>;
|
|
79
104
|
originalRender: (width: number) => string[];
|
|
80
105
|
originalUpdateDisplay: () => void;
|
|
81
106
|
originalHandleMouse?: (this: ToolExecutionRow, event: unknown) => unknown;
|
|
@@ -94,9 +119,7 @@ type GroupingPatchState = {
|
|
|
94
119
|
|
|
95
120
|
type GroupRenderCache = {
|
|
96
121
|
lines: string[];
|
|
97
|
-
//
|
|
98
|
-
// member absorbs the leading blank and the tool-name header, later
|
|
99
|
-
// members carry their own line (plus edit diffs).
|
|
122
|
+
// The first member absorbs the leading blank; each member owns its line.
|
|
100
123
|
heights: Array<{ component: ToolExecutionRow; height: number }>;
|
|
101
124
|
members: ToolExecutionRow[];
|
|
102
125
|
memberVersions: number[];
|
|
@@ -120,6 +143,19 @@ function envEnabled(name: string, defaultValue: boolean): boolean {
|
|
|
120
143
|
return defaultValue;
|
|
121
144
|
}
|
|
122
145
|
|
|
146
|
+
// Comma-separated tool names, e.g. `edit,write`. Blank entries and padding
|
|
147
|
+
// are ignored; an unset or empty variable yields no names.
|
|
148
|
+
function envToolNames(name: string): ReadonlySet<string> {
|
|
149
|
+
const value = process.env[name]?.trim();
|
|
150
|
+
if (!value) return new Set();
|
|
151
|
+
return new Set(
|
|
152
|
+
value
|
|
153
|
+
.split(",")
|
|
154
|
+
.map((entry) => entry.trim())
|
|
155
|
+
.filter(Boolean),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
123
159
|
function stripAnsi(text: string): string {
|
|
124
160
|
return text.replace(ANSI_RE, "");
|
|
125
161
|
}
|
|
@@ -153,22 +189,12 @@ function rowSignatureOf(row: ToolExecutionRow): string {
|
|
|
153
189
|
return `${row.isPartial}|${row.expanded}|${row.result ? 1 : 0}|${row.result?.isError ? 1 : 0}`;
|
|
154
190
|
}
|
|
155
191
|
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
192
|
+
// Render caches hold lines with resolved colors baked in, so they key on the
|
|
193
|
+
// palette's values rather than the theme object: Pi swaps the colors behind a
|
|
194
|
+
// stable Proxy, which made an identity key survive a theme switch. Probing is
|
|
195
|
+
// a handful of map lookups per row per frame.
|
|
160
196
|
function themeSampleFor(theme: ThemeLike): string {
|
|
161
|
-
|
|
162
|
-
if (!sample) {
|
|
163
|
-
sample =
|
|
164
|
-
theme.fg("toolTitle", "x") +
|
|
165
|
-
theme.fg("muted", "x") +
|
|
166
|
-
theme.fg("dim", "x") +
|
|
167
|
-
theme.fg("warning", "x") +
|
|
168
|
-
theme.fg("error", "x");
|
|
169
|
-
themeSamples.set(theme, sample);
|
|
170
|
-
}
|
|
171
|
-
return sample;
|
|
197
|
+
return paletteSample(theme);
|
|
172
198
|
}
|
|
173
199
|
|
|
174
200
|
type InsetLayout = {
|
|
@@ -432,10 +458,6 @@ function selfRenderedCallLabel(row: ToolExecutionRow): string {
|
|
|
432
458
|
return title ?? token;
|
|
433
459
|
}
|
|
434
460
|
|
|
435
|
-
function selfRenderedSummary(row: ToolExecutionRow, width: number): string {
|
|
436
|
-
return fitSummary(selfRenderedCallLabel(row), width);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
461
|
function isToolExecutionRow(
|
|
440
462
|
component: unknown,
|
|
441
463
|
): component is ToolExecutionRow & ComponentLike {
|
|
@@ -497,6 +519,18 @@ function isCollapsibleSuccess(row: ToolExecutionRow): boolean {
|
|
|
497
519
|
);
|
|
498
520
|
}
|
|
499
521
|
|
|
522
|
+
// Settled failures join groups like settled successes; the row's error
|
|
523
|
+
// color carries the failure, so the group stays visually unbroken.
|
|
524
|
+
function isCollapsibleFailure(row: ToolExecutionRow): boolean {
|
|
525
|
+
return (
|
|
526
|
+
row.expanded === false &&
|
|
527
|
+
row.isPartial === false &&
|
|
528
|
+
!!row.result &&
|
|
529
|
+
rowHasFailed(row) &&
|
|
530
|
+
!hasImageResult(row)
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
500
534
|
function hasToolSiblingInAssistantBatch(
|
|
501
535
|
children: unknown[],
|
|
502
536
|
index: number,
|
|
@@ -517,6 +551,16 @@ function hasToolSiblingInAssistantBatch(
|
|
|
517
551
|
return false;
|
|
518
552
|
}
|
|
519
553
|
|
|
554
|
+
// A self-rendered tool opts out of the transcript by drawing no lines. The
|
|
555
|
+
// singleton and grouped paths must agree on that, or grouping resurrects a
|
|
556
|
+
// row the tool deliberately hid.
|
|
557
|
+
function drawsNoRowLines(
|
|
558
|
+
row: ToolExecutionRow,
|
|
559
|
+
nativeLines: string[],
|
|
560
|
+
): boolean {
|
|
561
|
+
return nativeLines.length === 0 && (row.imageComponents?.length ?? 0) === 0;
|
|
562
|
+
}
|
|
563
|
+
|
|
520
564
|
function isGroupableToolRow(
|
|
521
565
|
row: ToolExecutionRow,
|
|
522
566
|
children: unknown[],
|
|
@@ -525,7 +569,13 @@ function isGroupableToolRow(
|
|
|
525
569
|
state: PresentationPatchState,
|
|
526
570
|
): boolean {
|
|
527
571
|
if (row.toolName === "subagent") return false;
|
|
528
|
-
if (
|
|
572
|
+
if (drawsNoRowLines(row, renderAt(index))) return false;
|
|
573
|
+
if (
|
|
574
|
+
!isLiveRow(row) &&
|
|
575
|
+
!isCollapsibleSuccess(row) &&
|
|
576
|
+
!isCollapsibleFailure(row)
|
|
577
|
+
)
|
|
578
|
+
return false;
|
|
529
579
|
return (
|
|
530
580
|
state.collapseParallel ||
|
|
531
581
|
!hasToolSiblingInAssistantBatch(children, index, renderAt)
|
|
@@ -612,15 +662,8 @@ function diffCounts(diff: string): { added: number; removed: number } {
|
|
|
612
662
|
}
|
|
613
663
|
|
|
614
664
|
// Pi's edit tool (renderShell "self") stores its display diff in
|
|
615
|
-
// result.details.diff
|
|
616
|
-
//
|
|
617
|
-
// normally keep only their one-line call label; edits additionally show
|
|
618
|
-
// the change as a bounded diff block under the summary and a +a/-b stat in
|
|
619
|
-
// the outcome tail, so the hunk stays visible without Ctrl+O while
|
|
620
|
-
// expanded rows keep Pi's native rendering.
|
|
621
|
-
const EDIT_DIFF_LINE_RE = /^([+\-\s])(\s*\d*)(.*)$/;
|
|
622
|
-
const MAX_EDIT_DIFF_LINES = 12;
|
|
623
|
-
|
|
665
|
+
// result.details.diff; collapsed rows show only its +a/-b stat, while the
|
|
666
|
+
// full hunk returns with Ctrl+O.
|
|
624
667
|
function editDiffText(row: ToolExecutionRow): string | undefined {
|
|
625
668
|
if (row.toolName !== "edit") return undefined;
|
|
626
669
|
if (!row.result || row.isPartial !== false || rowHasFailed(row))
|
|
@@ -636,43 +679,6 @@ function renderedEditDiffStat(row: ToolExecutionRow): string | undefined {
|
|
|
636
679
|
return added + removed === 0 ? "applied" : `+${added}/-${removed}`;
|
|
637
680
|
}
|
|
638
681
|
|
|
639
|
-
function renderedEditDiffLines(
|
|
640
|
-
row: ToolExecutionRow,
|
|
641
|
-
theme: ThemeLike,
|
|
642
|
-
): string[] {
|
|
643
|
-
const diff = editDiffText(row);
|
|
644
|
-
if (diff === undefined) return [];
|
|
645
|
-
const raw = diff.split("\n");
|
|
646
|
-
if (raw[raw.length - 1] === "") raw.pop();
|
|
647
|
-
const lines: string[] = [];
|
|
648
|
-
for (const line of raw.slice(0, MAX_EDIT_DIFF_LINES)) {
|
|
649
|
-
const clean = sanitizeInline(line).trimEnd();
|
|
650
|
-
if (clean.trim() === "...") {
|
|
651
|
-
lines.push(fgCollapsed(theme, "muted", " ..."));
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
const match = EDIT_DIFF_LINE_RE.exec(clean);
|
|
655
|
-
if (!match) continue;
|
|
656
|
-
const color =
|
|
657
|
-
match[1] === "+"
|
|
658
|
-
? "toolDiffAdded"
|
|
659
|
-
: match[1] === "-"
|
|
660
|
-
? "toolDiffRemoved"
|
|
661
|
-
: "toolDiffContext";
|
|
662
|
-
lines.push(theme.fg(color, ` ${match[1]}${match[2]}${match[3]}`));
|
|
663
|
-
}
|
|
664
|
-
if (raw.length > MAX_EDIT_DIFF_LINES) {
|
|
665
|
-
lines.push(
|
|
666
|
-
fgCollapsed(
|
|
667
|
-
theme,
|
|
668
|
-
"muted",
|
|
669
|
-
` ... +${raw.length - MAX_EDIT_DIFF_LINES} more`,
|
|
670
|
-
),
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
return lines;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
682
|
function outcomeSummary(row: ToolExecutionRow): string | undefined {
|
|
677
683
|
// Self-rendered tools own their result framing; the built-in heuristics
|
|
678
684
|
// (line counts, diff stats) describe default-shell rows only, so their
|
|
@@ -724,22 +730,6 @@ function renderedOutcome(
|
|
|
724
730
|
return fgCollapsed(theme, "toolOutput", `→ ${summary}`);
|
|
725
731
|
}
|
|
726
732
|
|
|
727
|
-
function renderedGroupedOutcome(
|
|
728
|
-
row: ToolExecutionRow,
|
|
729
|
-
theme: ThemeLike,
|
|
730
|
-
): string | undefined {
|
|
731
|
-
const outcome = renderedOutcome(row, theme);
|
|
732
|
-
if (outcome) return outcome;
|
|
733
|
-
if (isLiveRow(row)) {
|
|
734
|
-
const elapsed = liveElapsedText(row);
|
|
735
|
-
return theme.fg("warning", elapsed ? `… · ${elapsed}` : "…");
|
|
736
|
-
}
|
|
737
|
-
// Self-rendered tools have no per-tool outcome summary; keep the group's
|
|
738
|
-
// settled tail consistent with collapsed singletons.
|
|
739
|
-
if (row.getRenderShell?.() === "self") return renderedGenericOutcome(theme);
|
|
740
|
-
return undefined;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
733
|
// A budget too small for the suffix keeps literal styled text instead, so a
|
|
744
734
|
// one-column label never collapses into a bare ellipsis or loses its status.
|
|
745
735
|
function truncateStyled(text: string, width: number, suffix = "…"): string {
|
|
@@ -810,11 +800,6 @@ function renderedCallSummary(
|
|
|
810
800
|
width: number,
|
|
811
801
|
theme: ThemeLike,
|
|
812
802
|
): string {
|
|
813
|
-
// Self-rendered call components change shape across the live/settled
|
|
814
|
-
// boundary (the adapter's call disappears once settled), so always build
|
|
815
|
-
// the summary from the stable args label instead of scraping the preview.
|
|
816
|
-
if (row.getRenderShell?.() === "self") return selfRenderedSummary(row, width);
|
|
817
|
-
|
|
818
803
|
let component = row.callRendererComponent;
|
|
819
804
|
if (!component && Array.isArray(row.contentBox?.children)) {
|
|
820
805
|
component = row.contentBox.children[0] as ComponentLike | undefined;
|
|
@@ -865,39 +850,60 @@ function renderedCallSummary(
|
|
|
865
850
|
: fgCollapsed(theme, "muted", "(no arguments)");
|
|
866
851
|
}
|
|
867
852
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const match = /^(\S+)(.*)$/s.exec(plain);
|
|
875
|
-
if (!match) return fgCollapsed(theme, color, plain);
|
|
876
|
-
const rest = match[2] ?? "";
|
|
877
|
-
// The colon joins the bolded name to its content. Failed rows stay
|
|
878
|
-
// uniformly error-colored; settled rows use the collapsed mute — same
|
|
879
|
-
// color for the bolded name and the call content.
|
|
880
|
-
return (
|
|
881
|
-
fgCollapsed(theme, color, match[1] ?? "", true) +
|
|
882
|
-
(rest ? fgCollapsed(theme, color, `:${rest}`) : "")
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function collapsedCallLabel(
|
|
853
|
+
// The collapsed-row seam lives here and nowhere else: which anchor a row
|
|
854
|
+
// takes, how that anchor is styled, and the call text that follows it.
|
|
855
|
+
// Anchors always render bold and take no joining colon — `│ ● src/a.ts`,
|
|
856
|
+
// `│ $ npm test` — and subagent fallbacks keep their `↪ subagent …`
|
|
857
|
+
// reading instead of a glyph. Callers append the outcome tail.
|
|
858
|
+
function collapsedSeam(
|
|
887
859
|
row: ToolExecutionRow,
|
|
888
860
|
width: number,
|
|
889
861
|
theme: ThemeLike,
|
|
890
|
-
color
|
|
891
|
-
): string {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
862
|
+
color: string,
|
|
863
|
+
): { anchor: string; content: string } {
|
|
864
|
+
const isBash = row.toolName === "bash";
|
|
865
|
+
const isSubagent = row.toolName === "subagent";
|
|
866
|
+
const glyph = isBash
|
|
867
|
+
? "$"
|
|
868
|
+
: (TOOL_CALL_GLYPHS.get(row.toolName ?? "") ?? "*");
|
|
869
|
+
const anchor = isSubagent
|
|
870
|
+
? `${fgCollapsed(theme, color, SUBAGENT_MARKER, true)} `
|
|
871
|
+
: `${fgCollapsedRail(theme, GROUP_CALL_MARKER)} ${fgCollapsed(
|
|
872
|
+
theme,
|
|
873
|
+
color,
|
|
874
|
+
glyph,
|
|
875
|
+
true,
|
|
876
|
+
)} `;
|
|
877
|
+
const budget = Math.max(1, width - visibleWidth(anchor));
|
|
878
|
+
// Self-rendered rows build their own `tool {args}` label — Pi's call title
|
|
879
|
+
// is gone once settled — while ordinary rows scrape the rendered call
|
|
880
|
+
// line, which already drops its heading token.
|
|
881
|
+
const selfRendered = row.getRenderShell?.() === "self";
|
|
882
|
+
const source = selfRendered
|
|
883
|
+
? selfRenderedCallLabel(row)
|
|
884
|
+
: renderedCallSummary(row, budget, theme);
|
|
885
|
+
const plain = sanitizeInline(stripAnsi(source)).trim();
|
|
886
|
+
// The anchor stands in for the label's leading token. Only self-rendered
|
|
887
|
+
// labels still carry it; a subagent fallback keeps `subagent` as its own
|
|
888
|
+
// heading, which the scrape dropped.
|
|
889
|
+
const text = isSubagent
|
|
890
|
+
? `subagent${plain ? ` ${plain}` : ""}`
|
|
891
|
+
: selfRendered
|
|
892
|
+
? dropCallToken(plain, row.toolName ?? "tool")
|
|
893
|
+
: plain;
|
|
894
|
+
return {
|
|
895
|
+
anchor,
|
|
896
|
+
content: fgCollapsed(theme, color, text || "(no arguments)"),
|
|
897
|
+
};
|
|
898
|
+
}
|
|
895
899
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
900
|
+
// Drops the leading token an anchor now stands for, with whichever separator
|
|
901
|
+
// the label joined it with.
|
|
902
|
+
function dropCallToken(plain: string, token: string): string {
|
|
903
|
+
if (!plain.startsWith(token)) return plain;
|
|
904
|
+
const rest = plain.slice(token.length);
|
|
905
|
+
if (!/^:?\s|^:$/.test(rest)) return plain;
|
|
906
|
+
return rest.replace(/^:?\s*/, "");
|
|
901
907
|
}
|
|
902
908
|
|
|
903
909
|
function collapsedOutcome(
|
|
@@ -924,30 +930,28 @@ function collapsedOutcome(
|
|
|
924
930
|
return renderedOutcome(row, theme);
|
|
925
931
|
}
|
|
926
932
|
|
|
927
|
-
|
|
933
|
+
// One collapsed row — head and outcome tail — at the given width. Singleton
|
|
934
|
+
// and grouped rows differ only in what surrounds the line.
|
|
935
|
+
function collapsedRowLine(
|
|
928
936
|
row: ToolExecutionRow,
|
|
929
937
|
width: number,
|
|
930
938
|
theme: ThemeLike,
|
|
931
939
|
): string {
|
|
932
940
|
// Failed rows render entirely in error so the row reads as the one that
|
|
933
|
-
// failed, not just its outcome tail. Only the
|
|
934
|
-
const
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
? `${fgCollapsed(theme, tone, SUBAGENT_MARKER)} `
|
|
938
|
-
: `${fgCollapsedRail(theme, tone, GROUP_CALL_MARKER)} `;
|
|
939
|
-
const budget = Math.max(1, width - visibleWidth(marker));
|
|
940
|
-
const label = collapsedCallLabel(row, budget, theme, tone);
|
|
941
|
+
// failed, not just its outcome tail. Only the row's anchor stays bold.
|
|
942
|
+
const color = rowHasFailed(row) ? "error" : "muted";
|
|
943
|
+
const seam = collapsedSeam(row, width, theme, color);
|
|
944
|
+
const budget = Math.max(1, width - visibleWidth(seam.anchor));
|
|
941
945
|
const outcome = collapsedOutcome(row, budget, theme);
|
|
942
946
|
// The truncation suffix inherits the row tone; pi-tui's truncation resets
|
|
943
947
|
// around a plain suffix, which would render it in the terminal default
|
|
944
948
|
// foreground instead of the row color.
|
|
945
|
-
const suffix = fgCollapsed(theme,
|
|
949
|
+
const suffix = fgCollapsed(theme, color, "…");
|
|
946
950
|
return (
|
|
947
|
-
|
|
951
|
+
seam.anchor +
|
|
948
952
|
(outcome
|
|
949
|
-
? fitSummaryTail(
|
|
950
|
-
: fitSummary(
|
|
953
|
+
? fitSummaryTail(seam.content, outcome, budget, suffix)
|
|
954
|
+
: fitSummary(seam.content, budget, suffix))
|
|
951
955
|
);
|
|
952
956
|
}
|
|
953
957
|
|
|
@@ -1247,78 +1251,19 @@ function renderCollapsedToolRow(
|
|
|
1247
1251
|
row.toolName === "subagent"
|
|
1248
1252
|
? renderSubagentPlan(row, layout.contentWidth, theme)
|
|
1249
1253
|
: undefined;
|
|
1250
|
-
const body = plan ?? [
|
|
1251
|
-
if (row.toolName === "edit") {
|
|
1252
|
-
body.push(...renderedEditDiffLines(row, theme));
|
|
1253
|
-
}
|
|
1254
|
+
const body = plan ?? [collapsedRowLine(row, layout.contentWidth, theme)];
|
|
1254
1255
|
body.push(...imageResultLines(row, layout.contentWidth, theme));
|
|
1255
1256
|
return ["", ...insetLines(body, width)];
|
|
1256
1257
|
}
|
|
1257
1258
|
|
|
1258
|
-
function groupedChildLabel(
|
|
1259
|
-
row: ToolExecutionRow,
|
|
1260
|
-
width: number,
|
|
1261
|
-
theme: ThemeLike,
|
|
1262
|
-
color: string = "muted",
|
|
1263
|
-
): string {
|
|
1264
|
-
const label = collapsedCallLabel(row, width, theme, color);
|
|
1265
|
-
const plain = stripAnsi(label);
|
|
1266
|
-
const toolName = row.toolName ?? "tool";
|
|
1267
|
-
let prefix = "";
|
|
1268
|
-
if (plain === toolName) {
|
|
1269
|
-
prefix = toolName;
|
|
1270
|
-
} else if (plain.startsWith(`${toolName}:`)) {
|
|
1271
|
-
const whitespace = /^\s*/.exec(plain.slice(toolName.length + 1))?.[0] ?? "";
|
|
1272
|
-
prefix = `${toolName}:${whitespace}`;
|
|
1273
|
-
}
|
|
1274
|
-
const prefixWidth = visibleWidth(prefix);
|
|
1275
|
-
const child = sliceByColumn(
|
|
1276
|
-
label,
|
|
1277
|
-
prefixWidth,
|
|
1278
|
-
Math.max(0, visibleWidth(label) - prefixWidth),
|
|
1279
|
-
true,
|
|
1280
|
-
);
|
|
1281
|
-
return child || fgCollapsed(theme, color, "(no arguments)");
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
1259
|
function renderGroupedCallLines(
|
|
1285
1260
|
rows: ToolExecutionRow[],
|
|
1286
1261
|
width: number,
|
|
1287
1262
|
theme: ThemeLike,
|
|
1288
1263
|
): Array<{ row: ToolExecutionRow; lines: string[] }> {
|
|
1289
1264
|
const segments: Array<{ row: ToolExecutionRow; lines: string[] }> = [];
|
|
1290
|
-
let previousToolName: string | undefined;
|
|
1291
1265
|
for (const row of rows) {
|
|
1292
|
-
|
|
1293
|
-
const toolName = row.toolName ?? "tool";
|
|
1294
|
-
if (toolName !== previousToolName) {
|
|
1295
|
-
// Keep the rail unbroken across subgroup spacing and put each bold
|
|
1296
|
-
// tool name beside it. The separator belongs to the new row so
|
|
1297
|
-
// click-routing heights stay aligned with the rendered lines.
|
|
1298
|
-
if (previousToolName !== undefined) {
|
|
1299
|
-
lines.push(fgCollapsedRail(theme, "muted", GROUP_CALL_MARKER));
|
|
1300
|
-
}
|
|
1301
|
-
lines.push(
|
|
1302
|
-
`${fgCollapsedRail(theme, "muted", GROUP_CALL_MARKER)} ${fgCollapsed(theme, "toolTitle", toolName, true)}`,
|
|
1303
|
-
);
|
|
1304
|
-
previousToolName = toolName;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
const color = rowHasFailed(row) ? "error" : "muted";
|
|
1308
|
-
const prefix = `${fgCollapsedRail(theme, color, GROUP_CALL_MARKER)} `;
|
|
1309
|
-
const budget = Math.max(1, width - visibleWidth(prefix));
|
|
1310
|
-
const label = groupedChildLabel(row, budget, theme, color);
|
|
1311
|
-
const outcome = collapsedOutcome(row, budget, theme);
|
|
1312
|
-
const suffix = fgCollapsed(theme, color, "…");
|
|
1313
|
-
lines.push(
|
|
1314
|
-
prefix +
|
|
1315
|
-
(outcome
|
|
1316
|
-
? fitSummaryTail(label, outcome, budget, suffix)
|
|
1317
|
-
: fitSummary(label, budget, suffix)),
|
|
1318
|
-
);
|
|
1319
|
-
if (row.toolName === "edit")
|
|
1320
|
-
lines.push(...renderedEditDiffLines(row, theme));
|
|
1321
|
-
segments.push({ row, lines });
|
|
1266
|
+
segments.push({ row, lines: [collapsedRowLine(row, width, theme)] });
|
|
1322
1267
|
}
|
|
1323
1268
|
return segments;
|
|
1324
1269
|
}
|
|
@@ -1387,8 +1332,7 @@ function renderGroupedToolRows(
|
|
|
1387
1332
|
width,
|
|
1388
1333
|
),
|
|
1389
1334
|
];
|
|
1390
|
-
// The
|
|
1391
|
-
// both belong to the first member; every later member owns its lines.
|
|
1335
|
+
// The first member owns the leading blank; every member owns its call line.
|
|
1392
1336
|
const heights = segments.map((segment, index) => ({
|
|
1393
1337
|
component: segment.row,
|
|
1394
1338
|
height: index === 0 ? segment.lines.length + 1 : segment.lines.length,
|
|
@@ -1479,7 +1423,6 @@ function renderContainerWithToolGroups(
|
|
|
1479
1423
|
continue;
|
|
1480
1424
|
}
|
|
1481
1425
|
|
|
1482
|
-
for (const member of group) presentation.rowGroups.set(member, group);
|
|
1483
1426
|
// Use a live member while any call is pending, then the first member once
|
|
1484
1427
|
// settled. Either way, the grouped row keeps the same number of lines.
|
|
1485
1428
|
const shellRow = group.find(isLiveRow) ?? child;
|
|
@@ -1600,6 +1543,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1600
1543
|
const existing = proto[PRESENTATION_PATCHED];
|
|
1601
1544
|
if (existing) {
|
|
1602
1545
|
existing.owners++;
|
|
1546
|
+
existing.disabled = false;
|
|
1603
1547
|
return existing;
|
|
1604
1548
|
}
|
|
1605
1549
|
const legacy = proto[LEGACY_PRESENTATION_PATCHED];
|
|
@@ -1616,16 +1560,20 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1616
1560
|
const state: PresentationPatchState = {
|
|
1617
1561
|
owners: 1,
|
|
1618
1562
|
collapseParallel: envEnabled(COLLAPSE_PARALLEL_ENV, true),
|
|
1563
|
+
expandedTools: envToolNames(EXPANDED_TOOLS_ENV),
|
|
1619
1564
|
groupCache: new WeakMap(),
|
|
1620
1565
|
collapsedCache: new WeakMap(),
|
|
1621
1566
|
rowVersions: new WeakMap(),
|
|
1622
|
-
rowGroups: new WeakMap(),
|
|
1623
1567
|
originalRender: proto.render,
|
|
1624
1568
|
originalUpdateDisplay: proto.updateDisplay,
|
|
1625
1569
|
};
|
|
1626
|
-
const patchedUpdateDisplay = function
|
|
1570
|
+
const patchedUpdateDisplay = function updateDisplayWithToolPresentation(
|
|
1627
1571
|
this: ToolExecutionRow,
|
|
1628
1572
|
): void {
|
|
1573
|
+
if (state.disabled) {
|
|
1574
|
+
state.originalUpdateDisplay.call(this);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1629
1577
|
// Transcript rerenders reuse settled caches, but a real component
|
|
1630
1578
|
// display update can change args, results, renderer output, or images
|
|
1631
1579
|
// without changing the row's shape signature.
|
|
@@ -1633,6 +1581,17 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1633
1581
|
state.groupCache.delete(this);
|
|
1634
1582
|
state.collapsedCache.delete(this);
|
|
1635
1583
|
state.originalUpdateDisplay.call(this);
|
|
1584
|
+
// Rows for an always-expanded tool skip the collapsed presentation.
|
|
1585
|
+
// Forcing the flag here, before grouping decides whether the row is
|
|
1586
|
+
// eligible, keeps Pi's native expanded block authoritative; it also
|
|
1587
|
+
// re-expands the row after a global collapse.
|
|
1588
|
+
if (
|
|
1589
|
+
this.expanded === false &&
|
|
1590
|
+
state.expandedTools.has(this.toolName ?? "") &&
|
|
1591
|
+
typeof this.setExpanded === "function"
|
|
1592
|
+
) {
|
|
1593
|
+
this.setExpanded(true);
|
|
1594
|
+
}
|
|
1636
1595
|
};
|
|
1637
1596
|
// Pi 0.85 wraps settled tool results in a click-to-expand MouseRegion.
|
|
1638
1597
|
// The custom collapsed row replaces those lines with a single summary, so
|
|
@@ -1647,6 +1606,9 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1647
1606
|
event: unknown,
|
|
1648
1607
|
): unknown {
|
|
1649
1608
|
const mouseEvent = event as { type?: unknown; button?: unknown };
|
|
1609
|
+
if (state.disabled) {
|
|
1610
|
+
return state.originalHandleMouse?.call(this, event);
|
|
1611
|
+
}
|
|
1650
1612
|
if (
|
|
1651
1613
|
state.theme &&
|
|
1652
1614
|
this.expanded === false &&
|
|
@@ -1668,6 +1630,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1668
1630
|
): string[] {
|
|
1669
1631
|
const theme = state.theme;
|
|
1670
1632
|
if (
|
|
1633
|
+
state.disabled ||
|
|
1671
1634
|
typeof this.expanded !== "boolean" ||
|
|
1672
1635
|
typeof this.isPartial !== "boolean" ||
|
|
1673
1636
|
this.expanded ||
|
|
@@ -1705,7 +1668,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1705
1668
|
return cached.lines;
|
|
1706
1669
|
}
|
|
1707
1670
|
const lines = state.originalRender.call(this, width);
|
|
1708
|
-
if (
|
|
1671
|
+
if (drawsNoRowLines(this, lines)) {
|
|
1709
1672
|
return lines;
|
|
1710
1673
|
}
|
|
1711
1674
|
try {
|
|
@@ -1726,7 +1689,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1726
1689
|
// Partial (streaming) rows keep the collapsed presentation but are
|
|
1727
1690
|
// never cached: their content changes with every streamed chunk.
|
|
1728
1691
|
const lines = state.originalRender.call(this, width);
|
|
1729
|
-
if (
|
|
1692
|
+
if (drawsNoRowLines(this, lines)) {
|
|
1730
1693
|
return lines;
|
|
1731
1694
|
}
|
|
1732
1695
|
try {
|
|
@@ -1763,6 +1726,7 @@ function uninstallPresentationPatch(
|
|
|
1763
1726
|
if (!state || state.owners <= 0) return;
|
|
1764
1727
|
state.owners--;
|
|
1765
1728
|
if (state.owners > 0) return;
|
|
1729
|
+
state.disabled = true;
|
|
1766
1730
|
const proto =
|
|
1767
1731
|
ToolExecutionComponent?.prototype as unknown as ToolExecutionRow & {
|
|
1768
1732
|
[PRESENTATION_PATCHED]?: PresentationPatchState;
|
|
@@ -1813,17 +1777,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1813
1777
|
ctx.ui.setToolsExpanded(false);
|
|
1814
1778
|
});
|
|
1815
1779
|
|
|
1816
|
-
pi.on("session_start", (_event, ctx) => {
|
|
1817
|
-
if (patch) patch.theme = ctx.ui.theme;
|
|
1818
|
-
if (bashPatch) bashPatch.theme = ctx.ui.theme;
|
|
1819
|
-
ctx.ui.setToolsExpanded(false);
|
|
1820
|
-
});
|
|
1821
|
-
|
|
1822
1780
|
pi.on("session_shutdown", () => {
|
|
1823
1781
|
if (released) return;
|
|
1824
1782
|
released = true;
|
|
1825
1783
|
uninstallGroupingPatch(grouping);
|
|
1826
1784
|
uninstallPresentationPatch(patch);
|
|
1827
1785
|
uninstallBashBlockPatch(bashPatch);
|
|
1786
|
+
uninstallInfoVisibility();
|
|
1828
1787
|
});
|
|
1829
1788
|
}
|
|
@@ -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
|
+
}
|