@pi-kaush/pi-tool-call-markers 0.3.4 → 0.3.5

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/README.md CHANGED
@@ -15,19 +15,16 @@ Restart Pi or run `/reload`.
15
15
  Collapsed tool rows use semantic theme colors with no gear, background fill, box padding, or filled blank rows:
16
16
 
17
17
  ```text
18
- │ Read
19
- │ src/a.ts 42 lines
20
- │ src/b.ts 18 lines
21
- │
22
- │ Bash
23
- │ $: npm test → done
18
+ │ ●: src/a.ts 42 lines
19
+ │ ●: src/b.ts 18 lines
20
+ │ $: npm test → done
24
21
  ```
25
22
 
26
23
  - **Two-column outer inset.** Tool markers and image output align with an inset conversation surface. Very narrow terminals reduce the decoration before useful content.
27
- - **`│` tool-call rail.** Single calls use a rail beside the summary. Multi-call groups extend it through bold tool-name headings, indented call summaries, and separators between tool types.
28
- - **Semantic, low-contrast status.** Tool names are emphasized, summaries and settled metadata are muted, pending state is warning-colored, and failures remain error-colored. Ordinary tool states have no background.
24
+ - **`│` tool-call rail.** Every collapsed call anchors on a glyph instead of its tool name: `●` read, `+` write, `±` edit, `○` local search, `≡` ls, `↗` web search and fetch, and `$` bash; unmapped tools fall back to `*`. Grouped rows flow directly across tool types.
25
+ - **Semantic, low-contrast status.** The anchor and the call content share the collapsed mute, pending state is warning-colored, and failures remain error-colored. Ordinary tool states have no background.
29
26
  - **Width-safe outcome tails.** Long summaries truncate before useful tails such as `→ done`, `→ 42 lines`, `→ +2/-1`, or a `bash` duration.
30
- - **Stable running groups.** Adjacent calls group as they appear. Pending state and elapsed `bash` time settle into the final outcome without changing the row count.
27
+ - **Stable running groups.** Adjacent calls group as they appear, including settled failures, which stay error-colored inside the group. Pending state and elapsed `bash` time settle into the final outcome without changing the row count.
31
28
  - **Quiet-turn grouping.** Sequential calls can join across an assistant row with no visible prose or thinking. Visible assistant content remains a boundary.
32
29
  - **MCP and self-rendered tools.** Their stable call labels, compact arguments, pending state, success, and first error line use the same collapsed shell. Native self-rendered details return when expanded.
33
30
  - **Images remain visible.** Image fallback text and terminal image components render below the corresponding marker with the same inset.
@@ -36,7 +33,7 @@ Collapsed tool rows use semantic theme colors with no gear, background fill, box
36
33
 
37
34
  ## Subagent plans
38
35
 
39
- A recognized `subagent` call renders as an unboxed plan in the shared tool aesthetic, marked with `↪` instead of the ordinary `%` tool marker:
36
+ A recognized `subagent` call renders as an unboxed plan in the shared tool aesthetic, marked with `↪` instead of the ordinary `│` tool-call rail:
40
37
 
41
38
  ```text
42
39
  ↪ subagent chain (3 steps) [repo-review]
@@ -52,7 +49,7 @@ Malformed, ambiguous, future, or too-narrow shapes fall back to the generic `↪
52
49
 
53
50
  ## Edit diffs
54
51
 
55
- Settled `edit` calls keep their change visible without expanding: the call line gains a `+added/-removed` outcome stat, and the hunk renders as a bounded diff block underneath (`+` lines in the added tone, `-` in the removed tone, context muted, folded regions as `...`, capped at 12 lines with a count tail). The full native diff — line numbers, intra-line word highlights — still returns with `Ctrl+O`.
52
+ Settled `edit` calls collapse like every other row: the call line keeps its path and a `+added/-removed` outcome stat, and the hunk itself stays out of the collapsed row. `Ctrl+O` restores Pi's full native diff, including line numbers and intra-line word highlights. To keep edits expanded without pressing `Ctrl+O`, list `edit` in `PI_ALWAYS_EXPANDED_TOOL_CALL_MARKERS` (see Configuration).
56
53
 
