pi-cc-extensions 0.8.25 → 0.8.27

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.
@@ -6,6 +6,12 @@ import {
6
6
  type CompactStyleMode,
7
7
  } from "./compact-style.ts";
8
8
  import { showTextPreview } from "./context.ts";
9
+ import {
10
+ installToolGrouping,
11
+ ToolGroupComponent,
12
+ type ToolGroupingHooks,
13
+ } from "./tool-grouping.ts";
14
+ import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "./tool-loading-icon.ts";
9
15
  import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
10
16
  import {
11
17
  DEFAULT_TOOL_DISPLAY_CONFIG,
@@ -27,6 +33,7 @@ import {
27
33
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
28
34
  import { homedir } from "node:os";
29
35
  import { join } from "node:path";
36
+ import { inspect } from "node:util";
30
37
 
31
38
  /**
32
39
  * Claude Code Style for pi.
@@ -238,13 +245,30 @@ function oneLine(value: unknown, max = 72): string {
238
245
  }
239
246
 
240
247
  function rawTextFromResult(result: any): string {
241
- const item = result?.content?.find?.((c: any) => c.type === "text");
242
- return item?.type === "text" ? String(item.text ?? "") : "";
248
+ return Array.isArray(result?.content)
249
+ ? result.content
250
+ .filter((item: any) => item?.type === "text")
251
+ .map((item: any) => String(item.text ?? ""))
252
+ .join("\n")
253
+ : "";
254
+ }
255
+
256
+ function detailsFromResult(result: any): string {
257
+ if (result?.details === undefined) return "";
258
+ const details =
259
+ typeof result.details === "string"
260
+ ? result.details
261
+ : inspect(result.details, { depth: 8, breakLength: 100, compact: false });
262
+ return sanitizeToolResultText(details, 16_384);
243
263
  }
244
264
 
245
- function textFromResult(result: any): string {
265
+ function textFromResult(result: any, expanded = false): string {
246
266
  // Compact previews only need short text; bound sanitize work.
247
- return sanitizeToolResultText(rawTextFromResult(result), 16_384);
267
+ const content = sanitizeToolResultText(rawTextFromResult(result), 16_384);
268
+ const details = detailsFromResult(result);
269
+ if (!content) return details;
270
+ if (!expanded || !details || details === content) return content;
271
+ return `${content}\nDetails:\n${details}`;
248
272
  }
249
273
 
250
274
  export function outputLineCount(result: any): number {
@@ -267,23 +291,6 @@ function toolIcon(_name: string): string {
267
291
  return "●";
268
292
  }
269
293
 
270
- // Match the braille loader shown to the left of pi's "Working..." row.
271
- // Every frame is one cell wide, so tool titles remain horizontally stable.
272
- const WORKING_LOADER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
273
- const TOOL_PENDING_FRAMES: Record<string, string[]> = {
274
- read: WORKING_LOADER_FRAMES,
275
- bash: WORKING_LOADER_FRAMES,
276
- edit: WORKING_LOADER_FRAMES,
277
- write: WORKING_LOADER_FRAMES,
278
- find: WORKING_LOADER_FRAMES,
279
- grep: WORKING_LOADER_FRAMES,
280
- ls: WORKING_LOADER_FRAMES,
281
- };
282
-
283
- function animationFrame(frames: string[], intervalMs = 120): string {
284
- return frames[Math.floor(Date.now() / intervalMs) % frames.length] ?? frames[0] ?? "";
285
- }
286
-
287
294
  const activeAnimationContexts = new Set<any>();
288
295
  let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
289
296
 
@@ -308,7 +315,7 @@ function clearAllAnimations() {
308
315
  }
309
316
  }
310
317
 
311
- function scheduleAnimation(context: any, intervalMs = 80) {
318
+ function scheduleAnimation(context: any, intervalMs = TOOL_LOADING_INTERVAL_MS) {
312
319
  const state = (context.state ??= {});
313
320
  if (state.ccstyleAnimationScheduled) return;
314
321
  state.ccstyleAnimationScheduled = true;
@@ -326,8 +333,8 @@ function scheduleAnimation(context: any, intervalMs = 80) {
326
333
  }
327
334
  }
328
335
 
329
- function pendingIcon(name: string): string {
330
- return animationFrame(TOOL_PENDING_FRAMES[name] ?? [toolIcon(name)], 80);
336
+ function pendingIcon(_name: string): string {
337
+ return toolLoadingIcon();
331
338
  }
332
339
 
333
340
  type ToolVisualState = "pending" | "success" | "error";
@@ -434,7 +441,7 @@ const EXPANDED_TOOL_IO_VIEW_GENERATION = Symbol("ccstyle-expanded-tool-io-view")
434
441
  * Expanded tool body with clear Input / Output sections (Grok Build–style).
435
442
  *
436
443
  * Visual frame:
437
- * ┌ Input [show more]
444
+ * ├ Input [show more]
438
445
  * │ path: src/a.ts
439
446
  * │
440
447
  * └ Output [show more]
@@ -538,7 +545,7 @@ export class ExpandedToolIoView {
538
545
 
539
546
  const theme = this.theme;
540
547
  const safeWidth = Math.max(1, Math.floor(width));
541
- const rail = " │ ";
548
+ const rail = " │ ";
542
549
  const railWidth = visibleWidth(rail);
543
550
  const bodyWidth = toolViewportWidth(safeWidth);
544
551
  const contentWidth = Math.max(1, bodyWidth - railWidth);
@@ -547,12 +554,12 @@ export class ExpandedToolIoView {
547
554
  this.truncated = { input: false, output: false };
548
555
 
549
556
  const pushHeader = (
550
- corner: "┌" | "└",
557
+ corner: "├" | "└",
551
558
  label: string,
552
559
  section: ToolIoSection,
553
560
  showMore: boolean,
554
561
  ) => {
555
- const mark = theme.fg("dim", ` ${corner} `);
562
+ const mark = theme.fg("dim", ` ${corner} `);
556
563
  const title = theme.fg(
557
564
  "accent",
558
565
  typeof theme.bold === "function" ? theme.bold(label) : label,
@@ -563,12 +570,13 @@ export class ExpandedToolIoView {
563
570
  lines.push(truncateToWidth(mark + title + more, safeWidth, ""));
564
571
  };
565
572
 
566
- const pushRailLine = (styledContent: string) => {
567
- lines.push(truncateToWidth(theme.fg("dim", rail) + styledContent, safeWidth, ""));
573
+ const pushRailLine = (styledContent: string, continued = true) => {
574
+ const prefix = continued ? rail : " ";
575
+ lines.push(truncateToWidth(theme.fg("dim", prefix) + styledContent, safeWidth, ""));
568
576
  };
569
577
 
570
578
  const pushBlankRail = () => {
571
- lines.push(truncateToWidth(theme.fg("dim", " │"), safeWidth, ""));
579
+ lines.push(truncateToWidth(theme.fg("dim", " │"), safeWidth, ""));
572
580
  };
573
581
 
574
582
  /** Style `key: value` input rows — dim keys, readable values. */
@@ -576,16 +584,16 @@ export class ExpandedToolIoView {
576
584
  const match = rawLine.match(/^([A-Za-z_][\w.-]*)(:\s*)(.*)$/);
577
585
  if (!match) return theme.fg("muted", rawLine);
578
586
  const [, key, sep, rest] = match;
579
- return theme.fg("dim", key + sep) + theme.fg("text", rest ?? "");
587
+ return theme.fg("dim", key + sep) + theme.fg("muted", rest ?? "");
580
588
  };
581
589
 
582
590
  const pushBody = (
583
591
  body: string,
584
- opts: { input?: boolean; limit: number },
592
+ opts: { input?: boolean; limit: number; continued?: boolean },
585
593
  ): boolean /* truncated */ => {
586
594
  const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
587
595
  if (!raw.trim()) {
588
- pushRailLine(theme.fg("dim", "(empty)"));
596
+ pushRailLine(theme.fg("dim", "(empty)"), opts.continued);
589
597
  return false;
590
598
  }
591
599
  const sourceLines = raw.split("\n");
@@ -600,11 +608,11 @@ export class ExpandedToolIoView {
600
608
  // theme/wrap measurements disagree slightly.
601
609
  const truncated = wrapped.length > opts.limit || sourceLines.length > opts.limit;
602
610
  const visible = truncated ? wrapped.slice(0, Math.min(opts.limit, wrapped.length)) : wrapped;
603
- for (const line of visible) pushRailLine(line);
611
+ for (const line of visible) pushRailLine(line, opts.continued);
604
612
  if (truncated) {
605
613
  const hidden = Math.max(0, wrapped.length - visible.length);
606
614
  if (hidden > 0) {
607
- pushRailLine(theme.fg("dim", `… +${hidden} more lines`));
615
+ pushRailLine(theme.fg("dim", `… +${hidden} more lines`), opts.continued);
608
616
  }
609
617
  }
610
618
  return truncated;
@@ -628,16 +636,20 @@ export class ExpandedToolIoView {
628
636
 
629
637
  if (hasInput) {
630
638
  this.truncated.input = inputWouldTruncate;
631
- pushHeader("┌", "Input", "input", inputWouldTruncate);
632
- pushBody(this.inputBody, { input: true, limit: this.maxInputLines });
639
+ pushHeader("├", "Input", "input", inputWouldTruncate);
640
+ pushBody(this.inputBody, {
641
+ input: true,
642
+ limit: this.maxInputLines,
643
+ continued: true,
644
+ });
633
645
  pushBlankRail();
634
646
  this.truncated.output = outputWouldTruncate;
635
647
  pushHeader("└", "Output", "output", outputWouldTruncate);
636
- pushBody(outputText, { limit: this.maxOutputLines });
648
+ pushBody(outputText, { limit: this.maxOutputLines, continued: false });
637
649
  } else {
638
650
  this.truncated.output = outputWouldTruncate;
639
- pushHeader("┌", "Output", "output", outputWouldTruncate);
640
- pushBody(outputText, { limit: this.maxOutputLines });
651
+ pushHeader("└", "Output", "output", outputWouldTruncate);
652
+ pushBody(outputText, { limit: this.maxOutputLines, continued: false });
641
653
  }
642
654
 
643
655
  this.cachedWidth = width;
@@ -682,7 +694,7 @@ function bodyExceedsLineLimit(
682
694
  if (asInput) {
683
695
  const match = source.match(/^([A-Za-z_][\w.-]*)(:\s*)(.*)$/);
684
696
  styled = match
685
- ? theme.fg("dim", match[1] + match[2]) + theme.fg("text", match[3] ?? "")
697
+ ? theme.fg("dim", match[1] + match[2]) + theme.fg("muted", match[3] ?? "")
686
698
  : theme.fg("muted", source);
687
699
  } else {
688
700
  styled = theme.fg(bodyColor, source);
@@ -695,16 +707,16 @@ function bodyExceedsLineLimit(
695
707
  }
696
708
 
697
709
  export function renderCollapsedToolResult(body: string, collapsedHint = ""): string {
698
- return ` ↳ ${body}${collapsedHint}`;
710
+ return ` ↳ ${body}${collapsedHint}`;
699
711
  }
700
712
 
701
713
  export function renderCollapsedToolResultToWidth(
702
714
  body: string,
703
715
  collapsedHint: string,
704
716
  width: number,
717
+ prefix = " ↳ ",
705
718
  ): string {
706
719
  const previewWidth = toolViewportWidth(width);
707
- const prefix = " ↳ ";
708
720
  const bodyWidth = Math.max(1, previewWidth - visibleWidth(prefix) - visibleWidth(collapsedHint));
709
721
  return truncateToWidth(
710
722
  prefix + middleTruncateToWidth(body, bodyWidth) + collapsedHint,
@@ -879,6 +891,7 @@ let assistantMessageActive = false;
879
891
  let scrollButtonSyncScheduled = false;
880
892
  let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
881
893
  let hoveredToolCallId: string | null = null;
894
+ let hoveredToolGroup: ToolGroupComponent | null = null;
882
895
 
883
896
  function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
884
897
  const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
@@ -1004,7 +1017,7 @@ function fixedEditorContextScore(
1004
1017
  }
1005
1018
 
1006
1019
  /** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
1007
- const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿)/;
1020
+ const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗)/;
1008
1021
  /** fixedEditorContextScore max ≈ 100 + 2*(5+4+3+2); stop once we hit a near-perfect match. */
1009
1022
  const TOOL_CLICK_EARLY_EXIT_SCORE = 120;
1010
1023
 
@@ -1498,7 +1511,7 @@ function visibleMouseLine(tui: any, packet: SgrMousePacket): string {
1498
1511
  }
1499
1512
 
1500
1513
  function isCollapsedHintAtColumn(line: string, col: number): boolean {
1501
- const match = /(\([^()\n]* \/ click\))\s*$/.exec(line);
1514
+ const match = /(\([^()\n]* \/ click\)|click to show more)\s*$/.exec(line);
1502
1515
  if (!match?.[1] || col <= 0) return false;
1503
1516
  const start = visibleWidth(line.slice(0, match.index)) + 1;
1504
1517
  return col >= start && col < start + visibleWidth(match[1]);
@@ -1544,10 +1557,19 @@ function setHoveredToolIo(view: ExpandedToolIoView | null, section: ToolIoSectio
1544
1557
  return true;
1545
1558
  }
1546
1559
 
1560
+ function setHoveredToolGroup(group: ToolGroupComponent | null): boolean {
1561
+ if (group === hoveredToolGroup) return false;
1562
+ hoveredToolGroup?.setHintHovered(false);
1563
+ hoveredToolGroup = group;
1564
+ group?.setHintHovered(true);
1565
+ return true;
1566
+ }
1567
+
1547
1568
  function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
1548
1569
  if ((packet.code & 32) === 0 || packet.final !== "M") return;
1549
1570
  const visibleLine = visibleMouseLine(tui, packet);
1550
1571
  let nextToolCallId: string | null = null;
1572
+ let nextGroup: ToolGroupComponent | null = null;
1551
1573
  let nextIoView: ExpandedToolIoView | null = null;
1552
1574
  let nextIoSection: ToolIoSection | null = null;
1553
1575
 
@@ -1557,6 +1579,7 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
1557
1579
  ) {
1558
1580
  const hit = findToolAtScreenRow(tui, packet.row);
1559
1581
  nextToolCallId = hit && !hit.component.expanded ? hit.component.toolCallId : null;
1582
+ nextGroup = hit?.component instanceof ToolGroupComponent ? hit.component : null;
1560
1583
  } else if (visibleLine.includes(SHOW_MORE_LABEL)) {
1561
1584
  const hit = findToolAtScreenRow(tui, packet.row);
1562
1585
  const view = hit && hit.component.expanded ? resolveIoViewFromTool(hit.component) : null;
@@ -1570,11 +1593,13 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
1570
1593
 
1571
1594
  const changed = nextToolCallId !== hoveredToolCallId;
1572
1595
  hoveredToolCallId = nextToolCallId;
1573
- if (setHoveredToolIo(nextIoView, nextIoSection) || changed) tui.requestRender?.();
1596
+ if (setHoveredToolIo(nextIoView, nextIoSection) || setHoveredToolGroup(nextGroup) || changed)
1597
+ tui.requestRender?.();
1574
1598
  }
1575
1599
 
1576
1600
  function isExpandedToolCardHit(hit: ToolRenderHit, width: number): boolean {
1577
1601
  if (!Boolean(hit.component.expanded)) return false;
1602
+ if (hit.component instanceof ToolGroupComponent) return true;
1578
1603
  const box = hit.component.contentBox;
1579
1604
  if (!box || !Array.isArray(hit.component.children) || !hit.component.children.includes(box)) {
1580
1605
  return false;
@@ -1588,11 +1613,11 @@ function isExpandedToolCardHit(hit: ToolRenderHit, width: number): boolean {
1588
1613
 
1589
1614
  function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
1590
1615
  const line = visibleMouseLine(tui, packet);
1591
- const collapsedHint = isCollapsedHintAtColumn(line, packet.col);
1592
1616
  const hasShowMore = line.includes(SHOW_MORE_LABEL);
1593
1617
 
1594
1618
  const hit = findToolAtScreenRow(tui, packet.row);
1595
1619
  if (!hit) return false;
1620
+ const collapsedHint = isCollapsedHintAtColumn(line, packet.col);
1596
1621
  const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
1597
1622
  if (Boolean(hit.component.expanded)) {
1598
1623
  if (hasShowMore && tryOpenToolIoShowMore(tui, packet, hit)) return true;
@@ -1600,6 +1625,7 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
1600
1625
  const previousHeight = renderComponentTree(hit.component, width).length;
1601
1626
  hit.component.setExpanded(false);
1602
1627
  hoveredToolCallId = null;
1628
+ setHoveredToolGroup(null);
1603
1629
  setHoveredToolIo(null, null);
1604
1630
  hit.component.invalidate?.();
1605
1631
  const nextHeight = renderComponentTree(hit.component, width).length;
@@ -1611,6 +1637,7 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
1611
1637
 
1612
1638
  hit.component.setExpanded(true);
1613
1639
  hoveredToolCallId = null;
1640
+ setHoveredToolGroup(null);
1614
1641
  setHoveredToolIo(null, null);
1615
1642
  hit.component.invalidate?.();
1616
1643
  tui.requestRender?.();
@@ -1798,6 +1825,7 @@ function teardownToolMouseInteraction(): void {
1798
1825
  toolMouseInputUnsubscribe?.();
1799
1826
  toolMouseInputUnsubscribe = null;
1800
1827
  hoveredToolCallId = null;
1828
+ setHoveredToolGroup(null);
1801
1829
  setHoveredToolIo(null, null);
1802
1830
  try {
1803
1831
  toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
@@ -1877,16 +1905,26 @@ function scheduleSessionRender(refresh?: () => void): void {
1877
1905
  const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
1878
1906
  const ANSI_FG_RESET = "\x1b[39m";
1879
1907
 
1880
- function refreshCurrentTranscript(compactStyle: CompactStyleHooks, ctx?: any): void {
1908
+ function refreshCurrentTranscript(
1909
+ compactStyle: CompactStyleHooks,
1910
+ ctx?: any,
1911
+ toolGrouping?: ToolGroupingHooks,
1912
+ ): void {
1913
+ toolGrouping?.refresh();
1881
1914
  compactStyle.refresh();
1882
1915
  toolMouseTui?.requestRender?.(true);
1883
1916
  ctx?.ui?.requestRender?.(true);
1884
1917
  }
1885
1918
 
1886
- function applyStyleMode(mode: CompactStyleMode, ctx: any, compactStyle: CompactStyleHooks): void {
1919
+ function applyStyleMode(
1920
+ mode: CompactStyleMode,
1921
+ ctx: any,
1922
+ compactStyle: CompactStyleHooks,
1923
+ toolGrouping?: ToolGroupingHooks,
1924
+ ): void {
1887
1925
  config.mode = mode;
1888
1926
  saveConfig();
1889
- refreshCurrentTranscript(compactStyle, ctx);
1927
+ refreshCurrentTranscript(compactStyle, ctx, toolGrouping);
1890
1928
  ctx.ui.notify(`Claude Code style: ${mode}`, "info");
1891
1929
  }
1892
1930
 
@@ -2011,7 +2049,11 @@ function renderSectionTabBar(
2011
2049
  return truncateToWidth(pieces.join(""), Math.max(0, width));
2012
2050
  }
2013
2051
 
2014
- async function showCcstylePanel(ctx: any, compactStyle: CompactStyleHooks): Promise<void> {
2052
+ async function showCcstylePanel(
2053
+ ctx: any,
2054
+ compactStyle: CompactStyleHooks,
2055
+ toolGrouping?: ToolGroupingHooks,
2056
+ ): Promise<void> {
2015
2057
  if (ctx?.mode !== "tui" || !ctx?.hasUI || typeof ctx.ui?.custom !== "function") {
2016
2058
  ctx.ui?.notify?.("/ccstyle requires TUI mode", "warning");
2017
2059
  return;
@@ -2110,7 +2152,7 @@ async function showCcstylePanel(ctx: any, compactStyle: CompactStyleHooks): Prom
2110
2152
  switch (id) {
2111
2153
  case "mode":
2112
2154
  modeSetting.description = modeSettingDescription(value as CompactStyleMode);
2113
- applyStyleMode(value as CompactStyleMode, ctx, compactStyle);
2155
+ applyStyleMode(value as CompactStyleMode, ctx, compactStyle, toolGrouping);
2114
2156
  return;
2115
2157
  case "fixedEditorFeatures":
2116
2158
  config.fixedEditorFeatures = value === "on";
@@ -2287,21 +2329,64 @@ function singleLine(text: string) {
2287
2329
  };
2288
2330
  }
2289
2331
 
2290
- /** Return the most useful scalar argument without semantically truncating it. */
2291
- function toolCallArgument(args: any): { key: string; value: string } | undefined {
2292
- if (!args) return undefined;
2293
- const keys = Object.keys(args);
2294
- if (keys.length === 0) return undefined;
2295
- const preferred = ["query", "question", "command", "pattern", "name", "path", "url", "message"];
2296
- const key = preferred.find((candidate) => {
2297
- const val = args[candidate];
2298
- return val !== undefined && val !== null && typeof val !== "object";
2299
- });
2300
- const selected = key ?? keys[0];
2301
- const value = args[selected];
2302
- return value !== undefined && value !== null && typeof value !== "object"
2303
- ? { key: selected, value: oneLine(value, Infinity) }
2304
- : undefined;
2332
+ function insetComponent(component: any): any {
2333
+ return {
2334
+ render: (width: number) =>
2335
+ component.render(Math.max(1, width - 1)).map((line: string) => {
2336
+ const nestedMarker = line.replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*)↳/, "$1 ↳");
2337
+ return ` ${nestedMarker}`;
2338
+ }),
2339
+ invalidate: () => component.invalidate?.(),
2340
+ };
2341
+ }
2342
+
2343
+ function humanizeToolLabel(label: string): string {
2344
+ return label
2345
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
2346
+ .replace(/[_-]+/g, " ")
2347
+ .replace(/\b\w/g, (char) => char.toUpperCase());
2348
+ }
2349
+
2350
+ function singleToolCallSummary(
2351
+ toolName: string,
2352
+ label: string,
2353
+ args: any,
2354
+ ): { main: string; detail: string } {
2355
+ const title = label === toolName ? humanizeToolLabel(label) : label;
2356
+ if (!args || typeof args !== "object") return { main: title, detail: "" };
2357
+ if (AGENT_FAMILY_TOOL_NAMES.has(toolName) && args.agent_id) {
2358
+ return { main: `${title} ${oneLine(args.agent_id, Infinity)}`, detail: "" };
2359
+ }
2360
+ if (toolName === "read") {
2361
+ const details = [
2362
+ args.offset !== undefined ? `offset=${args.offset}` : "",
2363
+ args.limit !== undefined ? `limit=${args.limit}` : "",
2364
+ ].filter(Boolean);
2365
+ return {
2366
+ main: `${title}${args.path ? ` ${oneLine(args.path, Infinity)}` : ""}`,
2367
+ detail: details.length ? ` (${details.join(", ")})` : "",
2368
+ };
2369
+ }
2370
+ const value =
2371
+ args.path ??
2372
+ args.file_path ??
2373
+ args.command ??
2374
+ args.query ??
2375
+ args.question ??
2376
+ args.pattern ??
2377
+ args.url ??
2378
+ args.name ??
2379
+ args.tool_use_id ??
2380
+ args.toolCallId ??
2381
+ args.id ??
2382
+ args.message;
2383
+ return {
2384
+ main:
2385
+ value !== undefined && value !== null && typeof value !== "object"
2386
+ ? `${title} ${oneLine(value, Infinity)}`
2387
+ : title,
2388
+ detail: "",
2389
+ };
2305
2390
  }
2306
2391
 
2307
2392
  export function middleTruncateToWidth(text: string, width: number): string {
@@ -2337,7 +2422,7 @@ function createCcstyleTool(
2337
2422
  ): any {
2338
2423
  const toolName = originalTool.name;
2339
2424
  const label = isMcpToolDefinition(originalTool, toolName)
2340
- ? toolName
2425
+ ? humanizeMcpToolName(toolName)
2341
2426
  : originalTool.label || toolName;
2342
2427
 
2343
2428
  return {
@@ -2358,20 +2443,18 @@ function createCcstyleTool(
2358
2443
  visualState === "success"
2359
2444
  ? `${BRIGHT_GREEN}${rawIcon}${ANSI_FG_RESET}`
2360
2445
  : theme.fg(toolIconColor(context), rawIcon);
2361
-
2362
- const argument = toolCallArgument(args);
2446
+ const summary = singleToolCallSummary(toolName, label, args);
2363
2447
  let cachedWidth: number | undefined;
2364
2448
  let cachedLine: string | undefined;
2365
2449
  return {
2366
2450
  render(width: number) {
2367
2451
  if (cachedLine !== undefined && cachedWidth === width) return [cachedLine];
2368
- const callWidth = Math.max(0, toolViewportWidth(width) - visibleWidth(icon) - 1);
2369
- const argumentWidth = Math.max(1, callWidth - visibleWidth(`${label}()`));
2370
- const shown = argument ? middleTruncateToWidth(argument.value, argumentWidth) : undefined;
2371
- const call = shown === undefined ? label : `${label}(${shown})`;
2452
+ const viewportWidth = toolViewportWidth(width);
2453
+ const callWidth = Math.max(0, viewportWidth - visibleWidth(icon) - 2);
2454
+ const mainWidth = Math.max(0, callWidth - visibleWidth(summary.detail));
2372
2455
  cachedWidth = width;
2373
- cachedLine = `${icon} ${theme.fg("toolTitle", truncateToWidth(call, callWidth, ""))}`;
2374
- return [cachedLine];
2456
+ cachedLine = ` ${icon} ${theme.fg("toolTitle", middleTruncateToWidth(summary.main, mainWidth))}${theme.fg("muted", summary.detail)}`;
2457
+ return [truncateToWidth(cachedLine, viewportWidth, "")];
2375
2458
  },
2376
2459
  invalidate() {},
2377
2460
  };
@@ -2387,7 +2470,7 @@ function createCcstyleTool(
2387
2470
  }
2388
2471
 
2389
2472
  if (options?.isPartial) {
2390
- return new Text(theme.fg("muted", " ⎿ Pending…"), 0, 0);
2473
+ return new Text(theme.fg("muted", " ⎿ Pending…"), 0, 0);
2391
2474
  }
2392
2475
 
2393
2476
  const isError = options?.isError || context?.isError;
@@ -2405,17 +2488,24 @@ function createCcstyleTool(
2405
2488
  writeExecutionMetadata,
2406
2489
  getToolDisplayConfig,
2407
2490
  );
2408
- if (richResult) return richResult;
2491
+ if (richResult) return insetComponent(richResult);
2409
2492
  }
2410
2493
 
2411
- const text = textFromResult(result);
2494
+ const text = textFromResult(result, expanded);
2412
2495
  const args = context?.args;
2413
- const rendered = !expanded ? (text ? oneLine(text) : "Done") : text;
2414
-
2415
- const detailLineCount = outputLineCount(result) || countLines(formatToolInputArgs(args));
2416
- const hint =
2417
- !expanded && hasExpandableDetail(text, args) ? expandHint(theme, detailLineCount) : "";
2418
- const hoveredHint = hint ? expandHint(theme, detailLineCount, true) : "";
2496
+ const outputLines = outputLineCount(result) || countLines(text);
2497
+ const lineWord = outputLines === 1 ? "line" : "lines";
2498
+ const action = toolName === "read" ? "loaded" : "returned";
2499
+ const rendered = isError
2500
+ ? text
2501
+ ? oneLine(text)
2502
+ : "Failed"
2503
+ : outputLines
2504
+ ? `${outputLines} ${lineWord} ${action}`
2505
+ : "Done";
2506
+ const expandable = !expanded && hasExpandableDetail(text, args);
2507
+ const hint = expandable ? theme.fg("muted", " • click to show more") : "";
2508
+ const hoveredHint = expandable ? theme.fg("text", " • click to show more") : "";
2419
2509
  if (expanded) {
2420
2510
  return renderExpandedToolResult(
2421
2511
  text || "",
@@ -2461,7 +2551,13 @@ function createCcstyleTool(
2461
2551
  const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch");
2462
2552
  const COMPONENT_TOOL_RENDER_MODE = Symbol.for("pi.ccstyle.component-tool-render-mode");
2463
2553
  const COMPONENT_TOOL_SELF_SHELL_MODE = Symbol.for("pi.ccstyle.component-tool-self-shell-mode");
2464
- const SUBAGENT_TOOL_NAMES = new Set(["Agent"]);
2554
+ const AGENT_FAMILY_TOOL_NAMES = new Set([
2555
+ "Agent",
2556
+ "Agents",
2557
+ "get_subagent_result",
2558
+ "steer_subagent",
2559
+ ]);
2560
+ const DEDICATED_SUBAGENT_TOOL_NAMES = new Set(["Agent"]);
2465
2561
 
2466
2562
  type ToolRenderMethods = {
2467
2563
  hasRendererDefinition: (...args: any[]) => boolean;
@@ -2489,9 +2585,23 @@ type GlobalToolRenderPatch = {
2489
2585
  originalGetResultRenderer: ToolRenderMethods["getResultRenderer"];
2490
2586
  };
2491
2587
 
2492
- function isMcpToolDefinition(definition: any, toolName: string): boolean {
2493
- const label = typeof definition?.label === "string" ? definition.label : "";
2494
- return toolName === "mcp" || label === "MCP" || label.startsWith("MCP: ");
2588
+ export function isMcpToolDefinition(definition: any, toolName: string): boolean {
2589
+ const label = typeof definition?.label === "string" ? definition.label.trim() : "";
2590
+ if (/^MCP(?::|$)/i.test(label)) return true;
2591
+ if (toolName === "mcp" || /^mcp[_:-]|[_:-]mcp[_:-]/i.test(toolName)) return true;
2592
+ if (label) return false;
2593
+ const description = typeof definition?.description === "string" ? definition.description : "";
2594
+ return /\bModel Context Protocol\b/i.test(description);
2595
+ }
2596
+
2597
+ export function humanizeMcpToolName(toolName: string): string {
2598
+ const words = toolName
2599
+ .replace(/^mcp(?:[_:-]+)+/i, "")
2600
+ .split(/[_:-]+/)
2601
+ .filter(Boolean);
2602
+ return words.length
2603
+ ? words.map((word) => word[0]!.toUpperCase() + word.slice(1)).join(" ")
2604
+ : "MCP";
2495
2605
  }
2496
2606
 
2497
2607
  /** Return true when this tool must keep its original renderer. */
@@ -2501,7 +2611,7 @@ export function preservesOriginalRenderer(
2501
2611
  builtInToolDefinition?: any,
2502
2612
  excludeRenderers: readonly string[] = config.excludeRenderers,
2503
2613
  ): boolean {
2504
- if (SUBAGENT_TOOL_NAMES.has(toolName)) return true;
2614
+ if (DEDICATED_SUBAGENT_TOOL_NAMES.has(toolName)) return true;
2505
2615
  if (!excludeRenderers.includes(toolName)) return false;
2506
2616
  return [extensionDefinition, builtInToolDefinition].some(
2507
2617
  (definition) =>
@@ -2547,7 +2657,7 @@ function shouldUseSelfShell(component: any, patch: GlobalToolRenderPatch): boole
2547
2657
  const toolName = String(component.toolName || definition?.name || "");
2548
2658
  const useSelfShell =
2549
2659
  patch.enabled() &&
2550
- SUBAGENT_TOOL_NAMES.has(toolName) &&
2660
+ DEDICATED_SUBAGENT_TOOL_NAMES.has(toolName) &&
2551
2661
  definition != null &&
2552
2662
  definition.renderShell === undefined;
2553
2663
  component[COMPONENT_TOOL_SELF_SHELL_MODE] = useSelfShell;
@@ -2785,6 +2895,7 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
2785
2895
  if (configOverride) config = normalizeConfig({ ...config, ...configOverride });
2786
2896
  const writeExecutionMetadata = installWriteOverride(pi, new WriteExecutionMetadataStore());
2787
2897
  const globalToolRendering = installGlobalToolRendering(writeExecutionMetadata);
2898
+ const toolGrouping = installToolGrouping(() => config.mode === "on");
2788
2899
  deactivateLegacyCompactionRendering();
2789
2900
  const compactStyle: CompactStyleHooks = installCompactStyle(pi, {
2790
2901
  getMode: () => config.mode,
@@ -2806,11 +2917,11 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
2806
2917
  handler: async (args, ctx) => {
2807
2918
  const arg = args.trim().toLowerCase();
2808
2919
  if (!arg || arg === "panel") {
2809
- await showCcstylePanel(ctx, compactStyle);
2920
+ await showCcstylePanel(ctx, compactStyle, toolGrouping);
2810
2921
  return;
2811
2922
  }
2812
2923
  if (arg === "on" || arg === "off" || arg === "compact") {
2813
- applyStyleMode(arg, ctx, compactStyle);
2924
+ applyStyleMode(arg, ctx, compactStyle, toolGrouping);
2814
2925
  return;
2815
2926
  }
2816
2927
  if (arg === "status") {
@@ -2822,6 +2933,7 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
2822
2933
  });
2823
2934
 
2824
2935
  pi.on("session_start", async (event, ctx) => {
2936
+ toolGrouping.setTheme(ctx.ui.theme);
2825
2937
  compactStyle?.onSessionStart(event, ctx);
2826
2938
  pendingScrollMessages = 0;
2827
2939
  assistantMessageActive = false;
@@ -2831,6 +2943,7 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
2831
2943
  });
2832
2944
 
2833
2945
  pi.on("session_compact", async (event, ctx) => {
2946
+ toolGrouping.setTheme(ctx.ui.theme);
2834
2947
  compactStyle?.onSessionCompact(event, ctx);
2835
2948
  // Compaction rebuilds the transcript without session_start. Rebind after
2836
2949
  // other TUI extensions may have replaced the root input dispatcher.
@@ -2864,6 +2977,7 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
2864
2977
  });
2865
2978
 
2866
2979
  pi.on("tool_execution_start", async (event, ctx) => {
2980
+ toolGrouping.setTheme(ctx.ui.theme);
2867
2981
  compactStyle?.onToolExecutionStart(event, ctx);
2868
2982
  });
2869
2983
 
@@ -2880,6 +2994,7 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
2880
2994
  writeExecutionMetadata.clear();
2881
2995
  teardownToolMouseInteraction();
2882
2996
  deactivateGlobalToolRendering(globalToolRendering);
2997
+ toolGrouping.shutdown();
2883
2998
  deactivateLegacyCompactionRendering();
2884
2999
  clearAllAnimations();
2885
3000
  });