@pi-kaush/pi-tool-call-markers 0.3.1 → 0.3.3

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Restore Pi 0.85's click-to-expand under the custom collapsed rows. Pi routes
6
+ mouse clicks by per-child rendered line heights, and the grouping render
7
+ bypassed the native render that refreshes that cache, so clicks on anything
8
+ drawn after the first tool call landed nowhere (or one line off after a
9
+ resize). The grouped render now publishes the drawn accounting, and a
10
+ collapsed tool row toggles expansion on left-click like Pi's native result
11
+ region.
12
+
5
13
  - Restyle subagent rows: the subagent marker is now `↪` (was `&`), and single
6
14
  calls drop the `subagent` tool label — `↪ 🤖 [coder][c3po] Implement the…` —
7
15
  with chain/parallel step lines reordered to emoji, profile badge, name badge
package/README.md CHANGED
@@ -16,13 +16,16 @@ Collapsed tool rows use semantic theme colors with no gear, background fill, box
16
16
 
17
17
  ```text
18
18
  % Read
19
- • src/a.ts 42 lines
20
- • src/b.ts 18 lines
19
+ │ src/a.ts 42 lines
20
+ │ src/b.ts 18 lines
21
+
22
+ % Bash
23
+ │ $: npm test → done
21
24
  ```
22
25
 
23
26
  - **Two-column outer inset.** Tool markers and image output align with an inset conversation surface. Very narrow terminals reduce the decoration before useful content.
24
- - **`%` tool headings.** A singleton stays on one line when its summary and outcome fit. A multi-call group has one `%` heading per contiguous tool type.
25
- - **`•` grouped children.** Bullets appear only for members of a multi-call group.
27
+ - **`%` tool headings.** A singleton stays on one line when its summary and outcome fit. A multi-call group has one `%` heading per contiguous tool type, with a blank line before each subsequent heading.
28
+ - **`│` grouped children.** A vertical rail — fainter than the call text — spans the members of a multi-call group; singletons carry no rail.
26
29
  - **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.
27
30
  - **Width-safe outcome tails.** Long summaries truncate before useful tails such as `→ done`, `→ 42 lines`, `→ +2/-1`, or a `bash` duration.
28
31
  - **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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-kaush/pi-tool-call-markers",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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
@@ -16,11 +16,12 @@ import {
16
16
  } from "./bash-block.ts";
17
17
  import { runChatContainerHooks } from "./container-hooks.ts";
18
18
  import { installInfoVisibility } from "./info-visibility.ts";
19
- import { fgCollapsed } from "./muted.ts";
19
+ import { fgCollapsed, fgCollapsedRail } from "./muted.ts";
20
20
 
21
21
  const OUTER_INSET = 2;
22
22
  const GROUP_MARKER = "%";
23
23
  const SUBAGENT_MARKER = "↪";
24
+ const GROUP_CALL_MARKER = "│";
24
25
  const PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v3");
25
26
  const LEGACY_PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v2");
26
27
  const GROUPING_PATCHED = Symbol.for("kg.pi.toolGrouping.v1");
@@ -64,6 +65,8 @@ type ToolExecutionRow = {
64
65
  imageSpacers?: unknown[];
65
66
  getRenderShell?(): "default" | "self";
66
67
  getTextOutput?(): string;
68
+ setExpanded?(expanded: boolean): void;
69
+ handleMouse?(event: unknown): unknown;
67
70
  };
68
71
 