57
54
  Files and paths in collapsed rows are preserved as displayed by Pi: hyperlink-wrapped paths (Pi wraps `read` call paths in OSC 8 hyperlinks) keep their visible text when sanitized for one-line rows.
58
55
 
@@ -102,9 +99,18 @@ pi
102
99
 
103
100
  `0`, `false`, `no`, and `off` disable parallel grouping. `1`, `true`, `yes`, and `on` enable it. The value is read when the extension loads.
104
101
 
102
+ To render chosen tools as Pi's native expanded block by default — full result and diff without `Ctrl+O` — list their names:
103
+
104
+ ```fish
105
+ set -lx PI_ALWAYS_EXPANDED_TOOL_CALL_MARKERS edit,write
106
+ pi
107
+ ```
108
+
109
+ Names are comma-separated and matched exactly. Listed rows never join a group, and the list stays authoritative: collapsing the transcript with `Ctrl+O` re-expands them. The value is read when the extension loads.
110
+
105
111
  ### Collapsed-row colors
106
112
 
107
- Collapsed rows (tool calls, `+ Thought`, subagents) default to the theme's `syntaxComment` color — it ships with every Pi theme and reads as a muted tone — with the bolded tool name and the call content sharing it. Failures stay error-colored and live spinners keep their existing tints.
113
+ Collapsed rows (tool calls, `+ Thought`, subagents) default to the theme's `syntaxComment` color — it ships with every Pi theme and reads as a muted tone — with the row's anchor and the call content sharing it. Failures stay error-colored and live spinners keep their existing tints.
108
114
 
109
115
  A theme can override each collapsed kind independently with optional color tokens:
110
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-kaush/pi-tool-call-markers",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Compact and group Pi tool calls, with a bundled extension for adjacent thinking blocks.",
5
5
  "license": "MIT",
6
6
  "author": "Kaushik Gopal",
package/src/index.ts CHANGED
@@ -21,11 +21,28 @@ import { fgCollapsed, fgCollapsedRail } from "./muted.ts";
21
21
  const OUTER_INSET = 2;
22
22
  const SUBAGENT_MARKER = "↪";
23
23
  const GROUP_CALL_MARKER = "│";
24
+ // Collapsed rows anchor with a glyph instead of repeating the tool name;
25
+ // tools that read as one family share a glyph, and unmapped tools fall back
26
+ // to `*`.
27
+ const TOOL_CALL_GLYPHS: ReadonlyMap<string, string> = new Map([
28
+ ["read", "●"],
29
+ ["write", "+"],
30
+ ["edit", "±"],
31
+ ["grep", "○"],
32
+ ["ffgrep", "○"],
33
+ ["find", "○"],
34
+ ["fffind", "○"],
35
+ ["ls", "≡"],
36
+ ["web_search", "↗"],
37
+ ["fetch_content", "↗"],
38
+ ["get_search_content", "↗"],
39
+ ]);
24
40
  const PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v3");
25
41
  const LEGACY_PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v2");
26
42
  const GROUPING_PATCHED = Symbol.for("kg.pi.toolGrouping.v1");
