@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
package/src/index.ts
CHANGED
|
@@ -10,10 +10,16 @@ import {
|
|
|
10
10
|
visibleWidth,
|
|
11
11
|
wrapTextWithAnsi,
|
|
12
12
|
} from "@earendil-works/pi-tui";
|
|
13
|
+
import {
|
|
14
|
+
installBashBlockPatch,
|
|
15
|
+
uninstallBashBlockPatch,
|
|
16
|
+
} from "./bash-block.ts";
|
|
13
17
|
import { runChatContainerHooks } from "./container-hooks.ts";
|
|
18
|
+
import { installInfoVisibility } from "./info-visibility.ts";
|
|
14
19
|
|
|
15
20
|
const OUTER_INSET = 2;
|
|
16
21
|
const GROUP_MARKER = "%";
|
|
22
|
+
const SUBAGENT_MARKER = "&";
|
|
17
23
|
const PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v3");
|
|
18
24
|
const LEGACY_PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v2");
|
|
19
25
|
const GROUPING_PATCHED = Symbol.for("kg.pi.toolGrouping.v1");
|
|
@@ -58,13 +64,13 @@ type ToolExecutionRow = {
|
|
|
58
64
|
};
|
|
59
65
|
|
|
60
66
|
type PresentationPatchState = {
|
|
67
|
+
owners: number;
|
|
61
68
|
theme?: ThemeLike;
|
|
62
69
|
collapseParallel: boolean;
|
|
63
70
|
groupCache: WeakMap<ToolExecutionRow, GroupRenderCache>;
|
|
64
71
|
collapsedCache: WeakMap<ToolExecutionRow, CollapsedRenderCache>;
|
|
65
72
|
rowVersions: WeakMap<ToolExecutionRow, number>;
|
|
66
73
|
rowGroups: WeakMap<ToolExecutionRow, ToolExecutionRow[]>;
|
|
67
|
-
rowSignatures: WeakMap<ToolExecutionRow, string>;
|
|
68
74
|
originalRender: (width: number) => string[];
|
|
69
75
|
originalUpdateDisplay: () => void;
|
|
70
76
|
patchedRender?: (width: number) => string[];
|
|
@@ -72,6 +78,7 @@ type PresentationPatchState = {
|
|
|
72
78
|
};
|
|
73
79
|
|
|
74
80
|
type GroupingPatchState = {
|
|
81
|
+
owners: number;
|
|
75
82
|
presentation: PresentationPatchState;
|
|
76
83
|
originalRender: (width: number) => string[];
|
|
77
84
|
patchedRender?: (width: number) => string[];
|
|
@@ -106,16 +113,19 @@ function stripAnsi(text: string): string {
|
|
|
106
113
|
return text.replace(ANSI_RE, "");
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
// Command output can carry cursor-moving control bytes
|
|
110
|
-
//
|
|
111
|
-
// and scraped text is stripped of display sequences and
|
|
112
|
-
// it can reach a row;
|
|
113
|
-
|
|
114
|
-
const INLINE_CONTROL_RE = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
116
|
+
// Command output can carry cursor-moving control bytes, raw terminal
|
|
117
|
+
// sequences, line feeds, and tabs. Collapsed rows are single-line, so result
|
|
118
|
+
// and scraped text is stripped of display sequences and all C0 controls before
|
|
119
|
+
// it can reach a row; otherwise control bytes could split or overwrite it.
|
|
120
|
+
const INLINE_CONTROL_RE = /[\x00-\x1f\x7f]/g;
|
|
115
121
|
// OSC first: the two-byte alternative would otherwise consume `\x1b]` and
|
|
116
|
-
// leave the hyperlink payload behind as visible text.
|
|
122
|
+
// leave the hyperlink payload behind as visible text. OSC sequences (OSC 8
|
|
123
|
+
// hyperlinks, titles) end at their first BEL/ST terminator; a payload match
|
|
124
|
+
// must stop there too, because the visible text sits BETWEEN two OSC
|
|
125
|
+
// sequences — Pi wraps read paths as `ESC]8;;url ESC\ <path> ESC]8;; ESC\`,
|
|
126
|
+
// and a greedy `[^\x07]*` would swallow that path along with the sequences.
|
|
117
127
|
const DISPLAY_ANSI_RE =
|
|
118
|
-
/\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g;
|
|
128
|
+
/\x1b(?:\][^\x07\x1b]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g;
|
|
119
129
|
|
|
120
130
|
function sanitizeInline(text: string): string {
|
|
121
131
|
return text.replace(DISPLAY_ANSI_RE, "").replace(INLINE_CONTROL_RE, " ");
|
|
@@ -125,11 +135,9 @@ function hasVisibleContent(line: string): boolean {
|
|
|
125
135
|
return stripAnsi(line).trim().length > 0;
|
|
126
136
|
}
|
|
127
137
|
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// content streams and it is never cached, and once settled the rendered
|
|
132
|
-
// content is fully determined by this fingerprint + width + theme.
|
|
138
|
+
// Shape fingerprint used in settled-row cache keys. It protects against
|
|
139
|
+
// direct state mutation followed by render without updateDisplay; real
|
|
140
|
+
// component display updates invalidate the cache through rowVersions.
|
|
133
141
|
function rowSignatureOf(row: ToolExecutionRow): string {
|
|
134
142
|
return `${row.isPartial}|${row.expanded}|${row.result ? 1 : 0}|${row.result?.isError ? 1 : 0}`;
|
|
135
143
|
}
|
|
@@ -244,7 +252,7 @@ function capFailureTail(tail: string, cap: number, theme: ThemeLike): string {
|
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
function renderedGenericOutcome(theme: ThemeLike): string {
|
|
247
|
-
return theme.fg("
|
|
255
|
+
return theme.fg("toolOutput", "→ done");
|
|
248
256
|
}
|
|
249
257
|
|
|
250
258
|
// The MCP adapter stashes its call's first line in renderer state before
|
|
@@ -592,6 +600,64 @@ function diffCounts(diff: string): { added: number; removed: number } {
|
|
|
592
600
|
return { added, removed };
|
|
593
601
|
}
|
|
594
602
|
|
|
603
|
+
// Pi's edit tool (renderShell "self") stores its display diff in
|
|
604
|
+
// result.details.diff — lines like `+27 <content>`, `-27 <content>`,
|
|
605
|
+
// ` 28 <context>`, plus `...` for folded regions. Self-rendered rows
|
|
606
|
+
// normally keep only their one-line call label; edits additionally show
|
|
607
|
+
// the change as a bounded diff block under the summary and a +a/-b stat in
|
|
608
|
+
// the outcome tail, so the hunk stays visible without Ctrl+O while
|
|
609
|
+
// expanded rows keep Pi's native rendering.
|
|
610
|
+
const EDIT_DIFF_LINE_RE = /^([+\-\s])(\s*\d*)(.*)$/;
|
|
611
|
+
const MAX_EDIT_DIFF_LINES = 12;
|
|
612
|
+
|
|
613
|
+
function editDiffText(row: ToolExecutionRow): string | undefined {
|
|
614
|
+
if (row.toolName !== "edit") return undefined;
|
|
615
|
+
if (!row.result || row.isPartial !== false || rowHasFailed(row))
|
|
616
|
+
return undefined;
|
|
617
|
+
const diff = row.result.details?.diff;
|
|
618
|
+
return typeof diff === "string" && diff.length > 0 ? diff : undefined;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function renderedEditDiffStat(row: ToolExecutionRow): string | undefined {
|
|
622
|
+
const diff = editDiffText(row);
|
|
623
|
+
if (diff === undefined) return undefined;
|
|
624
|
+
const { added, removed } = diffCounts(diff);
|
|
625
|
+
return added + removed === 0 ? "applied" : `+${added}/-${removed}`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function renderedEditDiffLines(
|
|
629
|
+
row: ToolExecutionRow,
|
|
630
|
+
theme: ThemeLike,
|
|
631
|
+
): string[] {
|
|
632
|
+
const diff = editDiffText(row);
|
|
633
|
+
if (diff === undefined) return [];
|
|
634
|
+
const raw = diff.split("\n");
|
|
635
|
+
if (raw[raw.length - 1] === "") raw.pop();
|
|
636
|
+
const lines: string[] = [];
|
|
637
|
+
for (const line of raw.slice(0, MAX_EDIT_DIFF_LINES)) {
|
|
638
|
+
const clean = sanitizeInline(line).trimEnd();
|
|
639
|
+
if (clean.trim() === "...") {
|
|
640
|
+
lines.push(theme.fg("muted", " ..."));
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const match = EDIT_DIFF_LINE_RE.exec(clean);
|
|
644
|
+
if (!match) continue;
|
|
645
|
+
const color =
|
|
646
|
+
match[1] === "+"
|
|
647
|
+
? "toolDiffAdded"
|
|
648
|
+
: match[1] === "-"
|
|
649
|
+
? "toolDiffRemoved"
|
|
650
|
+
: "toolDiffContext";
|
|
651
|
+
lines.push(theme.fg(color, ` ${match[1]}${match[2]}${match[3]}`));
|
|
652
|
+
}
|
|
653
|
+
if (raw.length > MAX_EDIT_DIFF_LINES) {
|
|
654
|
+
lines.push(
|
|
655
|
+
theme.fg("muted", ` ... +${raw.length - MAX_EDIT_DIFF_LINES} more`),
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
return lines;
|
|
659
|
+
}
|
|
660
|
+
|
|
595
661
|
function outcomeSummary(row: ToolExecutionRow): string | undefined {
|
|
596
662
|
// Self-rendered tools own their result framing; the built-in heuristics
|
|
597
663
|
// (line counts, diff stats) describe default-shell rows only, so their
|
|
@@ -640,7 +706,7 @@ function renderedOutcome(
|
|
|
640
706
|
): string | undefined {
|
|
641
707
|
const summary = outcomeSummary(row);
|
|
642
708
|
if (!summary) return undefined;
|
|
643
|
-
return theme.fg("
|
|
709
|
+
return theme.fg("toolOutput", `→ ${summary}`);
|
|
644
710
|
}
|
|
645
711
|
|
|
646
712
|
function renderedGroupedOutcome(
|
|
@@ -780,7 +846,7 @@ function renderedCallSummary(
|
|
|
780
846
|
|
|
781
847
|
const fallback = compactArgs(row.args);
|
|
782
848
|
return fallback
|
|
783
|
-
? theme.fg("
|
|
849
|
+
? theme.fg("toolOutput", fallback)
|
|
784
850
|
: theme.fg("muted", "(no arguments)");
|
|
785
851
|
}
|
|
786
852
|
|
|
@@ -793,9 +859,13 @@ function styledCallLabel(
|
|
|
793
859
|
const match = /^(\S+)(.*)$/s.exec(plain);
|
|
794
860
|
if (!match) return theme.fg(color, plain);
|
|
795
861
|
const rest = match[2] ?? "";
|
|
862
|
+
// Failed rows stay uniformly error-colored; settled rows use Pi's native
|
|
863
|
+
// split — toolTitle for the name, toolOutput for the call content.
|
|
864
|
+
const nameColor = color === "muted" ? "toolTitle" : color;
|
|
865
|
+
const restColor = color === "muted" ? "toolOutput" : color;
|
|
796
866
|
return (
|
|
797
|
-
theme.fg(
|
|
798
|
-
(rest ? theme.fg(
|
|
867
|
+
theme.fg(nameColor, theme.bold(match[1] ?? "")) +
|
|
868
|
+
(rest ? theme.fg(restColor, `:${rest}`) : "")
|
|
799
869
|
);
|
|
800
870
|
}
|
|
801
871
|
|
|
@@ -832,7 +902,11 @@ function collapsedOutcome(
|
|
|
832
902
|
const elapsed = liveElapsedText(row);
|
|
833
903
|
return theme.fg("warning", elapsed ? `… · ${elapsed}` : "…");
|
|
834
904
|
}
|
|
835
|
-
if (row.getRenderShell?.() === "self")
|
|
905
|
+
if (row.getRenderShell?.() === "self") {
|
|
906
|
+
const editStat = renderedEditDiffStat(row);
|
|
907
|
+
if (editStat) return theme.fg("toolOutput", `→ ${editStat}`);
|
|
908
|
+
return renderedGenericOutcome(theme);
|
|
909
|
+
}
|
|
836
910
|
return renderedOutcome(row, theme);
|
|
837
911
|
}
|
|
838
912
|
|
|
@@ -844,7 +918,10 @@ function collapsedHeadline(
|
|
|
844
918
|
// Failed rows render entirely in error so the row reads as the one that
|
|
845
919
|
// failed, not just its outcome tail. Only the tool name is bold.
|
|
846
920
|
const color = rowHasFailed(row) ? "error" : "muted";
|
|
847
|
-
const marker = `${theme.fg(
|
|
921
|
+
const marker = `${theme.fg(
|
|
922
|
+
color,
|
|
923
|
+
row.toolName === "subagent" ? SUBAGENT_MARKER : GROUP_MARKER,
|
|
924
|
+
)} `;
|
|
848
925
|
const budget = Math.max(1, width - visibleWidth(marker));
|
|
849
926
|
const label = collapsedCallLabel(row, budget, theme, color);
|
|
850
927
|
const outcome = collapsedOutcome(row, budget, theme);
|
|
@@ -980,6 +1057,33 @@ function subagentStepPreview(task: string): string {
|
|
|
980
1057
|
return clean.length > 40 ? `${clean.slice(0, 40)}...` : clean;
|
|
981
1058
|
}
|
|
982
1059
|
|
|
1060
|
+
// Partial and final subagent results both carry per-task usage in details
|
|
1061
|
+
// (the subagent extension streams makeDetails snapshots through onUpdate), so
|
|
1062
|
+
// this works live and settled. Surface the smallest useful progress — total
|
|
1063
|
+
// turns, and the model when every task resolved to the same one.
|
|
1064
|
+
function subagentProgressText(row: ToolExecutionRow): string | undefined {
|
|
1065
|
+
const results = row.result?.details?.results;
|
|
1066
|
+
if (!Array.isArray(results) || results.length === 0) return undefined;
|
|
1067
|
+
let turns = 0;
|
|
1068
|
+
const models = new Set<string>();
|
|
1069
|
+
for (const result of results) {
|
|
1070
|
+
if (!isRecord(result)) continue;
|
|
1071
|
+
const usage = result.usage;
|
|
1072
|
+
const turnCount = isRecord(usage) ? usage.turns : undefined;
|
|
1073
|
+
if (typeof turnCount === "number" && Number.isFinite(turnCount)) {
|
|
1074
|
+
turns += turnCount;
|
|
1075
|
+
}
|
|
1076
|
+
const model = result.model ?? result.requestedModel;
|
|
1077
|
+
if (typeof model === "string" && model.trim().length > 0) {
|
|
1078
|
+
models.add(model.trim());
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const parts: string[] = [];
|
|
1082
|
+
if (turns > 0) parts.push(`${turns} turn${turns === 1 ? "" : "s"}`);
|
|
1083
|
+
if (models.size === 1) parts.push(sanitizeInline([...models][0]!));
|
|
1084
|
+
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
983
1087
|
// Subagents render as an ordinary unboxed tool block: a `%` heading with the
|
|
984
1088
|
// plan kind/count/scope, then the numbered chain steps or parallel tasks with
|
|
985
1089
|
// agent names in accent. Everything else stays muted (or error on failure),
|
|
@@ -1001,20 +1105,22 @@ function renderSubagentPlan(
|
|
|
1001
1105
|
// other collapsed-row text so control bytes cannot reach the terminal.
|
|
1002
1106
|
const displayOf = (agent: string) =>
|
|
1003
1107
|
theme.fg(nameColor, displayNames.get(agent) ?? sanitizeInline(agent));
|
|
1108
|
+
const detailColor = failed ? "error" : "toolOutput";
|
|
1004
1109
|
const detailOf = (step: SubagentStep) =>
|
|
1005
1110
|
theme.fg(
|
|
1006
|
-
|
|
1111
|
+
detailColor,
|
|
1007
1112
|
`${step.profile ? ` [${sanitizeInline(step.profile)}]` : ""} ${subagentStepPreview(step.task)}`,
|
|
1008
1113
|
);
|
|
1009
1114
|
|
|
1010
|
-
const marker = `${theme.fg(color, theme.bold(
|
|
1115
|
+
const marker = `${theme.fg(color, theme.bold(SUBAGENT_MARKER))} `;
|
|
1011
1116
|
const budget = Math.max(1, width - visibleWidth(marker));
|
|
1117
|
+
const progress = subagentProgressText(row);
|
|
1012
1118
|
const outcome = failed
|
|
1013
1119
|
? collapsedOutcome(row, budget, theme)
|
|
1014
1120
|
: isLiveRow(row)
|
|
1015
|
-
?
|
|
1121
|
+
? theme.fg("warning", progress ? `→ ${progress}` : "…")
|
|
1016
1122
|
: row.result
|
|
1017
|
-
?
|
|
1123
|
+
? theme.fg("toolOutput", progress ? `→ ${progress}` : "→ done")
|
|
1018
1124
|
: theme.fg("warning", "…");
|
|
1019
1125
|
const headline = (label: string) =>
|
|
1020
1126
|
marker +
|
|
@@ -1026,7 +1132,7 @@ function renderSubagentPlan(
|
|
|
1026
1132
|
const step = plan.steps[0]!;
|
|
1027
1133
|
return [
|
|
1028
1134
|
headline(
|
|
1029
|
-
theme.fg(
|
|
1135
|
+
theme.fg(failed ? "error" : "toolTitle", theme.bold("subagent")) +
|
|
1030
1136
|
" " +
|
|
1031
1137
|
displayOf(step.agent) +
|
|
1032
1138
|
detailOf(step),
|
|
@@ -1040,10 +1146,10 @@ function renderSubagentPlan(
|
|
|
1040
1146
|
: `parallel (${plan.steps.length} tasks)`;
|
|
1041
1147
|
const lines = [
|
|
1042
1148
|
headline(
|
|
1043
|
-
theme.fg(
|
|
1149
|
+
theme.fg(failed ? "error" : "toolTitle", theme.bold("subagent")) +
|
|
1044
1150
|
" " +
|
|
1045
|
-
theme.fg(
|
|
1046
|
-
theme.fg(
|
|
1151
|
+
theme.fg(failed ? "error" : "toolOutput", kindLabel) +
|
|
1152
|
+
theme.fg(failed ? "error" : "toolOutput", ` [${plan.scope}]`),
|
|
1047
1153
|
),
|
|
1048
1154
|
];
|
|
1049
1155
|
const shown = plan.steps.slice(0, 3);
|
|
@@ -1102,22 +1208,71 @@ function renderCollapsedToolRow(
|
|
|
1102
1208
|
? renderSubagentPlan(row, layout.contentWidth, theme)
|
|
1103
1209
|
: undefined;
|
|
1104
1210
|
const body = plan ?? [collapsedHeadline(row, layout.contentWidth, theme)];
|
|
1211
|
+
if (row.toolName === "edit") {
|
|
1212
|
+
body.push(...renderedEditDiffLines(row, theme));
|
|
1213
|
+
}
|
|
1105
1214
|
body.push(...imageResultLines(row, layout.contentWidth, theme));
|
|
1106
1215
|
return ["", ...insetLines(body, width)];
|
|
1107
1216
|
}
|
|
1108
1217
|
|
|
1109
|
-
function
|
|
1218
|
+
function groupedChildLabel(
|
|
1219
|
+
row: ToolExecutionRow,
|
|
1220
|
+
width: number,
|
|
1221
|
+
theme: ThemeLike,
|
|
1222
|
+
color: string = "muted",
|
|
1223
|
+
): string {
|
|
1224
|
+
const label = collapsedCallLabel(row, width, theme, color);
|
|
1225
|
+
const plain = stripAnsi(label);
|
|
1226
|
+
const toolName = row.toolName ?? "tool";
|
|
1227
|
+
let prefix = "";
|
|
1228
|
+
if (plain === toolName) {
|
|
1229
|
+
prefix = toolName;
|
|
1230
|
+
} else if (plain.startsWith(`${toolName}:`)) {
|
|
1231
|
+
const whitespace = /^\s*/.exec(plain.slice(toolName.length + 1))?.[0] ?? "";
|
|
1232
|
+
prefix = `${toolName}:${whitespace}`;
|
|
1233
|
+
}
|
|
1234
|
+
const prefixWidth = visibleWidth(prefix);
|
|
1235
|
+
const child = sliceByColumn(
|
|
1236
|
+
label,
|
|
1237
|
+
prefixWidth,
|
|
1238
|
+
Math.max(0, visibleWidth(label) - prefixWidth),
|
|
1239
|
+
true,
|
|
1240
|
+
);
|
|
1241
|
+
return child || theme.fg(color, "(no arguments)");
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function renderGroupedCallLines(
|
|
1110
1245
|
rows: ToolExecutionRow[],
|
|
1246
|
+
width: number,
|
|
1111
1247
|
theme: ThemeLike,
|
|
1112
|
-
):
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1248
|
+
): string[] {
|
|
1249
|
+
const lines: string[] = [];
|
|
1250
|
+
let previousToolName: string | undefined;
|
|
1251
|
+
for (const row of rows) {
|
|
1252
|
+
const toolName = row.toolName ?? "tool";
|
|
1253
|
+
if (toolName !== previousToolName) {
|
|
1254
|
+
lines.push(
|
|
1255
|
+
`${theme.fg("muted", GROUP_MARKER)} ${theme.fg("toolTitle", theme.bold(toolName))}`,
|
|
1256
|
+
);
|
|
1257
|
+
previousToolName = toolName;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const color = rowHasFailed(row) ? "error" : "muted";
|
|
1261
|
+
const prefix = ` ${theme.fg(color, "•")} `;
|
|
1262
|
+
const budget = Math.max(1, width - visibleWidth(prefix));
|
|
1263
|
+
const label = groupedChildLabel(row, budget, theme, color);
|
|
1264
|
+
const outcome = collapsedOutcome(row, budget, theme);
|
|
1265
|
+
const suffix = theme.fg(color, "…");
|
|
1266
|
+
lines.push(
|
|
1267
|
+
prefix +
|
|
1268
|
+
(outcome
|
|
1269
|
+
? fitSummaryTail(label, outcome, budget, suffix)
|
|
1270
|
+
: fitSummary(label, budget, suffix)),
|
|
1271
|
+
);
|
|
1272
|
+
if (row.toolName === "edit")
|
|
1273
|
+
lines.push(...renderedEditDiffLines(row, theme));
|
|
1274
|
+
}
|
|
1275
|
+
return lines;
|
|
1121
1276
|
}
|
|
1122
1277
|
|
|
1123
1278
|
function sameMembers(
|
|
@@ -1166,8 +1321,8 @@ function renderGroupedToolRows(
|
|
|
1166
1321
|
}
|
|
1167
1322
|
|
|
1168
1323
|
const layout = insetLayout(width);
|
|
1169
|
-
const
|
|
1170
|
-
const lines = ["", ...insetLines(
|
|
1324
|
+
const body = renderGroupedCallLines(rows, layout.contentWidth, theme);
|
|
1325
|
+
const lines = ["", ...insetLines(body, width)];
|
|
1171
1326
|
if (hasLiveMembers) {
|
|
1172
1327
|
state.groupCache.delete(row);
|
|
1173
1328
|
} else {
|
|
@@ -1266,12 +1421,14 @@ function installGroupingPatch(
|
|
|
1266
1421
|
|
|
1267
1422
|
const existing = proto[GROUPING_PATCHED];
|
|
1268
1423
|
if (existing) {
|
|
1424
|
+
existing.owners++;
|
|
1269
1425
|
existing.presentation = presentation;
|
|
1270
1426
|
existing.disabled = false;
|
|
1271
1427
|
return existing;
|
|
1272
1428
|
}
|
|
1273
1429
|
|
|
1274
1430
|
const state: GroupingPatchState = {
|
|
1431
|
+
owners: 1,
|
|
1275
1432
|
presentation,
|
|
1276
1433
|
originalRender: proto.render,
|
|
1277
1434
|
};
|
|
@@ -1319,10 +1476,12 @@ function installGroupingPatch(
|
|
|
1319
1476
|
}
|
|
1320
1477
|
|
|
1321
1478
|
function uninstallGroupingPatch(state: GroupingPatchState | undefined): void {
|
|
1322
|
-
if (!state) return;
|
|
1479
|
+
if (!state || state.owners <= 0) return;
|
|
1480
|
+
state.owners--;
|
|
1481
|
+
if (state.owners > 0) return;
|
|
1323
1482
|
// Even when another extension's wrapper sits on top of ours, grouping must
|
|
1324
|
-
// not outlive its owner: the wrapper delegates once disabled, and a
|
|
1325
|
-
// reinstall re-enables it.
|
|
1483
|
+
// not outlive its final owner: the wrapper delegates once disabled, and a
|
|
1484
|
+
// later reinstall re-enables it.
|
|
1326
1485
|
state.disabled = true;
|
|
1327
1486
|
const proto = Container?.prototype as unknown as ComponentContainer & {
|
|
1328
1487
|
[GROUPING_PATCHED]?: GroupingPatchState;
|
|
@@ -1346,7 +1505,10 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1346
1505
|
if (!proto) return undefined;
|
|
1347
1506
|
|
|
1348
1507
|
const existing = proto[PRESENTATION_PATCHED];
|
|
1349
|
-
if (existing)
|
|
1508
|
+
if (existing) {
|
|
1509
|
+
existing.owners++;
|
|
1510
|
+
return existing;
|
|
1511
|
+
}
|
|
1350
1512
|
const legacy = proto[LEGACY_PRESENTATION_PATCHED];
|
|
1351
1513
|
if (legacy) {
|
|
1352
1514
|
proto.render = legacy.originalRender;
|
|
@@ -1359,30 +1521,24 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1359
1521
|
return undefined;
|
|
1360
1522
|
|
|
1361
1523
|
const state: PresentationPatchState = {
|
|
1524
|
+
owners: 1,
|
|
1362
1525
|
collapseParallel: envEnabled(COLLAPSE_PARALLEL_ENV, true),
|
|
1363
1526
|
groupCache: new WeakMap(),
|
|
1364
1527
|
collapsedCache: new WeakMap(),
|
|
1365
1528
|
rowVersions: new WeakMap(),
|
|
1366
1529
|
rowGroups: new WeakMap(),
|
|
1367
|
-
rowSignatures: new WeakMap(),
|
|
1368
1530
|
originalRender: proto.render,
|
|
1369
1531
|
originalUpdateDisplay: proto.updateDisplay,
|
|
1370
1532
|
};
|
|
1371
1533
|
const patchedUpdateDisplay = function updateDisplayWithCollapsedResult(
|
|
1372
1534
|
this: ToolExecutionRow,
|
|
1373
1535
|
): void {
|
|
1374
|
-
//
|
|
1375
|
-
//
|
|
1376
|
-
//
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
state.rowSignatures.get(this) !== signature
|
|
1381
|
-
) {
|
|
1382
|
-
state.rowSignatures.set(this, signature);
|
|
1383
|
-
state.rowVersions.set(this, (state.rowVersions.get(this) ?? 0) + 1);
|
|
1384
|
-
state.groupCache.delete(this);
|
|
1385
|
-
}
|
|
1536
|
+
// Transcript rerenders reuse settled caches, but a real component
|
|
1537
|
+
// display update can change args, results, renderer output, or images
|
|
1538
|
+
// without changing the row's shape signature.
|
|
1539
|
+
state.rowVersions.set(this, (state.rowVersions.get(this) ?? 0) + 1);
|
|
1540
|
+
state.groupCache.delete(this);
|
|
1541
|
+
state.collapsedCache.delete(this);
|
|
1386
1542
|
state.originalUpdateDisplay.call(this);
|
|
1387
1543
|
};
|
|
1388
1544
|
const patchedRender = function renderWithToolPresentation(
|
|
@@ -1407,12 +1563,11 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1407
1563
|
// it touches. On a long session (~1,000 messages, hundreds of tool
|
|
1408
1564
|
// rows) that measured at ~80 ms per keystroke — visible input lag.
|
|
1409
1565
|
//
|
|
1410
|
-
// Settled rows (not partial, not expanded) have
|
|
1411
|
-
//
|
|
1412
|
-
// rowSignatureOf + rowVersions + theme sample.
|
|
1413
|
-
//
|
|
1414
|
-
// bypass the cache
|
|
1415
|
-
// mirrors the existing groupCache discipline; keep them in sync.
|
|
1566
|
+
// Settled rows (not partial, not expanded) have static output between
|
|
1567
|
+
// component display updates, so their collapsed lines are cached keyed
|
|
1568
|
+
// by width + rowSignatureOf + rowVersions + theme sample. Every real
|
|
1569
|
+
// updateDisplay call bumps rowVersions and clears both caches; partial
|
|
1570
|
+
// rows bypass the cache because their content streams.
|
|
1416
1571
|
// ================================================================
|
|
1417
1572
|
if (!this.isPartial) {
|
|
1418
1573
|
const signature = rowSignatureOf(this);
|
|
@@ -1484,7 +1639,9 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1484
1639
|
function uninstallPresentationPatch(
|
|
1485
1640
|
state: PresentationPatchState | undefined,
|
|
1486
1641
|
): void {
|
|
1487
|
-
if (!state) return;
|
|
1642
|
+
if (!state || state.owners <= 0) return;
|
|
1643
|
+
state.owners--;
|
|
1644
|
+
if (state.owners > 0) return;
|
|
1488
1645
|
const proto =
|
|
1489
1646
|
ToolExecutionComponent?.prototype as unknown as ToolExecutionRow & {
|
|
1490
1647
|
[PRESENTATION_PATCHED]?: PresentationPatchState;
|
|
@@ -1506,14 +1663,21 @@ function uninstallPresentationPatch(
|
|
|
1506
1663
|
export default function (pi: ExtensionAPI) {
|
|
1507
1664
|
const patch = installPresentationPatch();
|
|
1508
1665
|
const grouping = patch ? installGroupingPatch(patch) : undefined;
|
|
1666
|
+
const bashPatch = installBashBlockPatch();
|
|
1667
|
+
installInfoVisibility(pi);
|
|
1668
|
+
let released = false;
|
|
1509
1669
|
|
|
1510
1670
|
pi.on("session_start", (_event, ctx) => {
|
|
1511
1671
|
if (patch) patch.theme = ctx.ui.theme;
|
|
1672
|
+
if (bashPatch) bashPatch.theme = ctx.ui.theme;
|
|
1512
1673
|
ctx.ui.setToolsExpanded(false);
|
|
1513
1674
|
});
|
|
1514
1675
|
|
|
1515
1676
|
pi.on("session_shutdown", () => {
|
|
1677
|
+
if (released) return;
|
|
1678
|
+
released = true;
|
|
1516
1679
|
uninstallGroupingPatch(grouping);
|
|
1517
1680
|
uninstallPresentationPatch(patch);
|
|
1681
|
+
uninstallBashBlockPatch(bashPatch);
|
|
1518
1682
|
});
|
|
1519
1683
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Shared toggle state for /toggle-info. Pi loads extension entrypoints through
|
|
2
|
+
// jiti with moduleCache disabled, so each entrypoint's relative-import chain
|
|
3
|
+
// gets its OWN module instances — a plain module-level boolean here would be
|
|
4
|
+
// duplicated between the main entrypoint (the /toggle-info command) and the
|
|
5
|
+
// thinking-block-merger entrypoint (the strip). globalThis keeps it singular.
|
|
6
|
+
|
|
7
|
+
type InfoVisibilityState = { hidden: boolean };
|
|
8
|
+
|
|
9
|
+
const STATE_KEY = Symbol.for("kg.pi.toolCallMarkers.infoVisibility.v1");
|
|
10
|
+
|
|
11
|
+
function infoVisibilityState(): InfoVisibilityState {
|
|
12
|
+
const root = globalThis as Record<symbol, InfoVisibilityState | undefined>;
|
|
13
|
+
return (root[STATE_KEY] ??= { hidden: false });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function infoVisibilityHidden(): boolean {
|
|
17
|
+
return infoVisibilityState().hidden;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function setInfoVisibilityHidden(hidden: boolean): void {
|
|
21
|
+
infoVisibilityState().hidden = hidden;
|
|
22
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BashExecutionComponent,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
ToolExecutionComponent,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { chatContainerHooks } from "./container-hooks.ts";
|
|
7
|
+
import {
|
|
8
|
+
infoVisibilityHidden,
|
|
9
|
+
setInfoVisibilityHidden,
|
|
10
|
+
} from "./info-visibility-state.ts";
|
|
11
|
+
import { refreshThinkingVisibility } from "./thinking-block-merger.ts";
|
|
12
|
+
|
|
13
|
+
function isExecutionRow(child: unknown): boolean {
|
|
14
|
+
return (
|
|
15
|
+
child instanceof ToolExecutionComponent ||
|
|
16
|
+
child instanceof BashExecutionComponent
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Chat-container hook: while hidden, tool and user-bash rows are lifted out of
|
|
21
|
+
// the child list for the render pass and restored afterwards, so grouping and
|
|
22
|
+
// insets run on prose only. Nested invocations are naturally idempotent: a
|
|
23
|
+
// second pass finds nothing left to remove.
|
|
24
|
+
function infoFilterHook(
|
|
25
|
+
_container: object,
|
|
26
|
+
children: unknown[],
|
|
27
|
+
): (() => void) | undefined {
|
|
28
|
+
if (!infoVisibilityHidden()) return undefined;
|
|
29
|
+
const removed: Array<[number, unknown]> = [];
|
|
30
|
+
for (let index = children.length - 1; index >= 0; index--) {
|
|
31
|
+
if (isExecutionRow(children[index])) {
|
|
32
|
+
removed.push([index, children[index]!]);
|
|
33
|
+
children.splice(index, 1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (removed.length === 0) return undefined;
|
|
37
|
+
return () => {
|
|
38
|
+
for (const [index, child] of removed.reverse()) {
|
|
39
|
+
children.splice(index, 0, child);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function installInfoVisibility(pi: ExtensionAPI): void {
|
|
45
|
+
// The hook registry is process-global; dedupe by name so hot reloads swap
|
|
46
|
+
// rather than stack.
|
|
47
|
+
const hooks = chatContainerHooks();
|
|
48
|
+
for (const hook of [...hooks]) {
|
|
49
|
+
if (hook.name === infoFilterHook.name) hooks.delete(hook);
|
|
50
|
+
}
|
|
51
|
+
hooks.add(infoFilterHook);
|
|
52
|
+
|
|
53
|
+
pi.registerCommand("toggle-info", {
|
|
54
|
+
description: "Hide tool calls and thinking, or restore them collapsed",
|
|
55
|
+
handler: async (_args, ctx) => {
|
|
56
|
+
const hidden = !infoVisibilityHidden();
|
|
57
|
+
setInfoVisibilityHidden(hidden);
|
|
58
|
+
// Thinking labels/blocks only rebuild on content updates, so replay the
|
|
59
|
+
// last update per assistant row to apply the new state immediately.
|
|
60
|
+
refreshThinkingVisibility();
|
|
61
|
+
ctx.ui.notify(
|
|
62
|
+
hidden
|
|
63
|
+
? "Tool calls and thinking hidden — /toggle-info restores them"
|
|
64
|
+
: "Tool calls and thinking visible",
|
|
65
|
+
"info",
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Every session starts in the default collapsed-but-visible state. No
|
|
71
|
+
// shutdown reset: extra shutdown handlers would sit between this package's
|
|
72
|
+
// owner-counted patch releases.
|
|
73
|
+
pi.on("session_start", () => setInfoVisibilityHidden(false));
|
|
74
|
+
}
|