pi-cc-extensions 0.8.22 → 0.8.23

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.
@@ -1,9 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import {
3
- getSettingsListTheme,
4
- keyHint,
5
- ToolExecutionComponent,
6
- } from "@earendil-works/pi-coding-agent";
2
+ import { getSettingsListTheme, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
7
3
  import {
8
4
  installCompactStyle,
9
5
  type CompactStyleHooks,
@@ -235,10 +231,19 @@ function oneLine(value: unknown, max = 72): string {
235
231
  return text.length > max ? `${text.slice(0, max - 1)}…` : text;
236
232
  }
237
233
 
238
- function textFromResult(result: any): string {
234
+ function rawTextFromResult(result: any): string {
239
235
  const item = result?.content?.find?.((c: any) => c.type === "text");
240
- // Compact previews only need line counts / short text; bound sanitize work.
241
- return item?.type === "text" ? sanitizeToolResultText(item.text ?? "", 16_384) : "";
236
+ return item?.type === "text" ? String(item.text ?? "") : "";
237
+ }
238
+
239
+ function textFromResult(result: any): string {
240
+ // Compact previews only need short text; bound sanitize work.
241
+ return sanitizeToolResultText(rawTextFromResult(result), 16_384);
242
+ }
243
+
244
+ export function outputLineCount(result: any): number {
245
+ const text = rawTextFromResult(result).replace(/\r\n?/g, "\n").replace(/\n+$/, "");
246
+ return text ? text.split("\n").length : 0;
242
247
  }
243
248
 
244
249
  function countLines(text: string): number {
@@ -415,6 +420,10 @@ export const SHOW_MORE_LABEL = "[show more]";
415
420
 
416
421
  export type ToolIoSection = "input" | "output";
417
422
 
423
+ // Module-local token: /reload creates a fresh token, so stale view instances are
424
+ // replaced instead of retaining their old render implementation.
425
+ const EXPANDED_TOOL_IO_VIEW_GENERATION = Symbol("ccstyle-expanded-tool-io-view");
426
+
418
427
  /**
419
428
  * Expanded tool body with clear Input / Output sections (Grok Build–style).
420
429
  *
@@ -428,6 +437,7 @@ export type ToolIoSection = "input" | "output";
428
437
  * Reused across re-renders via context.lastComponent when possible.
429
438
  */
430
439
  export class ExpandedToolIoView {
440
+ readonly [EXPANDED_TOOL_IO_VIEW_GENERATION] = true;
431
441
  private inputBody: string;
432
442
  private outputBody: string;
433
443
  private isError: boolean;
@@ -436,6 +446,7 @@ export class ExpandedToolIoView {
436
446
  private maxInputLines: number;
437
447
  private cachedWidth: number | undefined;
438
448
  private cachedLines: string[] | undefined;
449
+ private hoveredSection: ToolIoSection | null = null;
439
450
  /** Which sections currently show the [show more] affordance (after last render). */
440
451
  private truncated: { input: boolean; output: boolean } = { input: false, output: false };
441
452
 
@@ -490,6 +501,12 @@ export class ExpandedToolIoView {
490
501
  return this.outputBody.trim() ? this.outputBody : "Done";
491
502
  }
492
503
 
504
+ setHoveredSection(section: ToolIoSection | null): void {
505
+ if (this.hoveredSection === section) return;
506
+ this.hoveredSection = section;
507
+ this.invalidate();
508
+ }
509
+
493
510
  /** True when the plain header line is a truncated section with [show more]. */
494
511
  matchShowMoreLine(plainLine: string): ToolIoSection | null {
495
512
  const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
@@ -517,18 +534,26 @@ export class ExpandedToolIoView {
517
534
  const safeWidth = Math.max(1, Math.floor(width));
518
535
  const rail = " │ ";
519
536
  const railWidth = visibleWidth(rail);
520
- const contentWidth = Math.max(1, safeWidth - railWidth);
537
+ const bodyWidth = Math.max(1, Math.floor(safeWidth * 0.8));
538
+ const contentWidth = Math.max(1, bodyWidth - railWidth);
521
539
  const bodyColor = this.isError ? "error" : "toolOutput";
522
540
  const lines: string[] = [];
523
541
  this.truncated = { input: false, output: false };
524
542
 
525
- const pushHeader = (corner: "┌" | "└", label: string, showMore: boolean) => {
543
+ const pushHeader = (
544
+ corner: "┌" | "└",
545
+ label: string,
546
+ section: ToolIoSection,
547
+ showMore: boolean,
548
+ ) => {
526
549
  const mark = theme.fg("dim", ` ${corner} `);
527
550
  const title = theme.fg(
528
551
  "accent",
529
552
  typeof theme.bold === "function" ? theme.bold(label) : label,
530
553
  );
531
- const more = showMore ? theme.fg("dim", ` ${SHOW_MORE_LABEL}`) : "";
554
+ const more = showMore
555
+ ? theme.fg(this.hoveredSection === section ? "text" : "dim", ` ${SHOW_MORE_LABEL}`)
556
+ : "";
532
557
  lines.push(truncateToWidth(mark + title + more, safeWidth, ""));
533
558
  };
534
559
 
@@ -597,15 +622,15 @@ export class ExpandedToolIoView {
597
622
 
598
623
  if (hasInput) {
599
624
  this.truncated.input = inputWouldTruncate;
600
- pushHeader("┌", "Input", inputWouldTruncate);
625
+ pushHeader("┌", "Input", "input", inputWouldTruncate);
601
626
  pushBody(this.inputBody, { input: true, limit: this.maxInputLines });
602
627
  pushBlankRail();
603
628
  this.truncated.output = outputWouldTruncate;
604
- pushHeader("└", "Output", outputWouldTruncate);
629
+ pushHeader("└", "Output", "output", outputWouldTruncate);
605
630
  pushBody(outputText, { limit: this.maxOutputLines });
606
631
  } else {
607
632
  this.truncated.output = outputWouldTruncate;
608
- pushHeader("┌", "Output", outputWouldTruncate);
633
+ pushHeader("┌", "Output", "output", outputWouldTruncate);
609
634
  pushBody(outputText, { limit: this.maxOutputLines });
610
635
  }
611
636
 
@@ -620,6 +645,18 @@ export class ExpandedToolIoView {
620
645
  }
621
646
  }
622
647
 
648
+ function isExpandedToolIoView(value: unknown): value is ExpandedToolIoView {
649
+ return Boolean(
650
+ value &&
651
+ typeof value === "object" &&
652
+ (value as ExpandedToolIoView)[EXPANDED_TOOL_IO_VIEW_GENERATION] === true &&
653
+ typeof (value as ExpandedToolIoView).getInputBody === "function" &&
654
+ typeof (value as ExpandedToolIoView).getOutputBody === "function" &&
655
+ typeof (value as ExpandedToolIoView).setHoveredSection === "function" &&
656
+ typeof (value as ExpandedToolIoView).render === "function",
657
+ );
658
+ }
659
+
623
660
  /** True when body needs truncation at the given line limit (source lines or wrapped rows). */
624
661
  function bodyExceedsLineLimit(
625
662
  body: string,
@@ -655,6 +692,20 @@ export function renderCollapsedToolResult(body: string, collapsedHint = ""): str
655
692
  return ` ↳ ${body}${collapsedHint}`;
656
693
  }
657
694
 
695
+ export function renderCollapsedToolResultToWidth(
696
+ body: string,
697
+ collapsedHint: string,
698
+ width: number,
699
+ ): string {
700
+ const prefix = " ↳ ";
701
+ const bodyWidth = Math.max(1, width - visibleWidth(prefix) - visibleWidth(collapsedHint));
702
+ return truncateToWidth(
703
+ prefix + middleTruncateToWidth(body, bodyWidth) + collapsedHint,
704
+ width,
705
+ "",
706
+ );
707
+ }
708
+
658
709
  /** Pretty-print tool call args for the expanded Input section. */
659
710
  export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
660
711
  if (args === undefined || args === null) return "";
@@ -752,7 +803,7 @@ function renderExpandedToolResult(
752
803
  // Prefer structured Input/Output when we have args or non-empty output.
753
804
  if (inputBody.trim() || outputBody.trim()) {
754
805
  let view: ExpandedToolIoView;
755
- if (lastComponent instanceof ExpandedToolIoView) {
806
+ if (isExpandedToolIoView(lastComponent)) {
756
807
  lastComponent.setContent(inputBody, outputBody, isError, maxLines, maxLines);
757
808
  view = lastComponent;
758
809
  } else {
@@ -767,10 +818,14 @@ function renderExpandedToolResult(
767
818
  return new Text(theme.fg(color, renderCollapsedToolResult("Done")), 0, 0);
768
819
  }
769
820
 
770
- function expandHint(theme: any): string {
821
+ export function formatExpandHint(lineCount: number): string {
822
+ return ` (${lineCount} more line${lineCount === 1 ? "" : "s"} / click)`;
823
+ }
824
+
825
+ function expandHint(theme: any, lineCount: number, hovered = false): string {
771
826
  // Keep interaction guidance neutral; it should not inherit success/error
772
827
  // coloring from the tool result surrounding it.
773
- return `${theme.fg("muted", " (")}${keyHint("app.tools.expand", "expand")}${theme.fg("muted", " / click)")}`;
828
+ return theme.fg(hovered ? "text" : "muted", formatExpandHint(lineCount));
774
829
  }
775
830
 
776
831
  type SgrMousePacket = {
@@ -784,10 +839,12 @@ type ToolRenderHit = {
784
839
  component: any;
785
840
  start: number;
786
841
  end: number;
842
+ row: number;
787
843
  };
788
844
 
789
845
  const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
790
846
  const TOOL_MOUSE_ENABLE = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
847
+ const TOOL_MOUSE_MOTION_ENABLE = "\x1b[?1003h\x1b[?1006h";
791
848
  const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
792
849
  const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
793
850
  const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
@@ -804,6 +861,10 @@ let toolMouseInputUnsubscribe: (() => void) | null = null;
804
861
  let toolMouseInputPatchTui: any = null;
805
862
  let toolMouseInputPatchOriginalHandle: ((...args: any[]) => any) | null = null;
806
863
  let toolMouseInputPatchWrapper: ((...args: any[]) => any) | null = null;
864
+ let toolMouseRenderPatchTui: any = null;
865
+ let toolMouseRenderPatchOriginal: ((...args: any[]) => any) | null = null;
866
+ let toolMouseRenderPatchWrapper: ((...args: any[]) => any) | null = null;
867
+ let toolMouseRawWrite: ((data: string) => unknown) | null = null;
807
868
  let scrollButtonVisible = false;
808
869
  let scrollButtonWidget: any = null;
809
870
  let pendingScrollMessages = 0;
@@ -836,12 +897,14 @@ function isSgrLeftPress(packet: SgrMousePacket): boolean {
836
897
  return packet.final === "M" && baseButton === 0 && (packet.code & 32) === 0;
837
898
  }
838
899
 
839
- function stripTerminalSequences(value: string): string {
900
+ function stripTerminalSequencesPreservingLayout(value: string): string {
840
901
  return value
841
902
  .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
842
- .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
843
- .replace(/\s+/g, " ")
844
- .trim();
903
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
904
+ }
905
+
906
+ function stripTerminalSequences(value: string): string {
907
+ return stripTerminalSequencesPreservingLayout(value).replace(/\s+/g, " ").trim();
845
908
  }
846
909
 
847
910
  function isToolExecutionComponent(value: any): boolean {
@@ -883,7 +946,7 @@ function collectToolRenderHits(
883
946
 
884
947
  const count = renderedLineCount(component, width, cache);
885
948
  if (isToolExecutionComponent(component)) {
886
- if (count > 0) hits.push({ component, start, end: start + count });
949
+ if (count > 0) hits.push({ component, start, end: start + count, row: 0 });
887
950
  return start + count;
888
951
  }
889
952
 
@@ -946,8 +1009,10 @@ function findToolAtFixedEditorRow(
946
1009
  ): ToolRenderHit | null {
947
1010
  if (visibleRow < 0 || visibleRow >= previousLines.length) return null;
948
1011
  const visibleLines = previousLines.map((line) => stripTerminalSequences(String(line)));
1012
+ const rawClickedLine = String(previousLines[visibleRow] ?? "");
949
1013
  const clickedLine = visibleLines[visibleRow] ?? "";
950
- if (!clickedLine) return null;
1014
+ const hasBackground = /\x1b\[(?:4[0-8]|10[0-7])(?:;[0-9;:]*)?m/.test(rawClickedLine);
1015
+ if (!clickedLine && !hasBackground) return null;
951
1016
 
952
1017
  const tools: any[] = [];
953
1018
  collectToolComponents(tui, tools);
@@ -964,7 +1029,12 @@ function findToolAtFixedEditorRow(
964
1029
  const score = fixedEditorContextScore(renderedLines, renderedRow, visibleLines, visibleRow);
965
1030
  if (!best || score > best.score) {
966
1031
  best = {
967
- hit: { component, start: visibleRow, end: visibleRow + renderedLines.length },
1032
+ hit: {
1033
+ component,
1034
+ start: visibleRow,
1035
+ end: visibleRow + renderedLines.length,
1036
+ row: renderedRow,
1037
+ },
968
1038
  score,
969
1039
  };
970
1040
  if (score >= TOOL_CLICK_EARLY_EXIT_SCORE) return best.hit;
@@ -997,7 +1067,7 @@ function findToolAtScreenRow(tui: any, screenRow: number): ToolRenderHit | null
997
1067
  // The stable summary marker keeps the whole row clickable even when the
998
1068
  // trailing keyboard/click hint was truncated (common for subagent output).
999
1069
  if (!hit.component.expanded && !COLLAPSED_TOOL_SUMMARY.test(clickedLine)) continue;
1000
- return hit;
1070
+ return { ...hit, row: bufferRow - hit.start };
1001
1071
  }
1002
1072
  return null;
1003
1073
  }
@@ -1364,6 +1434,8 @@ function scheduleCollapseViewportCompensation(
1364
1434
 
1365
1435
  /** toolCallId → latest expanded IO view (survives context/state identity quirks). */
1366
1436
  const ioViewsByToolCallId = new Map<string, ExpandedToolIoView>();
1437
+ let hoveredToolIoView: ExpandedToolIoView | null = null;
1438
+ let hoveredToolIoSection: ToolIoSection | null = null;
1367
1439
 
1368
1440
  function rememberIoView(context: any, view: ExpandedToolIoView): void {
1369
1441
  if (!context || typeof context !== "object") return;
@@ -1387,7 +1459,7 @@ function rememberIoView(context: any, view: ExpandedToolIoView): void {
1387
1459
 
1388
1460
  function resolveIoViewFromTool(component: any): ExpandedToolIoView | null {
1389
1461
  const fromState = component?.state?.ccstyleIoView;
1390
- if (fromState instanceof ExpandedToolIoView) return fromState;
1462
+ if (isExpandedToolIoView(fromState)) return fromState;
1391
1463
  const id =
1392
1464
  (typeof component?.toolCallId === "string" && component.toolCallId) ||
1393
1465
  (typeof component?.state?.toolCallId === "string" && component.state.toolCallId) ||
@@ -1398,7 +1470,7 @@ function resolveIoViewFromTool(component: any): ExpandedToolIoView | null {
1398
1470
  }
1399
1471
  // Some hosts keep the last result component on the tool instance.
1400
1472
  const last = component?.lastComponent ?? component?.resultComponent ?? component?.content;
1401
- if (last instanceof ExpandedToolIoView) return last;
1473
+ if (isExpandedToolIoView(last)) return last;
1402
1474
  return null;
1403
1475
  }
1404
1476
 
@@ -1406,6 +1478,25 @@ function resolveIoViewFromTool(component: any): ExpandedToolIoView | null {
1406
1478
  * If the click lands on a truncated section's [show more], open the /context-style
1407
1479
  * full-text preview instead of toggling expand/collapse.
1408
1480
  */
1481
+ function visibleMouseLine(tui: any, packet: SgrMousePacket): string {
1482
+ const previousLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
1483
+ const viewportTop = useFixedEditorFeatures(tui)
1484
+ ? 0
1485
+ : Number.isFinite(tui?.previousViewportTop)
1486
+ ? tui.previousViewportTop
1487
+ : 0;
1488
+ return stripTerminalSequencesPreservingLayout(
1489
+ String(previousLines[viewportTop + packet.row - 1] ?? ""),
1490
+ );
1491
+ }
1492
+
1493
+ function isCollapsedHintAtColumn(line: string, col: number): boolean {
1494
+ const match = /(\([^()\n]* \/ click\))\s*$/.exec(line);
1495
+ if (!match?.[1] || col <= 0) return false;
1496
+ const start = visibleWidth(line.slice(0, match.index)) + 1;
1497
+ return col >= start && col < start + visibleWidth(match[1]);
1498
+ }
1499
+
1409
1500
  function tryOpenToolIoShowMore(tui: any, packet: SgrMousePacket, hit: ToolRenderHit): boolean {
1410
1501
  if (!Boolean(hit.component.expanded)) return false;
1411
1502
  const ioView = resolveIoViewFromTool(hit.component);
@@ -1417,13 +1508,12 @@ function tryOpenToolIoShowMore(tui: any, packet: SgrMousePacket, hit: ToolRender
1417
1508
  : (Number.isFinite(tui?.previousViewportTop) ? tui.previousViewportTop : 0) + packet.row - 1;
1418
1509
  if (visibleRow < 0 || visibleRow >= previousLines.length) return false;
1419
1510
 
1420
- const plain = stripTerminalSequences(String(previousLines[visibleRow] ?? ""));
1511
+ const plain = stripTerminalSequencesPreservingLayout(String(previousLines[visibleRow] ?? ""));
1421
1512
  const section = ioView.matchShowMoreLine(plain);
1422
1513
  if (!section) return false;
1423
1514
 
1424
- // Prefer the [show more] cells; allow a little slack so imprecise clicks still work.
1425
1515
  const box = ioView.showMoreHitbox(plain);
1426
- if (box && packet.col > 0 && packet.col < box.startCol - 4) return false;
1516
+ if (!box || packet.col < box.startCol || packet.col > box.endCol) return false;
1427
1517
 
1428
1518
  const ui = toolMouseUi;
1429
1519
  if (!ui || typeof ui.custom !== "function") {
@@ -1438,31 +1528,85 @@ function tryOpenToolIoShowMore(tui: any, packet: SgrMousePacket, hit: ToolRender
1438
1528
  return true;
1439
1529
  }
1440
1530
 
1531
+ function setHoveredToolIo(view: ExpandedToolIoView | null, section: ToolIoSection | null): boolean {
1532
+ if (view === hoveredToolIoView && section === hoveredToolIoSection) return false;
1533
+ hoveredToolIoView?.setHoveredSection(null);
1534
+ hoveredToolIoView = view;
1535
+ hoveredToolIoSection = section;
1536
+ view?.setHoveredSection(section);
1537
+ return true;
1538
+ }
1539
+
1441
1540
  function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
1442
1541
  if ((packet.code & 32) === 0 || packet.final !== "M") return;
1443
- const hit = findToolAtScreenRow(tui, packet.row);
1444
- const nextToolCallId = hit && !hit.component.expanded ? hit.component.toolCallId : null;
1445
- if (nextToolCallId === hoveredToolCallId) return;
1542
+ const visibleLine = visibleMouseLine(tui, packet);
1543
+ let nextToolCallId: string | null = null;
1544
+ let nextIoView: ExpandedToolIoView | null = null;
1545
+ let nextIoSection: ToolIoSection | null = null;
1546
+
1547
+ if (
1548
+ COLLAPSED_TOOL_SUMMARY.test(visibleLine) &&
1549
+ isCollapsedHintAtColumn(visibleLine, packet.col)
1550
+ ) {
1551
+ const hit = findToolAtScreenRow(tui, packet.row);
1552
+ nextToolCallId = hit && !hit.component.expanded ? hit.component.toolCallId : null;
1553
+ } else if (visibleLine.includes(SHOW_MORE_LABEL)) {
1554
+ const hit = findToolAtScreenRow(tui, packet.row);
1555
+ const view = hit && hit.component.expanded ? resolveIoViewFromTool(hit.component) : null;
1556
+ const section = view?.matchShowMoreLine(visibleLine) ?? null;
1557
+ const box = view?.showMoreHitbox(visibleLine);
1558
+ if (view && section && box && packet.col >= box.startCol && packet.col <= box.endCol) {
1559
+ nextIoView = view;
1560
+ nextIoSection = section;
1561
+ }
1562
+ }
1563
+
1564
+ const changed = nextToolCallId !== hoveredToolCallId;
1446
1565
  hoveredToolCallId = nextToolCallId;
1447
- tui.requestRender?.();
1566
+ if (setHoveredToolIo(nextIoView, nextIoSection) || changed) tui.requestRender?.();
1567
+ }
1568
+
1569
+ function isExpandedToolCardHit(hit: ToolRenderHit, width: number): boolean {
1570
+ if (!Boolean(hit.component.expanded)) return false;
1571
+ const box = hit.component.contentBox;
1572
+ if (!box || !Array.isArray(hit.component.children) || !hit.component.children.includes(box)) {
1573
+ return false;
1574
+ }
1575
+ const boxLines = box.render?.(width);
1576
+ const componentLines = renderComponentTree(hit.component, width);
1577
+ if (!Array.isArray(boxLines) || boxLines.length === 0) return false;
1578
+ const cardStart = componentLines.length - boxLines.length;
1579
+ return hit.row >= cardStart && hit.row < componentLines.length;
1448
1580
  }
1449
1581
 
1450
1582
  function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
1583
+ const line = visibleMouseLine(tui, packet);
1584
+ const collapsedHint = isCollapsedHintAtColumn(line, packet.col);
1585
+ const hasShowMore = line.includes(SHOW_MORE_LABEL);
1586
+
1451
1587
  const hit = findToolAtScreenRow(tui, packet.row);
1452
1588
  if (!hit) return false;
1453
-
1454
- if (tryOpenToolIoShowMore(tui, packet, hit)) return true;
1455
-
1456
- const wasExpanded = Boolean(hit.component.expanded);
1457
1589
  const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
1458
- const previousHeight = wasExpanded ? renderComponentTree(hit.component, width).length : 0;
1459
- hit.component.setExpanded(!wasExpanded);
1460
- hit.component.invalidate?.();
1461
- const nextHeight = wasExpanded ? renderComponentTree(hit.component, width).length : 0;
1462
- tui.requestRender?.();
1463
- if (wasExpanded) {
1590
+ if (Boolean(hit.component.expanded)) {
1591
+ if (hasShowMore && tryOpenToolIoShowMore(tui, packet, hit)) return true;
1592
+ if (!isExpandedToolCardHit(hit, width)) return false;
1593
+ const previousHeight = renderComponentTree(hit.component, width).length;
1594
+ hit.component.setExpanded(false);
1595
+ hoveredToolCallId = null;
1596
+ setHoveredToolIo(null, null);
1597
+ hit.component.invalidate?.();
1598
+ const nextHeight = renderComponentTree(hit.component, width).length;
1599
+ tui.requestRender?.();
1464
1600
  scheduleCollapseViewportCompensation(tui, previousHeight - nextHeight, packet);
1601
+ return true;
1465
1602
  }
1603
+ if (!collapsedHint) return false;
1604
+
1605
+ hit.component.setExpanded(true);
1606
+ hoveredToolCallId = null;
1607
+ setHoveredToolIo(null, null);
1608
+ hit.component.invalidate?.();
1609
+ tui.requestRender?.();
1466
1610
  return true;
1467
1611
  }
1468
1612
 
@@ -1548,8 +1692,59 @@ function restoreToolMouseInputCapture(): void {
1548
1692
  toolMouseInputPatchWrapper = null;
1549
1693
  }
1550
1694
 
1695
+ function restoreToolMouseRenderPatch(): void {
1696
+ if (
1697
+ toolMouseRenderPatchTui &&
1698
+ toolMouseRenderPatchOriginal &&
1699
+ toolMouseRenderPatchTui.doRender === toolMouseRenderPatchWrapper
1700
+ ) {
1701
+ toolMouseRenderPatchTui.doRender = toolMouseRenderPatchOriginal;
1702
+ }
1703
+ toolMouseRenderPatchTui = null;
1704
+ toolMouseRenderPatchOriginal = null;
1705
+ toolMouseRenderPatchWrapper = null;
1706
+ toolMouseRawWrite = null;
1707
+ }
1708
+
1709
+ function patchToolMouseMotionAfterRender(tui: any): void {
1710
+ if (!useFixedEditorFeatures(tui) || toolMouseRenderPatchTui === tui) return;
1711
+ restoreToolMouseRenderPatch();
1712
+ const original = tui?.doRender;
1713
+ const terminal = tui?.terminal;
1714
+ let prototype = terminal && Object.getPrototypeOf(terminal);
1715
+ let rawWrite: ((...args: any[]) => unknown) | undefined;
1716
+ while (prototype && !rawWrite) {
1717
+ const candidate = Object.getOwnPropertyDescriptor(prototype, "write")?.value;
1718
+ if (typeof candidate === "function") rawWrite = candidate;
1719
+ prototype = Object.getPrototypeOf(prototype);
1720
+ }
1721
+ if (typeof original !== "function" || !rawWrite) return;
1722
+
1723
+ toolMouseRawWrite = (data) => Reflect.apply(rawWrite, terminal, [data]);
1724
+ const wrapper = function (this: any, ...args: any[]) {
1725
+ const result = Reflect.apply(original, this, args);
1726
+ toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
1727
+ return result;
1728
+ };
1729
+ try {
1730
+ tui.doRender = wrapper;
1731
+ } catch {
1732
+ toolMouseRawWrite = null;
1733
+ return;
1734
+ }
1735
+ toolMouseRenderPatchTui = tui;
1736
+ toolMouseRenderPatchOriginal = original;
1737
+ toolMouseRenderPatchWrapper = wrapper;
1738
+ toolMouseRawWrite(TOOL_MOUSE_MOTION_ENABLE);
1739
+ }
1740
+
1551
1741
  function handleToolMouseInput(data: string): { consume: true } | undefined {
1552
1742
  if (!toolMouseTui) return undefined;
1743
+ if (
1744
+ toolMouseInputPatchTui === toolMouseTui &&
1745
+ toolMouseTui.handleInput === toolMouseInputPatchWrapper
1746
+ )
1747
+ return undefined;
1553
1748
  updateScrollButtonFromInput(toolMouseTui, data);
1554
1749
  if (isScrollBottomInput(data)) {
1555
1750
  if (useFixedEditorFeatures(toolMouseTui) && jumpToBottomWithoutSubmit(toolMouseTui)) {
@@ -1596,6 +1791,7 @@ function teardownToolMouseInteraction(): void {
1596
1791
  toolMouseInputUnsubscribe?.();
1597
1792
  toolMouseInputUnsubscribe = null;
1598
1793
  hoveredToolCallId = null;
1794
+ setHoveredToolIo(null, null);
1599
1795
  try {
1600
1796
  toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
1601
1797
  } catch {
@@ -1607,6 +1803,7 @@ function teardownToolMouseInteraction(): void {
1607
1803
  // The UI context may already have been reset during /reload.
1608
1804
  }
1609
1805
  restoreToolMouseInputCapture();
1806
+ restoreToolMouseRenderPatch();
1610
1807
  scrollButtonVisible = false;
1611
1808
  scrollButtonWidget = null;
1612
1809
  pendingScrollMessages = 0;
@@ -1635,7 +1832,7 @@ export function installToolMouseInteraction(
1635
1832
  toolMouseTui = tui;
1636
1833
  if (fixedEditorFeatures) patchToolMouseInputCapture(tui);
1637
1834
  // Motion reporting is required for hover in both native and fixed-editor layouts.
1638
- tui?.terminal?.write?.(fixedEditorFeatures ? TOOL_MOUSE_ENABLE : "\x1b[?1003h\x1b[?1006h");
1835
+ tui?.terminal?.write?.(fixedEditorFeatures ? TOOL_MOUSE_ENABLE : TOOL_MOUSE_MOTION_ENABLE);
1639
1836
  const widget = {
1640
1837
  render: (width: number) => renderScrollButton(width, theme),
1641
1838
  invalidate() {},
@@ -1646,6 +1843,12 @@ export function installToolMouseInteraction(
1646
1843
  toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
1647
1844
  }
1648
1845
 
1846
+ function refreshToolRendererComponents(tui: any): void {
1847
+ const tools: any[] = [];
1848
+ collectToolComponents(tui, tools);
1849
+ for (const tool of tools) tool.invalidate?.();
1850
+ }
1851
+
1649
1852
  function scheduleSessionRender(refresh?: () => void): void {
1650
1853
  const tui = toolMouseTui;
1651
1854
  if (!tui || typeof tui.requestRender !== "function") return;
@@ -1656,6 +1859,8 @@ function scheduleSessionRender(refresh?: () => void): void {
1656
1859
  sessionRenderTimer = setTimeout(() => {
1657
1860
  sessionRenderTimer = null;
1658
1861
  if (toolMouseTui !== tui) return;
1862
+ patchToolMouseMotionAfterRender(tui);
1863
+ refreshToolRendererComponents(tui);
1659
1864
  refresh?.();
1660
1865
  tui.requestRender(true);
1661
1866
  }, 0);
@@ -1663,7 +1868,7 @@ function scheduleSessionRender(refresh?: () => void): void {
1663
1868
 
1664
1869
  // Bright green for success icon (truecolor ANSI escape)
1665
1870
  const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
1666
- const ANSI_RESET = "\x1b[0m";
1871
+ const ANSI_FG_RESET = "\x1b[39m";
1667
1872
 
1668
1873
  function refreshCurrentTranscript(compactStyle: CompactStyleHooks, ctx?: any): void {
1669
1874
  compactStyle.refresh();
@@ -2144,19 +2349,22 @@ function createCcstyleTool(
2144
2349
  const rawIcon = isPending ? pendingIcon(toolName) : settledIcon(toolName, visualState);
2145
2350
  const icon =
2146
2351
  visualState === "success"
2147
- ? `${BRIGHT_GREEN}${rawIcon}${ANSI_RESET}`
2352
+ ? `${BRIGHT_GREEN}${rawIcon}${ANSI_FG_RESET}`
2148
2353
  : theme.fg(toolIconColor(context), rawIcon);
2149
2354
 
2150
2355
  const argument = toolCallArgument(args);
2356
+ let cachedWidth: number | undefined;
2357
+ let cachedLine: string | undefined;
2151
2358
  return {
2152
2359
  render(width: number) {
2153
- const argumentWidth = Math.max(1, width - visibleWidth(`${rawIcon} ${label}()`));
2360
+ if (cachedLine !== undefined && cachedWidth === width) return [cachedLine];
2361
+ const callWidth = Math.max(0, width - visibleWidth(icon) - 1);
2362
+ const argumentWidth = Math.max(1, callWidth - visibleWidth(`${label}()`));
2154
2363
  const shown = argument ? middleTruncateToWidth(argument.value, argumentWidth) : undefined;
2155
2364
  const call = shown === undefined ? label : `${label}(${shown})`;
2156
- // Leave one cell unused so the host shell never performs a second,
2157
- // unthemed truncation after this renderer has applied toolTitle.
2158
- const callWidth = Math.max(0, width - visibleWidth(icon) - 2);
2159
- return [`${icon} ${theme.fg("toolTitle", truncateToWidth(call, callWidth, "…"))}`];
2365
+ cachedWidth = width;
2366
+ cachedLine = `${icon} ${theme.fg("toolTitle", truncateToWidth(call, callWidth, ""))}`;
2367
+ return [cachedLine];
2160
2368
  },
2161
2369
  invalidate() {},
2162
2370
  };
@@ -2195,9 +2403,12 @@ function createCcstyleTool(
2195
2403
 
2196
2404
  const text = textFromResult(result);
2197
2405
  const args = context?.args;
2198
- const rendered = !expanded ? (text ? oneLine(text, 96) : "Done") : text;
2406
+ const rendered = !expanded ? (text ? oneLine(text, Infinity) : "Done") : text;
2199
2407
 
2200
- const hint = !expanded && hasExpandableDetail(text, args) ? expandHint(theme) : "";
2408
+ const detailLineCount = outputLineCount(result) || countLines(formatToolInputArgs(args));
2409
+ const hint =
2410
+ !expanded && hasExpandableDetail(text, args) ? expandHint(theme, detailLineCount) : "";
2411
+ const hoveredHint = hint ? expandHint(theme, detailLineCount, true) : "";
2201
2412
  if (expanded) {
2202
2413
  return renderExpandedToolResult(
2203
2414
  text || "",
@@ -2210,18 +2421,23 @@ function createCcstyleTool(
2210
2421
  }
2211
2422
  if (context?.state) context.state.ccstyleIoView = undefined;
2212
2423
  const toolCallId = context?.toolCallId;
2213
- const summary = theme.fg(
2214
- isError ? "error" : "muted",
2215
- renderCollapsedToolResult(rendered, hint),
2216
- );
2424
+ let cachedWidth: number | undefined;
2425
+ let cachedLine: string | undefined;
2426
+ let cachedHoveredLine: string | undefined;
2217
2427
  return {
2218
2428
  render(width: number) {
2219
- const line = truncateToWidth(summary, width, "…");
2220
- if (!toolCallId || hoveredToolCallId !== toolCallId) return [line];
2221
- const highlighted = line.replace(/\x1b\[0m/g, "\x1b[0;7m");
2222
- return [
2223
- `\x1b[7m${highlighted}${" ".repeat(Math.max(0, width - visibleWidth(line)))}\x1b[0m`,
2224
- ];
2429
+ if (cachedLine === undefined || cachedWidth !== width) {
2430
+ cachedWidth = width;
2431
+ cachedLine = theme.fg(
2432
+ isError ? "error" : "muted",
2433
+ renderCollapsedToolResultToWidth(rendered, hint, width),
2434
+ );
2435
+ cachedHoveredLine = theme.fg(
2436
+ isError ? "error" : "muted",
2437
+ renderCollapsedToolResultToWidth(rendered, hoveredHint, width),
2438
+ );
2439
+ }
2440
+ return [toolCallId && hoveredToolCallId === toolCallId ? cachedHoveredLine! : cachedLine];
2225
2441
  },
2226
2442
  invalidate() {},
2227
2443
  };
@@ -2480,10 +2696,14 @@ function installGlobalToolRendering(
2480
2696
  },
2481
2697
  getRenderShell: function (this: any, ...args: any[]) {
2482
2698
  if (!patch.active) return patch.downstream.getRenderShell.apply(this, args);
2699
+ const useSelfShell = shouldUseSelfShell(this, patch);
2700
+ const useCcstyle = shouldGloballyStyleTool(this, patch);
2483
2701
  const shell =
2484
- shouldUseSelfShell(this, patch) || shouldGloballyStyleTool(this, patch)
2702
+ useSelfShell || (useCcstyle && !this.expanded)
2485
2703
  ? "self"
2486
- : patch.downstream.getRenderShell.apply(this, args);
2704
+ : useCcstyle
2705
+ ? "default"
2706
+ : patch.downstream.getRenderShell.apply(this, args);
2487
2707
  syncToolShell(this, shell);
2488
2708
  return shell;
2489
2709
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.22",
3
+ "version": "0.8.23",
4
4
  "description": "A Pi productivity suite with Claude Code-style UI, structured questionnaires, context inspection, and agent/session references",
5
5
  "type": "module",
6
6
  "keywords": [