27
43
  const ANSI_RE = /\u001b\[[0-9;]*m/g;
28
44
  const COLLAPSE_PARALLEL_ENV = "PI_TOOL_CALL_MARKERS_COLLAPSE_PARALLEL";
45
+ const EXPANDED_TOOLS_ENV = "PI_ALWAYS_EXPANDED_TOOL_CALL_MARKERS";
29
46
 
30
47
  type ThemeLike = {
31
48
  bold(text: string): string;
@@ -72,6 +89,7 @@ type PresentationPatchState = {
72
89
  owners: number;
73
90
  theme?: ThemeLike;
74
91
  collapseParallel: boolean;
92
+ expandedTools: ReadonlySet<string>;
75
93
  groupCache: WeakMap<ToolExecutionRow, GroupRenderCache>;
76
94
  collapsedCache: WeakMap<ToolExecutionRow, CollapsedRenderCache>;
77
95
  rowVersions: WeakMap<ToolExecutionRow, number>;
@@ -94,9 +112,7 @@ type GroupingPatchState = {
94
112
 
95
113
  type GroupRenderCache = {
96
114
  lines: string[];
97
- // Per-member click-routing accounting for the group block: the first
98
- // member absorbs the leading blank and the tool-name header, later
99
- // members carry their own line (plus edit diffs).
115
+ // The first member absorbs the leading blank; each member owns its line.
100
116
  heights: Array<{ component: ToolExecutionRow; height: number }>;
101
117
  members: ToolExecutionRow[];
102
118
  memberVersions: number[];
@@ -120,6 +136,19 @@ function envEnabled(name: string, defaultValue: boolean): boolean {
120
136
  return defaultValue;
121
137
  }
122
138
 
139
+ // Comma-separated tool names, e.g. `edit,write`. Blank entries and padding
140
+ // are ignored; an unset or empty variable yields no names.
141
+ function envToolNames(name: string): ReadonlySet<string> {
142
+ const value = process.env[name]?.trim();
143
+ if (!value) return new Set();
144
+ return new Set(
145
+ value
146
+ .split(",")
147
+ .map((entry) => entry.trim())
148
+ .filter(Boolean),
149
+ );
150
+ }
151
+
123
152
  function stripAnsi(text: string): string {
124
153
  return text.replace(ANSI_RE, "");
125
154
  }
@@ -497,6 +526,18 @@ function isCollapsibleSuccess(row: ToolExecutionRow): boolean {
497
526
  );
498
527
  }
499
528
 
529
+ // Settled failures join groups like settled successes; the row's error
530
+ // color carries the failure, so the group stays visually unbroken.
531
+ function isCollapsibleFailure(row: ToolExecutionRow): boolean {
532
+ return (
533
+ row.expanded === false &&
534
+ row.isPartial === false &&
535
+ !!row.result &&
536
+ rowHasFailed(row) &&
537
+ !hasImageResult(row)
538
+ );
539
+ }
540
+
500
541
  function hasToolSiblingInAssistantBatch(
501
542
  children: unknown[],
502
543
  index: number,
@@ -525,7 +566,12 @@ function isGroupableToolRow(
525
566
  state: PresentationPatchState,
526
567
  ): boolean {
527
568
  if (row.toolName === "subagent") return false;
528
- if (!isLiveRow(row) && !isCollapsibleSuccess(row)) return false;
569
+ if (
570
+ !isLiveRow(row) &&
571
+ !isCollapsibleSuccess(row) &&
572
+ !isCollapsibleFailure(row)
573
+ )
574
+ return false;
529
575
  return (
530
576
  state.collapseParallel ||
531
577
  !hasToolSiblingInAssistantBatch(children, index, renderAt)
@@ -612,15 +658,8 @@ function diffCounts(diff: string): { added: number; removed: number } {
612
658
  }
613
659
 
614
660
  // Pi's edit tool (renderShell "self") stores its display diff in
615
- // result.details.diff — lines like `+27 <content>`, `-27 <content>`,
616
- // ` 28 <context>`, plus `...` for folded regions. Self-rendered rows
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
-
661
+ // result.details.diff; collapsed rows show only its +a/-b stat, while the
662
+ // full hunk returns with Ctrl+O.
624
663
  function editDiffText(row: ToolExecutionRow): string | undefined {
625
664
  if (row.toolName !== "edit") return undefined;
626
665
  if (!row.result || row.isPartial !== false || rowHasFailed(row))
@@ -636,43 +675,6 @@ function renderedEditDiffStat(row: ToolExecutionRow): string | undefined {
636
675
  return added + removed === 0 ? "applied" : `+${added}/-${removed}`;
637
676
  }
638
677
 
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
678
  function outcomeSummary(row: ToolExecutionRow): string | undefined {
677
679
  // Self-rendered tools own their result framing; the built-in heuristics
678
680
  // (line counts, diff stats) describe default-shell rows only, so their
@@ -724,22 +726,6 @@ function renderedOutcome(
724
726
  return fgCollapsed(theme, "toolOutput", `→ ${summary}`);
725
727
  }
726
728
 
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
729
  // A budget too small for the suffix keeps literal styled text instead, so a
744
730
  // one-column label never collapses into a bare ellipsis or loses its status.
745
731
  function truncateStyled(text: string, width: number, suffix = "…"): string {
@@ -900,6 +886,23 @@ function collapsedCallLabel(
900
886
  return styledCallLabel(label, theme, color);
901
887
  }
902
888
 
889
+ // Collapsed rows anchor their call text with a glyph instead of repeating
890
+ // the tool name, grouped or not. Bash carries `$:` inside its own label and
891
+ // the subagent fallback keeps its `↪ subagent` heading, so neither takes a
892
+ // glyph.
893
+ function collapsedCallMarker(
894
+ row: ToolExecutionRow,
895
+ theme: ThemeLike,
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
+ );
904
+ }
905
+
903
906
  function collapsedOutcome(
904
907
  row: ToolExecutionRow,
905
908
  width: number,
@@ -930,14 +933,15 @@ function collapsedHeadline(
930
933
  theme: ThemeLike,
931
934
  ): string {
932
935
  // Failed rows render entirely in error so the row reads as the one that
933
- // failed, not just its outcome tail. Only the tool name is bold.
936
+ // failed, not just its outcome tail. Only the row's anchor stays bold.
934
937
  const tone = rowHasFailed(row) ? "error" : "muted";
935
938
  const marker =
936
939
  row.toolName === "subagent"
937
940
  ? `${fgCollapsed(theme, tone, SUBAGENT_MARKER)} `
938
- : `${fgCollapsedRail(theme, tone, GROUP_CALL_MARKER)} `;
941
+ : `${fgCollapsedRail(theme, GROUP_CALL_MARKER)} `;
942
+ const glyph = collapsedCallMarker(row, theme, tone);
939
943
  const budget = Math.max(1, width - visibleWidth(marker));
940
- const label = collapsedCallLabel(row, budget, theme, tone);
944
+ const label = glyph + collapsedChildLabel(row, budget, theme, tone);
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
@@ -1248,20 +1252,20 @@ function renderCollapsedToolRow(
1248
1252
  ? renderSubagentPlan(row, layout.contentWidth, theme)
1249
1253
  : undefined;
1250
1254
  const body = plan ?? [collapsedHeadline(row, layout.contentWidth, theme)];
1251
- if (row.toolName === "edit") {
1252
- body.push(...renderedEditDiffLines(row, theme));
1253
- }
1254
1255
  body.push(...imageResultLines(row, layout.contentWidth, theme));
1255
1256
  return ["", ...insetLines(body, width)];
1256
1257
  }
1257
1258
 
1258
- function groupedChildLabel(
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(
1259
1262
  row: ToolExecutionRow,
1260
1263
  width: number,
1261
1264
  theme: ThemeLike,
1262
1265
  color: string = "muted",
1263
1266
  ): string {
1264
1267
  const label = collapsedCallLabel(row, width, theme, color);
1268
+ if (row.toolName === "subagent") return label;
1265
1269
  const plain = stripAnsi(label);
1266
1270
  const toolName = row.toolName ?? "tool";
1267
1271
  let prefix = "";
@@ -1287,37 +1291,26 @@ function renderGroupedCallLines(
1287
1291
  theme: ThemeLike,
1288
1292
  ): Array<{ row: ToolExecutionRow; lines: string[] }> {
1289
1293
  const segments: Array<{ row: ToolExecutionRow; lines: string[] }> = [];
1290
- let previousToolName: string | undefined;
1291
1294
  for (const row of rows) {
1292
1295
  const lines: string[] = [];
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
1296
 
1307
1297
  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);
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);
1311
1305
  const outcome = collapsedOutcome(row, budget, theme);
1312
1306
  const suffix = fgCollapsed(theme, color, "…");
1313
1307
  lines.push(
1314
1308
  prefix +
1309
+ marker +
1315
1310
  (outcome
1316
1311
  ? fitSummaryTail(label, outcome, budget, suffix)
1317
1312
  : fitSummary(label, budget, suffix)),
1318
1313
  );
1319
- if (row.toolName === "edit")
1320
- lines.push(...renderedEditDiffLines(row, theme));
1321
1314
  segments.push({ row, lines });
1322
1315
  }
1323
1316
  return segments;
@@ -1387,8 +1380,7 @@ function renderGroupedToolRows(
1387
1380
  width,
1388
1381
  ),
1389
1382
  ];
1390
- // The leading blank and the first segment (tool-name header + call line)
1391
- // both belong to the first member; every later member owns its lines.
1383
+ // The first member owns the leading blank; every member owns its call line.
1392
1384
  const heights = segments.map((segment, index) => ({
1393
1385
  component: segment.row,
1394
1386
  height: index === 0 ? segment.lines.length + 1 : segment.lines.length,
@@ -1616,6 +1608,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
1616
1608
  const state: PresentationPatchState = {
1617
1609
  owners: 1,
1618
1610
  collapseParallel: envEnabled(COLLAPSE_PARALLEL_ENV, true),
1611
+ expandedTools: envToolNames(EXPANDED_TOOLS_ENV),
1619
1612
  groupCache: new WeakMap(),
1620
1613
  collapsedCache: new WeakMap(),
1621
1614
  rowVersions: new WeakMap(),
@@ -1623,7 +1616,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
1623
1616
  originalRender: proto.render,
1624
1617
  originalUpdateDisplay: proto.updateDisplay,
1625
1618
  };
1626
- const patchedUpdateDisplay = function updateDisplayWithCollapsedResult(
1619
+ const patchedUpdateDisplay = function updateDisplayWithToolPresentation(
1627
1620
  this: ToolExecutionRow,
1628
1621
  ): void {
1629
1622
  // Transcript rerenders reuse settled caches, but a real component
@@ -1633,6 +1626,17 @@ function installPresentationPatch(): PresentationPatchState | undefined {
1633
1626
  state.groupCache.delete(this);
1634
1627
  state.collapsedCache.delete(this);
1635
1628
  state.originalUpdateDisplay.call(this);
1629
+ // Rows for an always-expanded tool skip the collapsed presentation.
1630
+ // Forcing the flag here, before grouping decides whether the row is
1631
+ // eligible, keeps Pi's native expanded block authoritative; it also
1632
+ // re-expands the row after a global collapse.
1633
+ if (
1634
+ this.expanded === false &&
1635
+ state.expandedTools.has(this.toolName ?? "") &&
1636
+ typeof this.setExpanded === "function"
1637
+ ) {
1638
+ this.setExpanded(true);
1639
+ }
1636
1640
  };
1637
1641
  // Pi 0.85 wraps settled tool results in a click-to-expand MouseRegion.
1638
1642
  // The custom collapsed row replaces those lines with a single summary, so
package/src/muted.ts CHANGED
@@ -83,12 +83,8 @@ export function fgCollapsed(
83
83
  // guaranteed lighter than the collapsed color (e.g. catppuccin-latte's `dim`
84
84
  // resolves to the same overlay0 as syntaxComment), so apply the terminal's
85
85
  // faint attribute: it lightens toward the background on light themes and
86
- // darkens on dark ones. Error rails keep their full-strength semantic red.
87
- export function fgCollapsedRail(
88
- theme: CollapsedTheme,
89
- color: string,
90
- text: string,
91
- ): string {
92
- const styled = fgCollapsed(theme, color, text);
93
- return color === "error" ? styled : `\x1b[2m${styled}\x1b[22m`;
86
+ // darkens on dark ones. The rail keeps this muted weight even on failed
87
+ // rows, where only the row's text turns error-colored.
88
+ export function fgCollapsedRail(theme: CollapsedTheme, text: string): string {
89
+ return `\x1b[2m${fgCollapsed(theme, "muted", text)}\x1b[22m`;
94
90
  }