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.
- package/extensions/claude-code-style.ts +286 -66
- package/package.json +1 -1
|
@@ -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
|
|
234
|
+
function rawTextFromResult(result: any): string {
|
|
239
235
|
const item = result?.content?.find?.((c: any) => c.type === "text");
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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 = (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
900
|
+
function stripTerminalSequencesPreservingLayout(value: string): string {
|
|
840
901
|
return value
|
|
841
902
|
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
842
|
-
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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 :
|
|
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
|
|
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}${
|
|
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
|
-
|
|
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
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
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,
|
|
2406
|
+
const rendered = !expanded ? (text ? oneLine(text, Infinity) : "Done") : text;
|
|
2199
2407
|
|
|
2200
|
-
const
|
|
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
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
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
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
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
|
-
|
|
2702
|
+
useSelfShell || (useCcstyle && !this.expanded)
|
|
2485
2703
|
? "self"
|
|
2486
|
-
:
|
|
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