@pi-kaush/pi-tool-call-markers 0.3.0 → 0.3.2

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,20 @@
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
+
13
+ - Restyle subagent rows: the subagent marker is now `↪` (was `&`), and single
14
+ calls drop the `subagent` tool label — `↪ 🤖 [coder][c3po] Implement the…` —
15
+ with chain/parallel step lines reordered to emoji, profile badge, name badge
16
+ (`1. 🐝 [workhorse][bee] …`). Single-call headings are now scraped too, so
17
+ agent emojis appear on single rows, not just chain/parallel steps.
18
+
5
19
  - Color collapsed rows (tool calls, `+ Thought`, subagents) from the
6
20
  theme's `syntaxComment` token — which ships with every theme — instead of
7
21
  the louder muted/toolTitle/toolOutput split. The bolded tool name and the
package/README.md CHANGED
@@ -34,19 +34,19 @@ Collapsed tool rows use semantic theme colors with no gear, background fill, box
34
34
 
35
35
  ## Subagent plans
36
36
 
37
- A recognized `subagent` call renders as an unboxed plan in the shared tool aesthetic, marked with `&` instead of the ordinary `%` tool marker:
37
+ A recognized `subagent` call renders as an unboxed plan in the shared tool aesthetic, marked with `↪` instead of the ordinary `%` tool marker:
38
38
 
39
39
  ```text
40
- & subagent chain (3 steps) [repo-review]
40
+ subagent chain (3 steps) [repo-review]
41
41
  1. 🐝 bee [workhorse] Challenge the compatibility conclusion…
42
42
  2. 🐝 bee …
43
43
  ```
44
44
 
45
- Single calls stay on one `& subagent <agent> [profile] <task preview>` row; chain calls get a heading with the kind, count, and scope followed by numbered steps (parallel tasks list without numbers). Agent display names (emoji + name) are scraped from the native plan component with an args fallback, and render in `accent`; everything else stays muted, and failed subagents go full red. Subagents never join ordinary tool groups.
45
+ Single calls stay on one `↪ [<emoji>] [<profile>][<agent>] <task preview>` row — no tool label; the `↪` marker identifies it. Chain calls get a heading with the kind, count, and scope followed by numbered steps (parallel tasks list without numbers), with each step ordered emoji, profile badge, name badge. Agent display names (emoji + name) are scraped from the native plan component — including the single-call heading — with an args fallback, and render in `accent`; everything else stays muted, and failed subagents go full red. Subagents never join ordinary tool groups.
46
46
 
47
47
  While a subagent runs, the plan headline's tail shows live progress from the streamed result details — `→ 1 turn · provider/model` in the warning tone — and a settled call keeps the same `→ N turns · provider/model` summary in muted (turns aggregate across tasks; the model shows only when every task used the same one).
48
48
 
49
- Malformed, ambiguous, future, or too-narrow shapes fall back to the generic `& subagent …` collapsed row rather than dropping information, and `Ctrl+O` still exposes the native subagent renderer.
49
+ Malformed, ambiguous, future, or too-narrow shapes fall back to the generic `↪ subagent …` collapsed row rather than dropping information, and `Ctrl+O` still exposes the native subagent renderer.
50
50
 
51
51
  ## Edit diffs
52
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-kaush/pi-tool-call-markers",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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
@@ -20,7 +20,7 @@ import { fgCollapsed } from "./muted.ts";
20
20
 
21
21
  const OUTER_INSET = 2;
22
22
  const GROUP_MARKER = "%";
23
- const SUBAGENT_MARKER = "&";
23
+ const SUBAGENT_MARKER = "";
24
24
  const PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v3");
25
25
  const LEGACY_PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v2");
26
26
  const GROUPING_PATCHED = Symbol.for("kg.pi.toolGrouping.v1");
@@ -64,6 +64,8 @@ type ToolExecutionRow = {
64
64
  imageSpacers?: unknown[];
65
65
  getRenderShell?(): "default" | "self";
66
66
  getTextOutput?(): string;
67
+ setExpanded?(expanded: boolean): void;
68
+ handleMouse?(event: unknown): unknown;
67
69
  };
68
70
 