69
72
  type PresentationPatchState = {
@@ -76,8 +79,10 @@ type PresentationPatchState = {
76
79
  rowGroups: WeakMap<ToolExecutionRow, ToolExecutionRow[]>;
77
80
  originalRender: (width: number) => string[];
78
81
  originalUpdateDisplay: () => void;
82
+ originalHandleMouse?: (this: ToolExecutionRow, event: unknown) => unknown;
79
83
  patchedRender?: (width: number) => string[];
80
84
  patchedUpdateDisplay?: () => void;
85
+ patchedHandleMouse?: (this: ToolExecutionRow, event: unknown) => unknown;
81
86
  };
82
87
 
83
88
  type GroupingPatchState = {
@@ -90,6 +95,10 @@ type GroupingPatchState = {
90
95
 
91
96
  type GroupRenderCache = {
92
97
  lines: string[];
98
+ // Per-member click-routing accounting for the group block: the first
99
+ // member absorbs the leading blank and the tool-name header, later
100
+ // members carry their own line (plus edit diffs).
101
+ heights: Array<{ component: ToolExecutionRow; height: number }>;
93
102
  members: ToolExecutionRow[];
94
103
  memberVersions: number[];
95
104
  themeSample: string;
@@ -1278,12 +1287,18 @@ function renderGroupedCallLines(
1278
1287
  rows: ToolExecutionRow[],
1279
1288
  width: number,
1280
1289
  theme: ThemeLike,
1281
- ): string[] {
1282
- const lines: string[] = [];
1290
+ ): Array<{ row: ToolExecutionRow; lines: string[] }> {
1291
+ const segments: Array<{ row: ToolExecutionRow; lines: string[] }> = [];
1283
1292
  let previousToolName: string | undefined;
1284
1293
  for (const row of rows) {
1294
+ const lines: string[] = [];
1285
1295
  const toolName = row.toolName ?? "tool";
1286
1296
  if (toolName !== previousToolName) {
1297
+ // One blank line between tool-name subgroups, mirroring the block's
1298
+ // leading blank so each `%` heading starts its own visual run. The
1299
+ // blank belongs to the new run's first row, keeping the per-member
1300
+ // click-routing heights aligned with the drawn lines.
1301
+ if (previousToolName !== undefined) lines.push("");
1287
1302
  lines.push(
1288
1303
  `${fgCollapsed(theme, "muted", GROUP_MARKER)} ${fgCollapsed(theme, "toolTitle", toolName, true)}`,
1289
1304
  );
@@ -1291,7 +1306,7 @@ function renderGroupedCallLines(
1291
1306
  }
1292
1307
 
1293
1308
  const color = rowHasFailed(row) ? "error" : "muted";
1294
- const prefix = ` ${fgCollapsed(theme, color, "•")} `;
1309
+ const prefix = ` ${fgCollapsedRail(theme, color, GROUP_CALL_MARKER)} `;
1295
1310
  const budget = Math.max(1, width - visibleWidth(prefix));
1296
1311
  const label = groupedChildLabel(row, budget, theme, color);
1297
1312
  const outcome = collapsedOutcome(row, budget, theme);
@@ -1304,8 +1319,9 @@ function renderGroupedCallLines(
1304
1319
  );
1305
1320
  if (row.toolName === "edit")
1306
1321
  lines.push(...renderedEditDiffLines(row, theme));
1322
+ segments.push({ row, lines });
1307
1323
  }
1308
- return lines;
1324
+ return segments;
1309
1325
  }
1310
1326
 
1311
1327
  function sameMembers(
@@ -1331,14 +1347,24 @@ function sameMemberVersions(
1331
1347
  );
1332
1348
  }
1333
1349
 
1350
+ type GroupBlock = {
1351
+ lines: string[];
1352
+ // Click-routing accounting aligned with `lines`: one entry per drawn row
1353
+ // segment, so mouse events land on the member that owns the line.
1354
+ heights: Array<{ component: ToolExecutionRow; height: number }>;
1355
+ };
1356
+
1334
1357
  function renderGroupedToolRows(
1335
1358
  row: ToolExecutionRow,
1336
1359
  rows: ToolExecutionRow[],
1337
1360
  width: number,
1338
1361
  state: PresentationPatchState,
1339
- ): string[] {
1362
+ ): GroupBlock {
1340
1363
  const theme = state.theme;
1341
- if (!theme) return state.originalRender.call(row, width);
1364
+ if (!theme) {
1365
+ const lines = state.originalRender.call(row, width);
1366
+ return { lines, heights: [{ component: row, height: lines.length }] };
1367
+ }
1342
1368
  const themeSample = themeSampleFor(theme);
1343
1369
  const cached = state.groupCache.get(row);
1344
1370
  const hasLiveMembers = rows.some(isLiveRow);
@@ -1350,32 +1376,52 @@ function renderGroupedToolRows(
1350
1376
  sameMembers(cached.members, rows) &&
1351
1377
  sameMemberVersions(rows, cached.memberVersions, state)
1352
1378
  ) {
1353
- return cached.lines;
1379
+ return { lines: cached.lines, heights: cached.heights };
1354
1380
  }
1355
1381
 
1356
1382
  const layout = insetLayout(width);
1357
- const body = renderGroupedCallLines(rows, layout.contentWidth, theme);
1358
- const lines = ["", ...insetLines(body, width)];
1383
+ const segments = renderGroupedCallLines(rows, layout.contentWidth, theme);
1384
+ const lines = [
1385
+ "",
1386
+ ...insetLines(
1387
+ segments.flatMap((s) => s.lines),
1388
+ width,
1389
+ ),
1390
+ ];
1391
+ // The leading blank and the first segment (tool-name header + call line)
1392
+ // both belong to the first member; every later member owns its lines.
1393
+ const heights = segments.map((segment, index) => ({
1394
+ component: segment.row,
1395
+ height: index === 0 ? segment.lines.length + 1 : segment.lines.length,
1396
+ }));
1359
1397
  if (hasLiveMembers) {
1360
1398
  state.groupCache.delete(row);
1361
1399
  } else {
1362
1400
  state.groupCache.set(row, {
1363
1401
  lines,
1402
+ heights,
1364
1403
  members: [...rows],
1365
1404
  memberVersions: rows.map((member) => state.rowVersions.get(member) ?? 0),
1366
1405
  themeSample,
1367
1406
  width,
1368
1407
  });
1369
1408
  }
1370
- return lines;
1409
+ return { lines, heights };
1371
1410
  }
1372
1411
 
1412
+ type ContainerRender = {
1413
+ lines: string[];
1414
+ // Click-routing accounting aligned with `lines`.
1415
+ heights: Array<{ component: unknown; height: number }>;
1416
+ };
1417
+
1373
1418
  function renderContainerWithToolGroups(
1374
1419
  children: unknown[],
1375
1420
  width: number,
1376
1421
  presentation: PresentationPatchState,
1377
- ): string[] {
1422
+ ): ContainerRender {
1378
1423
  const lines: string[] = [];
1424
+ const heights: Array<{ component: unknown; height: number }> = [];
1379
1425
  const rendered = new Map<number, string[]>();
1380
1426
  const renderAt = (index: number): string[] => {
1381
1427
  const cached = rendered.get(index);
@@ -1391,7 +1437,9 @@ function renderContainerWithToolGroups(
1391
1437
  !isToolExecutionRow(child) ||
1392
1438
  !isGroupableToolRow(child, children, index, renderAt, presentation)
1393
1439
  ) {
1394
- lines.push(...renderAt(index));
1440
+ const drawn = renderAt(index);
1441
+ lines.push(...drawn);
1442
+ heights.push({ component: child, height: drawn.length });
1395
1443
  continue;
1396
1444
  }
1397
1445
 
@@ -1426,7 +1474,9 @@ function renderContainerWithToolGroups(
1426
1474
  }
1427
1475
 
1428
1476
  if (group.length === 1) {
1429
- lines.push(...renderAt(index));
1477
+ const drawn = renderAt(index);
1478
+ lines.push(...drawn);
1479
+ heights.push({ component: child, height: drawn.length });
1430
1480
  continue;
1431
1481
  }
1432
1482
 
@@ -1434,11 +1484,13 @@ function renderContainerWithToolGroups(
1434
1484
  // Use a live member while any call is pending, then the first member once
1435
1485
  // settled. Either way, the grouped row keeps the same number of lines.
1436
1486
  const shellRow = group.find(isLiveRow) ?? child;
1437
- lines.push(...renderGroupedToolRows(shellRow, group, width, presentation));
1487
+ const block = renderGroupedToolRows(shellRow, group, width, presentation);
1488
+ lines.push(...block.lines);
1489
+ heights.push(...block.heights);
1438
1490
  index = lastMemberIndex;
1439
1491
  }
1440
1492
 
1441
- return lines;
1493
+ return { lines, heights };
1442
1494
  }
1443
1495
 
1444
1496
  // TODO: Replace prototype patching with a public Pi tool/transcript rendering API when available.
@@ -1483,11 +1535,20 @@ function installGroupingPatch(
1483
1535
  // inset) decorate children through the shared hooks before grouping
1484
1536
  // rewrites the rows; delegation alone cannot reach them from here.
1485
1537
  restoreHooks = runChatContainerHooks(this, children, width);
1486
- return renderContainerWithToolGroups(
1538
+ const drawn = renderContainerWithToolGroups(
1487
1539
  children,
1488
1540
  width,
1489
1541
  state.presentation,
1490
1542
  );
1543
+ // The grouping path bypasses the native render that refreshes the
1544
+ // container's mouse-layout cache. Publish the drawn accounting so
1545
+ // click-to-expand (Pi routes by per-child line heights) keeps landing
1546
+ // on the row that actually owns the line, including grouped members.
1547
+ (this as { mouseLayout?: unknown }).mouseLayout = {
1548
+ width,
1549
+ children: drawn.heights,
1550
+ };
1551
+ return drawn.lines;
1491
1552
  } catch {
1492
1553
  return state.originalRender.call(this, width);
1493
1554
  } finally {
@@ -1574,6 +1635,34 @@ function installPresentationPatch(): PresentationPatchState | undefined {
1574
1635
  state.collapsedCache.delete(this);
1575
1636
  state.originalUpdateDisplay.call(this);
1576
1637
  };
1638
+ // Pi 0.85 wraps settled tool results in a click-to-expand MouseRegion.
1639
+ // The custom collapsed row replaces those lines with a single summary, so
1640
+ // the region never receives the click; toggle expansion here instead.
1641
+ let patchedHandleMouse:
1642
+ | ((this: ToolExecutionRow, event: unknown) => unknown)
1643
+ | undefined;
1644
+ if (typeof proto.handleMouse === "function") {
1645
+ state.originalHandleMouse = proto.handleMouse;
1646
+ patchedHandleMouse = function handleMouseWithCollapsedToggle(
1647
+ this: ToolExecutionRow,
1648
+ event: unknown,
1649
+ ): unknown {
1650
+ const mouseEvent = event as { type?: unknown; button?: unknown };
1651
+ if (
1652
+ state.theme &&
1653
+ this.expanded === false &&
1654
+ !!this.result &&
1655
+ mouseEvent?.type === "click" &&
1656
+ mouseEvent.button === "left" &&
1657
+ typeof this.setExpanded === "function"
1658
+ ) {
1659
+ this.setExpanded(true);
1660
+ return { handled: true };
1661
+ }
1662
+ return state.originalHandleMouse?.call(this, event);
1663
+ };
1664
+ proto.handleMouse = patchedHandleMouse;
1665
+ }
1577
1666
  const patchedRender = function renderWithToolPresentation(
1578
1667
  this: ToolExecutionRow,
1579
1668
  width: number,
@@ -1680,6 +1769,7 @@ function uninstallPresentationPatch(
1680
1769
  [PRESENTATION_PATCHED]?: PresentationPatchState;
1681
1770
  render?: (width: number) => string[];
1682
1771
  updateDisplay?: () => void;
1772
+ handleMouse?: (event: unknown) => unknown;
1683
1773
  };
1684
1774
  if (
1685
1775
  proto[PRESENTATION_PATCHED] !== state ||
@@ -1690,6 +1780,16 @@ function uninstallPresentationPatch(
1690
1780
  }
1691
1781
  proto.render = state.originalRender;
1692
1782
  proto.updateDisplay = state.originalUpdateDisplay;
1783
+ if (
1784
+ state.patchedHandleMouse &&
1785
+ proto.handleMouse === state.patchedHandleMouse
1786
+ ) {
1787
+ if (state.originalHandleMouse) {
1788
+ proto.handleMouse = state.originalHandleMouse;
1789
+ } else {
1790
+ delete proto.handleMouse;
1791
+ }
1792
+ }
1693
1793
  delete proto[PRESENTATION_PATCHED];
1694
1794
  }
1695
1795
 
package/src/muted.ts CHANGED
@@ -78,3 +78,17 @@ export function fgCollapsed(
78
78
  }
79
79
  return theme.fg(color, bold && theme.bold ? theme.bold(text) : text);
80
80
  }
81
+
82
+ // Decoration rails recede further than collapsed text. No palette token is
83
+ // guaranteed lighter than the collapsed color (e.g. catppuccin-latte's `dim`
84
+ // resolves to the same overlay0 as syntaxComment), so apply the terminal's
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`;
94
+ }