69
71
  type PresentationPatchState = {
@@ -76,8 +78,10 @@ type PresentationPatchState = {
76
78
  rowGroups: WeakMap<ToolExecutionRow, ToolExecutionRow[]>;
77
79
  originalRender: (width: number) => string[];
78
80
  originalUpdateDisplay: () => void;
81
+ originalHandleMouse?: (this: ToolExecutionRow, event: unknown) => unknown;
79
82
  patchedRender?: (width: number) => string[];
80
83
  patchedUpdateDisplay?: () => void;
84
+ patchedHandleMouse?: (this: ToolExecutionRow, event: unknown) => unknown;
81
85
  };
82
86
 
83
87
  type GroupingPatchState = {
@@ -90,6 +94,10 @@ type GroupingPatchState = {
90
94
 
91
95
  type GroupRenderCache = {
92
96
  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).
100
+ heights: Array<{ component: ToolExecutionRow; height: number }>;
93
101
  members: ToolExecutionRow[];
94
102
  memberVersions: number[];
95
103
  themeSample: string;
@@ -1028,6 +1036,19 @@ function parseStepContent(content: string): {
1028
1036
  };
1029
1037
  }
1030
1038
 
1039
+ // Splits a scraped "emoji name" display into its emoji prefix and bare name;
1040
+ // plain names pass through with no emoji.
1041
+ function splitEmojiName(
1042
+ displayName: string,
1043
+ bareName: string,
1044
+ ): { emoji: string; name: string } {
1045
+ const tokens = displayName.trim().split(/\s+/).filter(Boolean);
1046
+ if (tokens.length > 1 && tokens[tokens.length - 1] === bareName) {
1047
+ return { emoji: tokens.slice(0, -1).join(" "), name: bareName };
1048
+ }
1049
+ return { emoji: "", name: bareName };
1050
+ }
1051
+
1031
1052
  // Agent display names (emoji + name) come from the subagent extension's own
1032
1053
  // call component when available; args only carry the bare agent name.
1033
1054
  function scrapeSubagentDisplayNames(
@@ -1042,10 +1063,16 @@ function scrapeSubagentDisplayNames(
1042
1063
  try {
1043
1064
  for (const raw of component.render(120)) {
1044
1065
  const line = sanitizeInline(stripAnsi(raw)).trim();
1045
- if (!line || /^subagent\b/.test(line)) continue;
1046
- const { displayName, bareName } = parseStepContent(
1047
- line.replace(/^\d+\.\s*/, ""),
1048
- );
1066
+ if (!line) continue;
1067
+ // Single calls carry the agent name on their "subagent <name>" heading
1068
+ // line; stripping the heading word (and step numbers) lets both shapes
1069
+ // flow through the same parse. Heading remainders without an emoji
1070
+ // (e.g. "chain (2 steps)") degrade to displayName === bareName and are
1071
+ // skipped by the guard below.
1072
+ const content = line
1073
+ .replace(/^subagent\b\s*/, "")
1074
+ .replace(/^\d+\.\s*/, "");
1075
+ const { displayName, bareName } = parseStepContent(content);
1049
1076
  if (displayName && bareName && displayName !== bareName) {
1050
1077
  names.set(bareName, displayName);
1051
1078
  }
@@ -1091,10 +1118,11 @@ function subagentProgressText(row: ToolExecutionRow): string | undefined {
1091
1118
  return parts.length > 0 ? parts.join(" · ") : undefined;
1092
1119
  }
1093
1120
 
1094
- // Subagents render as an ordinary unboxed tool block: a `%` heading with the
1121
+ // Subagents render as an ordinary unboxed tool block: a `↪` heading with the
1095
1122
  // plan kind/count/scope, then the numbered chain steps or parallel tasks with
1096
- // agent names in accent. Everything else stays muted (or error on failure),
1097
- // matching the shared tool-row aesthetic.
1123
+ // agent identities (emoji, profile badge, name badge) in accent. Single calls
1124
+ // collapse to one row: `↪ <emoji> [<profile>][<name>] <task preview>`. Everything
1125
+ // else stays muted (or error on failure), matching the shared tool-row aesthetic.
1098
1126
  function renderSubagentPlan(
1099
1127
  row: ToolExecutionRow,
1100
1128
  width: number,
@@ -1110,19 +1138,21 @@ function renderSubagentPlan(
1110
1138
  const displayNames = scrapeSubagentDisplayNames(row);
1111
1139
  // Agent/profile/task values are model-supplied; sanitize them like every
1112
1140
  // other collapsed-row text so control bytes cannot reach the terminal.
1113
- const displayOf = (agent: string) =>
1114
- fgCollapsed(
1115
- theme,
1116
- nameColor,
1117
- displayNames.get(agent) ?? sanitizeInline(agent),
1118
- );
1141
+ // Identity order: emoji, then the profile and agent-name badges.
1142
+ const identityOf = (step: SubagentStep) => {
1143
+ const displayName =
1144
+ displayNames.get(step.agent) ?? sanitizeInline(step.agent);
1145
+ const { emoji, name } = splitEmojiName(displayName, step.agent);
1146
+ const accentPart = (text: string) => fgCollapsed(theme, nameColor, text);
1147
+ const emojiPart = emoji ? `${accentPart(emoji)} ` : "";
1148
+ const profilePart = step.profile
1149
+ ? accentPart(`[${sanitizeInline(step.profile)}]`)
1150
+ : "";
1151
+ return `${emojiPart}${profilePart}${accentPart(`[${sanitizeInline(name)}]`)}`;
1152
+ };
1119
1153
  const detailColor = failed ? "error" : "toolOutput";
1120
- const detailOf = (step: SubagentStep) =>
1121
- fgCollapsed(
1122
- theme,
1123
- detailColor,
1124
- `${step.profile ? ` [${sanitizeInline(step.profile)}]` : ""} ${subagentStepPreview(step.task)}`,
1125
- );
1154
+ const previewOf = (step: SubagentStep) =>
1155
+ fgCollapsed(theme, detailColor, ` ${subagentStepPreview(step.task)}`);
1126
1156
 
1127
1157
  const marker = `${fgCollapsed(theme, color, SUBAGENT_MARKER, true)} `;
1128
1158
  const budget = Math.max(1, width - visibleWidth(marker));
@@ -1146,14 +1176,7 @@ function renderSubagentPlan(
1146
1176
 
1147
1177
  if (plan.kind === "single") {
1148
1178
  const step = plan.steps[0]!;
1149
- return [
1150
- headline(
1151
- fgCollapsed(theme, failed ? "error" : "toolTitle", "subagent", true) +
1152
- " " +
1153
- displayOf(step.agent) +
1154
- detailOf(step),
1155
- ),
1156
- ];
1179
+ return [headline(identityOf(step) + previewOf(step))];
1157
1180
  }
1158
1181
 
1159
1182
  const kindLabel =
@@ -1175,7 +1198,7 @@ function renderSubagentPlan(
1175
1198
  plan.kind === "chain"
1176
1199
  ? `${fgCollapsed(theme, color, `${index + 1}.`)} `
1177
1200
  : "";
1178
- lines.push(` ${number}${displayOf(step.agent)}${detailOf(step)}`);
1201
+ lines.push(` ${number}${identityOf(step)}${previewOf(step)}`);
1179
1202
  }
1180
1203
  if (plan.steps.length > shown.length) {
1181
1204
  lines.push(
@@ -1263,10 +1286,11 @@ function renderGroupedCallLines(
1263
1286
  rows: ToolExecutionRow[],
1264
1287
  width: number,
1265
1288
  theme: ThemeLike,
1266
- ): string[] {
1267
- const lines: string[] = [];
1289
+ ): Array<{ row: ToolExecutionRow; lines: string[] }> {
1290
+ const segments: Array<{ row: ToolExecutionRow; lines: string[] }> = [];
1268
1291
  let previousToolName: string | undefined;
1269
1292
  for (const row of rows) {
1293
+ const lines: string[] = [];
1270
1294
  const toolName = row.toolName ?? "tool";
1271
1295
  if (toolName !== previousToolName) {
1272
1296
  lines.push(
@@ -1289,8 +1313,9 @@ function renderGroupedCallLines(
1289
1313
  );
1290
1314
  if (row.toolName === "edit")
1291
1315
  lines.push(...renderedEditDiffLines(row, theme));
1316
+ segments.push({ row, lines });
1292
1317
  }
1293
- return lines;
1318
+ return segments;
1294
1319
  }
1295
1320
 
1296
1321
  function sameMembers(
@@ -1316,14 +1341,24 @@ function sameMemberVersions(
1316
1341
  );
1317
1342
  }
1318
1343
 
1344
+ type GroupBlock = {
1345
+ lines: string[];
1346
+ // Click-routing accounting aligned with `lines`: one entry per drawn row
1347
+ // segment, so mouse events land on the member that owns the line.
1348
+ heights: Array<{ component: ToolExecutionRow; height: number }>;
1349
+ };
1350
+
1319
1351
  function renderGroupedToolRows(
1320
1352
  row: ToolExecutionRow,
1321
1353
  rows: ToolExecutionRow[],
1322
1354
  width: number,
1323
1355
  state: PresentationPatchState,
1324
- ): string[] {
1356
+ ): GroupBlock {
1325
1357
  const theme = state.theme;
1326
- if (!theme) return state.originalRender.call(row, width);
1358
+ if (!theme) {
1359
+ const lines = state.originalRender.call(row, width);
1360
+ return { lines, heights: [{ component: row, height: lines.length }] };
1361
+ }
1327
1362
  const themeSample = themeSampleFor(theme);
1328
1363
  const cached = state.groupCache.get(row);
1329
1364
  const hasLiveMembers = rows.some(isLiveRow);
@@ -1335,32 +1370,52 @@ function renderGroupedToolRows(
1335
1370
  sameMembers(cached.members, rows) &&
1336
1371
  sameMemberVersions(rows, cached.memberVersions, state)
1337
1372
  ) {
1338
- return cached.lines;
1373
+ return { lines: cached.lines, heights: cached.heights };
1339
1374
  }
1340
1375
 
1341
1376
  const layout = insetLayout(width);
1342
- const body = renderGroupedCallLines(rows, layout.contentWidth, theme);
1343
- const lines = ["", ...insetLines(body, width)];
1377
+ const segments = renderGroupedCallLines(rows, layout.contentWidth, theme);
1378
+ const lines = [
1379
+ "",
1380
+ ...insetLines(
1381
+ segments.flatMap((s) => s.lines),
1382
+ width,
1383
+ ),
1384
+ ];
1385
+ // The leading blank and the first segment (tool-name header + call line)
1386
+ // both belong to the first member; every later member owns its lines.
1387
+ const heights = segments.map((segment, index) => ({
1388
+ component: segment.row,
1389
+ height: index === 0 ? segment.lines.length + 1 : segment.lines.length,
1390
+ }));
1344
1391
  if (hasLiveMembers) {
1345
1392
  state.groupCache.delete(row);
1346
1393
  } else {
1347
1394
  state.groupCache.set(row, {
1348
1395
  lines,
1396
+ heights,
1349
1397
  members: [...rows],
1350
1398
  memberVersions: rows.map((member) => state.rowVersions.get(member) ?? 0),
1351
1399
  themeSample,
1352
1400
  width,
1353
1401
  });
1354
1402
  }
1355
- return lines;
1403
+ return { lines, heights };
1356
1404
  }
1357
1405
 
1406
+ type ContainerRender = {
1407
+ lines: string[];
1408
+ // Click-routing accounting aligned with `lines`.
1409
+ heights: Array<{ component: unknown; height: number }>;
1410
+ };
1411
+
1358
1412
  function renderContainerWithToolGroups(
1359
1413
  children: unknown[],
1360
1414
  width: number,
1361
1415
  presentation: PresentationPatchState,
1362
- ): string[] {
1416
+ ): ContainerRender {
1363
1417
  const lines: string[] = [];
1418
+ const heights: Array<{ component: unknown; height: number }> = [];
1364
1419
  const rendered = new Map<number, string[]>();
1365
1420
  const renderAt = (index: number): string[] => {
1366
1421
  const cached = rendered.get(index);
@@ -1376,7 +1431,9 @@ function renderContainerWithToolGroups(
1376
1431
  !isToolExecutionRow(child) ||
1377
1432
  !isGroupableToolRow(child, children, index, renderAt, presentation)
1378
1433
  ) {
1379
- lines.push(...renderAt(index));
1434
+ const drawn = renderAt(index);
1435
+ lines.push(...drawn);
1436
+ heights.push({ component: child, height: drawn.length });
1380
1437
  continue;
1381
1438
  }
1382
1439
 
@@ -1411,7 +1468,9 @@ function renderContainerWithToolGroups(
1411
1468
  }
1412
1469
 
1413
1470
  if (group.length === 1) {
1414
- lines.push(...renderAt(index));
1471
+ const drawn = renderAt(index);
1472
+ lines.push(...drawn);
1473
+ heights.push({ component: child, height: drawn.length });
1415
1474
  continue;
1416
1475
  }
1417
1476
 
@@ -1419,11 +1478,13 @@ function renderContainerWithToolGroups(
1419
1478
  // Use a live member while any call is pending, then the first member once
1420
1479
  // settled. Either way, the grouped row keeps the same number of lines.
1421
1480
  const shellRow = group.find(isLiveRow) ?? child;
1422
- lines.push(...renderGroupedToolRows(shellRow, group, width, presentation));
1481
+ const block = renderGroupedToolRows(shellRow, group, width, presentation);
1482
+ lines.push(...block.lines);
1483
+ heights.push(...block.heights);
1423
1484
  index = lastMemberIndex;
1424
1485
  }
1425
1486
 
1426
- return lines;
1487
+ return { lines, heights };
1427
1488
  }
1428
1489
 
1429
1490
  // TODO: Replace prototype patching with a public Pi tool/transcript rendering API when available.
@@ -1468,11 +1529,20 @@ function installGroupingPatch(
1468
1529
  // inset) decorate children through the shared hooks before grouping
1469
1530
  // rewrites the rows; delegation alone cannot reach them from here.
1470
1531
  restoreHooks = runChatContainerHooks(this, children, width);
1471
- return renderContainerWithToolGroups(
1532
+ const drawn = renderContainerWithToolGroups(
1472
1533
  children,
1473
1534
  width,
1474
1535
  state.presentation,
1475
1536
  );
1537
+ // The grouping path bypasses the native render that refreshes the
1538
+ // container's mouse-layout cache. Publish the drawn accounting so
1539
+ // click-to-expand (Pi routes by per-child line heights) keeps landing
1540
+ // on the row that actually owns the line, including grouped members.
1541
+ (this as { mouseLayout?: unknown }).mouseLayout = {
1542
+ width,
1543
+ children: drawn.heights,
1544
+ };
1545
+ return drawn.lines;
1476
1546
  } catch {
1477
1547
  return state.originalRender.call(this, width);
1478
1548
  } finally {
@@ -1559,6 +1629,34 @@ function installPresentationPatch(): PresentationPatchState | undefined {
1559
1629
  state.collapsedCache.delete(this);
1560
1630
  state.originalUpdateDisplay.call(this);
1561
1631
  };
1632
+ // Pi 0.85 wraps settled tool results in a click-to-expand MouseRegion.
1633
+ // The custom collapsed row replaces those lines with a single summary, so
1634
+ // the region never receives the click; toggle expansion here instead.
1635
+ let patchedHandleMouse:
1636
+ | ((this: ToolExecutionRow, event: unknown) => unknown)
1637
+ | undefined;
1638
+ if (typeof proto.handleMouse === "function") {
1639
+ state.originalHandleMouse = proto.handleMouse;
1640
+ patchedHandleMouse = function handleMouseWithCollapsedToggle(
1641
+ this: ToolExecutionRow,
1642
+ event: unknown,
1643
+ ): unknown {
1644
+ const mouseEvent = event as { type?: unknown; button?: unknown };
1645
+ if (
1646
+ state.theme &&
1647
+ this.expanded === false &&
1648
+ !!this.result &&
1649
+ mouseEvent?.type === "click" &&
1650
+ mouseEvent.button === "left" &&
1651
+ typeof this.setExpanded === "function"
1652
+ ) {
1653
+ this.setExpanded(true);
1654
+ return { handled: true };
1655
+ }
1656
+ return state.originalHandleMouse?.call(this, event);
1657
+ };
1658
+ proto.handleMouse = patchedHandleMouse;
1659
+ }
1562
1660
  const patchedRender = function renderWithToolPresentation(
1563
1661
  this: ToolExecutionRow,
1564
1662
  width: number,
@@ -1665,6 +1763,7 @@ function uninstallPresentationPatch(
1665
1763
  [PRESENTATION_PATCHED]?: PresentationPatchState;
1666
1764
  render?: (width: number) => string[];
1667
1765
  updateDisplay?: () => void;
1766
+ handleMouse?: (event: unknown) => unknown;
1668
1767
  };
1669
1768
  if (
1670
1769
  proto[PRESENTATION_PATCHED] !== state ||
@@ -1675,6 +1774,16 @@ function uninstallPresentationPatch(
1675
1774
  }
1676
1775
  proto.render = state.originalRender;
1677
1776
  proto.updateDisplay = state.originalUpdateDisplay;
1777
+ if (
1778
+ state.patchedHandleMouse &&
1779
+ proto.handleMouse === state.patchedHandleMouse
1780
+ ) {
1781
+ if (state.originalHandleMouse) {
1782
+ proto.handleMouse = state.originalHandleMouse;
1783
+ } else {
1784
+ delete proto.handleMouse;
1785
+ }
1786
+ }
1678
1787
  delete proto[PRESENTATION_PATCHED];
1679
1788
  }
1680
1789