pi-cc-extensions 0.8.21 → 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 +302 -105
- package/extensions/context.ts +3 -24
- package/package.json +1 -1
- package/extensions/closable-dialog.ts +0 -57
|
@@ -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,
|
|
@@ -29,12 +25,6 @@ import {
|
|
|
29
25
|
wrapTextWithAnsi,
|
|
30
26
|
} from "@earendil-works/pi-tui";
|
|
31
27
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
32
|
-
import {
|
|
33
|
-
bottomDialogBounds,
|
|
34
|
-
isDialogCloseClick,
|
|
35
|
-
renderDialogHeader,
|
|
36
|
-
renderDialogTopBorder,
|
|
37
|
-
} from "./closable-dialog.ts";
|
|
38
28
|
import { homedir } from "node:os";
|
|
39
29
|
import { join } from "node:path";
|
|
40
30
|
|
|
@@ -241,10 +231,19 @@ function oneLine(value: unknown, max = 72): string {
|
|
|
241
231
|
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
242
232
|
}
|
|
243
233
|
|
|
244
|
-
function
|
|
234
|
+
function rawTextFromResult(result: any): string {
|
|
245
235
|
const item = result?.content?.find?.((c: any) => c.type === "text");
|
|
246
|
-
|
|
247
|
-
|
|
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;
|
|
248
247
|
}
|
|
249
248
|
|
|
250
249
|
function countLines(text: string): number {
|
|
@@ -421,6 +420,10 @@ export const SHOW_MORE_LABEL = "[show more]";
|
|
|
421
420
|
|
|
422
421
|
export type ToolIoSection = "input" | "output";
|
|
423
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
|
+
|
|
424
427
|
/**
|
|
425
428
|
* Expanded tool body with clear Input / Output sections (Grok Build–style).
|
|
426
429
|
*
|
|
@@ -434,6 +437,7 @@ export type ToolIoSection = "input" | "output";
|
|
|
434
437
|
* Reused across re-renders via context.lastComponent when possible.
|
|
435
438
|
*/
|
|
436
439
|
export class ExpandedToolIoView {
|
|
440
|
+
readonly [EXPANDED_TOOL_IO_VIEW_GENERATION] = true;
|
|
437
441
|
private inputBody: string;
|
|
438
442
|
private outputBody: string;
|
|
439
443
|
private isError: boolean;
|
|
@@ -442,6 +446,7 @@ export class ExpandedToolIoView {
|
|
|
442
446
|
private maxInputLines: number;
|
|
443
447
|
private cachedWidth: number | undefined;
|
|
444
448
|
private cachedLines: string[] | undefined;
|
|
449
|
+
private hoveredSection: ToolIoSection | null = null;
|
|
445
450
|
/** Which sections currently show the [show more] affordance (after last render). */
|
|
446
451
|
private truncated: { input: boolean; output: boolean } = { input: false, output: false };
|
|
447
452
|
|
|
@@ -496,6 +501,12 @@ export class ExpandedToolIoView {
|
|
|
496
501
|
return this.outputBody.trim() ? this.outputBody : "Done";
|
|
497
502
|
}
|
|
498
503
|
|
|
504
|
+
setHoveredSection(section: ToolIoSection | null): void {
|
|
505
|
+
if (this.hoveredSection === section) return;
|
|
506
|
+
this.hoveredSection = section;
|
|
507
|
+
this.invalidate();
|
|
508
|
+
}
|
|
509
|
+
|
|
499
510
|
/** True when the plain header line is a truncated section with [show more]. */
|
|
500
511
|
matchShowMoreLine(plainLine: string): ToolIoSection | null {
|
|
501
512
|
const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
|
|
@@ -523,18 +534,26 @@ export class ExpandedToolIoView {
|
|
|
523
534
|
const safeWidth = Math.max(1, Math.floor(width));
|
|
524
535
|
const rail = " │ ";
|
|
525
536
|
const railWidth = visibleWidth(rail);
|
|
526
|
-
const
|
|
537
|
+
const bodyWidth = Math.max(1, Math.floor(safeWidth * 0.8));
|
|
538
|
+
const contentWidth = Math.max(1, bodyWidth - railWidth);
|
|
527
539
|
const bodyColor = this.isError ? "error" : "toolOutput";
|
|
528
540
|
const lines: string[] = [];
|
|
529
541
|
this.truncated = { input: false, output: false };
|
|
530
542
|
|
|
531
|
-
const pushHeader = (
|
|
543
|
+
const pushHeader = (
|
|
544
|
+
corner: "┌" | "└",
|
|
545
|
+
label: string,
|
|
546
|
+
section: ToolIoSection,
|
|
547
|
+
showMore: boolean,
|
|
548
|
+
) => {
|
|
532
549
|
const mark = theme.fg("dim", ` ${corner} `);
|
|
533
550
|
const title = theme.fg(
|
|
534
551
|
"accent",
|
|
535
552
|
typeof theme.bold === "function" ? theme.bold(label) : label,
|
|
536
553
|
);
|
|
537
|
-
const more = showMore
|
|
554
|
+
const more = showMore
|
|
555
|
+
? theme.fg(this.hoveredSection === section ? "text" : "dim", ` ${SHOW_MORE_LABEL}`)
|
|
556
|
+
: "";
|
|
538
557
|
lines.push(truncateToWidth(mark + title + more, safeWidth, ""));
|
|
539
558
|
};
|
|
540
559
|
|
|
@@ -603,15 +622,15 @@ export class ExpandedToolIoView {
|
|
|
603
622
|
|
|
604
623
|
if (hasInput) {
|
|
605
624
|
this.truncated.input = inputWouldTruncate;
|
|
606
|
-
pushHeader("┌", "Input", inputWouldTruncate);
|
|
625
|
+
pushHeader("┌", "Input", "input", inputWouldTruncate);
|
|
607
626
|
pushBody(this.inputBody, { input: true, limit: this.maxInputLines });
|
|
608
627
|
pushBlankRail();
|
|
609
628
|
this.truncated.output = outputWouldTruncate;
|
|
610
|
-
pushHeader("└", "Output", outputWouldTruncate);
|
|
629
|
+
pushHeader("└", "Output", "output", outputWouldTruncate);
|
|
611
630
|
pushBody(outputText, { limit: this.maxOutputLines });
|
|
612
631
|
} else {
|
|
613
632
|
this.truncated.output = outputWouldTruncate;
|
|
614
|
-
pushHeader("┌", "Output", outputWouldTruncate);
|
|
633
|
+
pushHeader("┌", "Output", "output", outputWouldTruncate);
|
|
615
634
|
pushBody(outputText, { limit: this.maxOutputLines });
|
|
616
635
|
}
|
|
617
636
|
|
|
@@ -626,6 +645,18 @@ export class ExpandedToolIoView {
|
|
|
626
645
|
}
|
|
627
646
|
}
|
|
628
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
|
+
|
|
629
660
|
/** True when body needs truncation at the given line limit (source lines or wrapped rows). */
|
|
630
661
|
function bodyExceedsLineLimit(
|
|
631
662
|
body: string,
|
|
@@ -661,6 +692,20 @@ export function renderCollapsedToolResult(body: string, collapsedHint = ""): str
|
|
|
661
692
|
return ` ↳ ${body}${collapsedHint}`;
|
|
662
693
|
}
|
|
663
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
|
+
|
|
664
709
|
/** Pretty-print tool call args for the expanded Input section. */
|
|
665
710
|
export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
|
|
666
711
|
if (args === undefined || args === null) return "";
|
|
@@ -758,7 +803,7 @@ function renderExpandedToolResult(
|
|
|
758
803
|
// Prefer structured Input/Output when we have args or non-empty output.
|
|
759
804
|
if (inputBody.trim() || outputBody.trim()) {
|
|
760
805
|
let view: ExpandedToolIoView;
|
|
761
|
-
if (lastComponent
|
|
806
|
+
if (isExpandedToolIoView(lastComponent)) {
|
|
762
807
|
lastComponent.setContent(inputBody, outputBody, isError, maxLines, maxLines);
|
|
763
808
|
view = lastComponent;
|
|
764
809
|
} else {
|
|
@@ -773,10 +818,14 @@ function renderExpandedToolResult(
|
|
|
773
818
|
return new Text(theme.fg(color, renderCollapsedToolResult("Done")), 0, 0);
|
|
774
819
|
}
|
|
775
820
|
|
|
776
|
-
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 {
|
|
777
826
|
// Keep interaction guidance neutral; it should not inherit success/error
|
|
778
827
|
// coloring from the tool result surrounding it.
|
|
779
|
-
return
|
|
828
|
+
return theme.fg(hovered ? "text" : "muted", formatExpandHint(lineCount));
|
|
780
829
|
}
|
|
781
830
|
|
|
782
831
|
type SgrMousePacket = {
|
|
@@ -790,10 +839,12 @@ type ToolRenderHit = {
|
|
|
790
839
|
component: any;
|
|
791
840
|
start: number;
|
|
792
841
|
end: number;
|
|
842
|
+
row: number;
|
|
793
843
|
};
|
|
794
844
|
|
|
795
845
|
const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
|
|
796
846
|
const TOOL_MOUSE_ENABLE = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
|
|
847
|
+
const TOOL_MOUSE_MOTION_ENABLE = "\x1b[?1003h\x1b[?1006h";
|
|
797
848
|
const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
|
|
798
849
|
const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
|
|
799
850
|
const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
|
|
@@ -810,6 +861,10 @@ let toolMouseInputUnsubscribe: (() => void) | null = null;
|
|
|
810
861
|
let toolMouseInputPatchTui: any = null;
|
|
811
862
|
let toolMouseInputPatchOriginalHandle: ((...args: any[]) => any) | null = null;
|
|
812
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;
|
|
813
868
|
let scrollButtonVisible = false;
|
|
814
869
|
let scrollButtonWidget: any = null;
|
|
815
870
|
let pendingScrollMessages = 0;
|
|
@@ -842,12 +897,14 @@ function isSgrLeftPress(packet: SgrMousePacket): boolean {
|
|
|
842
897
|
return packet.final === "M" && baseButton === 0 && (packet.code & 32) === 0;
|
|
843
898
|
}
|
|
844
899
|
|
|
845
|
-
function
|
|
900
|
+
function stripTerminalSequencesPreservingLayout(value: string): string {
|
|
846
901
|
return value
|
|
847
902
|
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
848
|
-
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
849
|
-
|
|
850
|
-
|
|
903
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function stripTerminalSequences(value: string): string {
|
|
907
|
+
return stripTerminalSequencesPreservingLayout(value).replace(/\s+/g, " ").trim();
|
|
851
908
|
}
|
|
852
909
|
|
|
853
910
|
function isToolExecutionComponent(value: any): boolean {
|
|
@@ -889,7 +946,7 @@ function collectToolRenderHits(
|
|
|
889
946
|
|
|
890
947
|
const count = renderedLineCount(component, width, cache);
|
|
891
948
|
if (isToolExecutionComponent(component)) {
|
|
892
|
-
if (count > 0) hits.push({ component, start, end: start + count });
|
|
949
|
+
if (count > 0) hits.push({ component, start, end: start + count, row: 0 });
|
|
893
950
|
return start + count;
|
|
894
951
|
}
|
|
895
952
|
|
|
@@ -952,8 +1009,10 @@ function findToolAtFixedEditorRow(
|
|
|
952
1009
|
): ToolRenderHit | null {
|
|
953
1010
|
if (visibleRow < 0 || visibleRow >= previousLines.length) return null;
|
|
954
1011
|
const visibleLines = previousLines.map((line) => stripTerminalSequences(String(line)));
|
|
1012
|
+
const rawClickedLine = String(previousLines[visibleRow] ?? "");
|
|
955
1013
|
const clickedLine = visibleLines[visibleRow] ?? "";
|
|
956
|
-
|
|
1014
|
+
const hasBackground = /\x1b\[(?:4[0-8]|10[0-7])(?:;[0-9;:]*)?m/.test(rawClickedLine);
|
|
1015
|
+
if (!clickedLine && !hasBackground) return null;
|
|
957
1016
|
|
|
958
1017
|
const tools: any[] = [];
|
|
959
1018
|
collectToolComponents(tui, tools);
|
|
@@ -970,7 +1029,12 @@ function findToolAtFixedEditorRow(
|
|
|
970
1029
|
const score = fixedEditorContextScore(renderedLines, renderedRow, visibleLines, visibleRow);
|
|
971
1030
|
if (!best || score > best.score) {
|
|
972
1031
|
best = {
|
|
973
|
-
hit: {
|
|
1032
|
+
hit: {
|
|
1033
|
+
component,
|
|
1034
|
+
start: visibleRow,
|
|
1035
|
+
end: visibleRow + renderedLines.length,
|
|
1036
|
+
row: renderedRow,
|
|
1037
|
+
},
|
|
974
1038
|
score,
|
|
975
1039
|
};
|
|
976
1040
|
if (score >= TOOL_CLICK_EARLY_EXIT_SCORE) return best.hit;
|
|
@@ -1003,7 +1067,7 @@ function findToolAtScreenRow(tui: any, screenRow: number): ToolRenderHit | null
|
|
|
1003
1067
|
// The stable summary marker keeps the whole row clickable even when the
|
|
1004
1068
|
// trailing keyboard/click hint was truncated (common for subagent output).
|
|
1005
1069
|
if (!hit.component.expanded && !COLLAPSED_TOOL_SUMMARY.test(clickedLine)) continue;
|
|
1006
|
-
return hit;
|
|
1070
|
+
return { ...hit, row: bufferRow - hit.start };
|
|
1007
1071
|
}
|
|
1008
1072
|
return null;
|
|
1009
1073
|
}
|
|
@@ -1370,6 +1434,8 @@ function scheduleCollapseViewportCompensation(
|
|
|
1370
1434
|
|
|
1371
1435
|
/** toolCallId → latest expanded IO view (survives context/state identity quirks). */
|
|
1372
1436
|
const ioViewsByToolCallId = new Map<string, ExpandedToolIoView>();
|
|
1437
|
+
let hoveredToolIoView: ExpandedToolIoView | null = null;
|
|
1438
|
+
let hoveredToolIoSection: ToolIoSection | null = null;
|
|
1373
1439
|
|
|
1374
1440
|
function rememberIoView(context: any, view: ExpandedToolIoView): void {
|
|
1375
1441
|
if (!context || typeof context !== "object") return;
|
|
@@ -1393,7 +1459,7 @@ function rememberIoView(context: any, view: ExpandedToolIoView): void {
|
|
|
1393
1459
|
|
|
1394
1460
|
function resolveIoViewFromTool(component: any): ExpandedToolIoView | null {
|
|
1395
1461
|
const fromState = component?.state?.ccstyleIoView;
|
|
1396
|
-
if (fromState
|
|
1462
|
+
if (isExpandedToolIoView(fromState)) return fromState;
|
|
1397
1463
|
const id =
|
|
1398
1464
|
(typeof component?.toolCallId === "string" && component.toolCallId) ||
|
|
1399
1465
|
(typeof component?.state?.toolCallId === "string" && component.state.toolCallId) ||
|
|
@@ -1404,7 +1470,7 @@ function resolveIoViewFromTool(component: any): ExpandedToolIoView | null {
|
|
|
1404
1470
|
}
|
|
1405
1471
|
// Some hosts keep the last result component on the tool instance.
|
|
1406
1472
|
const last = component?.lastComponent ?? component?.resultComponent ?? component?.content;
|
|
1407
|
-
if (last
|
|
1473
|
+
if (isExpandedToolIoView(last)) return last;
|
|
1408
1474
|
return null;
|
|
1409
1475
|
}
|
|
1410
1476
|
|
|
@@ -1412,6 +1478,25 @@ function resolveIoViewFromTool(component: any): ExpandedToolIoView | null {
|
|
|
1412
1478
|
* If the click lands on a truncated section's [show more], open the /context-style
|
|
1413
1479
|
* full-text preview instead of toggling expand/collapse.
|
|
1414
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
|
+
|
|
1415
1500
|
function tryOpenToolIoShowMore(tui: any, packet: SgrMousePacket, hit: ToolRenderHit): boolean {
|
|
1416
1501
|
if (!Boolean(hit.component.expanded)) return false;
|
|
1417
1502
|
const ioView = resolveIoViewFromTool(hit.component);
|
|
@@ -1423,13 +1508,12 @@ function tryOpenToolIoShowMore(tui: any, packet: SgrMousePacket, hit: ToolRender
|
|
|
1423
1508
|
: (Number.isFinite(tui?.previousViewportTop) ? tui.previousViewportTop : 0) + packet.row - 1;
|
|
1424
1509
|
if (visibleRow < 0 || visibleRow >= previousLines.length) return false;
|
|
1425
1510
|
|
|
1426
|
-
const plain =
|
|
1511
|
+
const plain = stripTerminalSequencesPreservingLayout(String(previousLines[visibleRow] ?? ""));
|
|
1427
1512
|
const section = ioView.matchShowMoreLine(plain);
|
|
1428
1513
|
if (!section) return false;
|
|
1429
1514
|
|
|
1430
|
-
// Prefer the [show more] cells; allow a little slack so imprecise clicks still work.
|
|
1431
1515
|
const box = ioView.showMoreHitbox(plain);
|
|
1432
|
-
if (box
|
|
1516
|
+
if (!box || packet.col < box.startCol || packet.col > box.endCol) return false;
|
|
1433
1517
|
|
|
1434
1518
|
const ui = toolMouseUi;
|
|
1435
1519
|
if (!ui || typeof ui.custom !== "function") {
|
|
@@ -1444,31 +1528,85 @@ function tryOpenToolIoShowMore(tui: any, packet: SgrMousePacket, hit: ToolRender
|
|
|
1444
1528
|
return true;
|
|
1445
1529
|
}
|
|
1446
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
|
+
|
|
1447
1540
|
function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
1448
1541
|
if ((packet.code & 32) === 0 || packet.final !== "M") return;
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
|
|
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;
|
|
1452
1565
|
hoveredToolCallId = nextToolCallId;
|
|
1453
|
-
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;
|
|
1454
1580
|
}
|
|
1455
1581
|
|
|
1456
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
|
+
|
|
1457
1587
|
const hit = findToolAtScreenRow(tui, packet.row);
|
|
1458
1588
|
if (!hit) return false;
|
|
1459
|
-
|
|
1460
|
-
if (tryOpenToolIoShowMore(tui, packet, hit)) return true;
|
|
1461
|
-
|
|
1462
|
-
const wasExpanded = Boolean(hit.component.expanded);
|
|
1463
1589
|
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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?.();
|
|
1470
1600
|
scheduleCollapseViewportCompensation(tui, previousHeight - nextHeight, packet);
|
|
1601
|
+
return true;
|
|
1471
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?.();
|
|
1472
1610
|
return true;
|
|
1473
1611
|
}
|
|
1474
1612
|
|
|
@@ -1554,8 +1692,59 @@ function restoreToolMouseInputCapture(): void {
|
|
|
1554
1692
|
toolMouseInputPatchWrapper = null;
|
|
1555
1693
|
}
|
|
1556
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
|
+
|
|
1557
1741
|
function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
1558
1742
|
if (!toolMouseTui) return undefined;
|
|
1743
|
+
if (
|
|
1744
|
+
toolMouseInputPatchTui === toolMouseTui &&
|
|
1745
|
+
toolMouseTui.handleInput === toolMouseInputPatchWrapper
|
|
1746
|
+
)
|
|
1747
|
+
return undefined;
|
|
1559
1748
|
updateScrollButtonFromInput(toolMouseTui, data);
|
|
1560
1749
|
if (isScrollBottomInput(data)) {
|
|
1561
1750
|
if (useFixedEditorFeatures(toolMouseTui) && jumpToBottomWithoutSubmit(toolMouseTui)) {
|
|
@@ -1602,6 +1791,7 @@ function teardownToolMouseInteraction(): void {
|
|
|
1602
1791
|
toolMouseInputUnsubscribe?.();
|
|
1603
1792
|
toolMouseInputUnsubscribe = null;
|
|
1604
1793
|
hoveredToolCallId = null;
|
|
1794
|
+
setHoveredToolIo(null, null);
|
|
1605
1795
|
try {
|
|
1606
1796
|
toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
1607
1797
|
} catch {
|
|
@@ -1613,6 +1803,7 @@ function teardownToolMouseInteraction(): void {
|
|
|
1613
1803
|
// The UI context may already have been reset during /reload.
|
|
1614
1804
|
}
|
|
1615
1805
|
restoreToolMouseInputCapture();
|
|
1806
|
+
restoreToolMouseRenderPatch();
|
|
1616
1807
|
scrollButtonVisible = false;
|
|
1617
1808
|
scrollButtonWidget = null;
|
|
1618
1809
|
pendingScrollMessages = 0;
|
|
@@ -1641,7 +1832,7 @@ export function installToolMouseInteraction(
|
|
|
1641
1832
|
toolMouseTui = tui;
|
|
1642
1833
|
if (fixedEditorFeatures) patchToolMouseInputCapture(tui);
|
|
1643
1834
|
// Motion reporting is required for hover in both native and fixed-editor layouts.
|
|
1644
|
-
tui?.terminal?.write?.(fixedEditorFeatures ? TOOL_MOUSE_ENABLE :
|
|
1835
|
+
tui?.terminal?.write?.(fixedEditorFeatures ? TOOL_MOUSE_ENABLE : TOOL_MOUSE_MOTION_ENABLE);
|
|
1645
1836
|
const widget = {
|
|
1646
1837
|
render: (width: number) => renderScrollButton(width, theme),
|
|
1647
1838
|
invalidate() {},
|
|
@@ -1652,6 +1843,12 @@ export function installToolMouseInteraction(
|
|
|
1652
1843
|
toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
|
|
1653
1844
|
}
|
|
1654
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
|
+
|
|
1655
1852
|
function scheduleSessionRender(refresh?: () => void): void {
|
|
1656
1853
|
const tui = toolMouseTui;
|
|
1657
1854
|
if (!tui || typeof tui.requestRender !== "function") return;
|
|
@@ -1662,6 +1859,8 @@ function scheduleSessionRender(refresh?: () => void): void {
|
|
|
1662
1859
|
sessionRenderTimer = setTimeout(() => {
|
|
1663
1860
|
sessionRenderTimer = null;
|
|
1664
1861
|
if (toolMouseTui !== tui) return;
|
|
1862
|
+
patchToolMouseMotionAfterRender(tui);
|
|
1863
|
+
refreshToolRendererComponents(tui);
|
|
1665
1864
|
refresh?.();
|
|
1666
1865
|
tui.requestRender(true);
|
|
1667
1866
|
}, 0);
|
|
@@ -1669,7 +1868,7 @@ function scheduleSessionRender(refresh?: () => void): void {
|
|
|
1669
1868
|
|
|
1670
1869
|
// Bright green for success icon (truecolor ANSI escape)
|
|
1671
1870
|
const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
|
|
1672
|
-
const
|
|
1871
|
+
const ANSI_FG_RESET = "\x1b[39m";
|
|
1673
1872
|
|
|
1674
1873
|
function refreshCurrentTranscript(compactStyle: CompactStyleHooks, ctx?: any): void {
|
|
1675
1874
|
compactStyle.refresh();
|
|
@@ -1992,8 +2191,6 @@ async function showCcstylePanel(ctx: any, compactStyle: CompactStyleHooks): Prom
|
|
|
1992
2191
|
];
|
|
1993
2192
|
|
|
1994
2193
|
let activeSection = 0;
|
|
1995
|
-
let lastWidth = 0;
|
|
1996
|
-
let lastHeight = 0;
|
|
1997
2194
|
const settingsTheme = getSettingsListTheme();
|
|
1998
2195
|
const lists = sections.map(
|
|
1999
2196
|
(section) =>
|
|
@@ -2030,15 +2227,8 @@ async function showCcstylePanel(ctx: any, compactStyle: CompactStyleHooks): Prom
|
|
|
2030
2227
|
while (listBody.length > 0 && listBody[listBody.length - 1] === "") listBody.pop();
|
|
2031
2228
|
|
|
2032
2229
|
// Frame: top rule · tabs · mid rule · settings · mid rule · footer · bottom rule
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
renderDialogHeader(
|
|
2036
|
-
" CC Style",
|
|
2037
|
-
safeWidth,
|
|
2038
|
-
(text) => theme.fg("border", text),
|
|
2039
|
-
(text) => theme.fg("toolTitle", text),
|
|
2040
|
-
(text) => theme.fg("muted", text),
|
|
2041
|
-
),
|
|
2230
|
+
return [
|
|
2231
|
+
rule,
|
|
2042
2232
|
renderSectionTabBar(theme, sections, activeSection, safeWidth),
|
|
2043
2233
|
rule,
|
|
2044
2234
|
...listBody,
|
|
@@ -2052,22 +2242,11 @@ async function showCcstylePanel(ctx: any, compactStyle: CompactStyleHooks): Prom
|
|
|
2052
2242
|
),
|
|
2053
2243
|
rule,
|
|
2054
2244
|
];
|
|
2055
|
-
lastWidth = safeWidth;
|
|
2056
|
-
lastHeight = lines.length;
|
|
2057
|
-
return lines;
|
|
2058
2245
|
},
|
|
2059
2246
|
invalidate() {
|
|
2060
2247
|
for (const list of lists) list.invalidate();
|
|
2061
2248
|
},
|
|
2062
2249
|
handleInput(data: string) {
|
|
2063
|
-
if (
|
|
2064
|
-
!excludeSubmenuOpen &&
|
|
2065
|
-
tui.terminal &&
|
|
2066
|
-
isDialogCloseClick(data, bottomDialogBounds(tui.terminal, lastWidth, lastHeight))
|
|
2067
|
-
) {
|
|
2068
|
-
done();
|
|
2069
|
-
return;
|
|
2070
|
-
}
|
|
2071
2250
|
if (!excludeSubmenuOpen && isForwardTabKey(data)) {
|
|
2072
2251
|
switchSection(1);
|
|
2073
2252
|
tui.requestRender();
|
|
@@ -2118,15 +2297,22 @@ function toolCallArgument(args: any): { key: string; value: string } | undefined
|
|
|
2118
2297
|
: undefined;
|
|
2119
2298
|
}
|
|
2120
2299
|
|
|
2121
|
-
function
|
|
2300
|
+
export function middleTruncateToWidth(text: string, width: number): string {
|
|
2122
2301
|
if (visibleWidth(text) <= width) return text;
|
|
2123
2302
|
if (width <= 1) return "…";
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2303
|
+
const chars = Array.from(text);
|
|
2304
|
+
const leftWidth = Math.ceil((width - 1) / 2);
|
|
2305
|
+
let left = "";
|
|
2306
|
+
let right = "";
|
|
2307
|
+
for (const char of chars) {
|
|
2308
|
+
if (visibleWidth(left + char) > leftWidth) break;
|
|
2309
|
+
left += char;
|
|
2128
2310
|
}
|
|
2129
|
-
|
|
2311
|
+
for (const char of chars.reverse()) {
|
|
2312
|
+
if (visibleWidth(left + "…" + char + right) > width) break;
|
|
2313
|
+
right = char + right;
|
|
2314
|
+
}
|
|
2315
|
+
return `${left}…${right}`;
|
|
2130
2316
|
}
|
|
2131
2317
|
|
|
2132
2318
|
export function shouldRenderRichDiff(
|
|
@@ -2163,23 +2349,22 @@ function createCcstyleTool(
|
|
|
2163
2349
|
const rawIcon = isPending ? pendingIcon(toolName) : settledIcon(toolName, visualState);
|
|
2164
2350
|
const icon =
|
|
2165
2351
|
visualState === "success"
|
|
2166
|
-
? `${BRIGHT_GREEN}${rawIcon}${
|
|
2352
|
+
? `${BRIGHT_GREEN}${rawIcon}${ANSI_FG_RESET}`
|
|
2167
2353
|
: theme.fg(toolIconColor(context), rawIcon);
|
|
2168
2354
|
|
|
2169
2355
|
const argument = toolCallArgument(args);
|
|
2356
|
+
let cachedWidth: number | undefined;
|
|
2357
|
+
let cachedLine: string | undefined;
|
|
2170
2358
|
return {
|
|
2171
2359
|
render(width: number) {
|
|
2172
|
-
|
|
2173
|
-
const
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
: truncateToWidth(argument.value, argumentWidth, "…")
|
|
2177
|
-
: undefined;
|
|
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}()`));
|
|
2363
|
+
const shown = argument ? middleTruncateToWidth(argument.value, argumentWidth) : undefined;
|
|
2178
2364
|
const call = shown === undefined ? label : `${label}(${shown})`;
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
return [`${icon} ${theme.fg("toolTitle", truncateToWidth(call, callWidth, "…"))}`];
|
|
2365
|
+
cachedWidth = width;
|
|
2366
|
+
cachedLine = `${icon} ${theme.fg("toolTitle", truncateToWidth(call, callWidth, ""))}`;
|
|
2367
|
+
return [cachedLine];
|
|
2183
2368
|
},
|
|
2184
2369
|
invalidate() {},
|
|
2185
2370
|
};
|
|
@@ -2218,9 +2403,12 @@ function createCcstyleTool(
|
|
|
2218
2403
|
|
|
2219
2404
|
const text = textFromResult(result);
|
|
2220
2405
|
const args = context?.args;
|
|
2221
|
-
const rendered = !expanded ? (text ? oneLine(text,
|
|
2406
|
+
const rendered = !expanded ? (text ? oneLine(text, Infinity) : "Done") : text;
|
|
2222
2407
|
|
|
2223
|
-
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) : "";
|
|
2224
2412
|
if (expanded) {
|
|
2225
2413
|
return renderExpandedToolResult(
|
|
2226
2414
|
text || "",
|
|
@@ -2233,18 +2421,23 @@ function createCcstyleTool(
|
|
|
2233
2421
|
}
|
|
2234
2422
|
if (context?.state) context.state.ccstyleIoView = undefined;
|
|
2235
2423
|
const toolCallId = context?.toolCallId;
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
);
|
|
2424
|
+
let cachedWidth: number | undefined;
|
|
2425
|
+
let cachedLine: string | undefined;
|
|
2426
|
+
let cachedHoveredLine: string | undefined;
|
|
2240
2427
|
return {
|
|
2241
2428
|
render(width: number) {
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
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];
|
|
2248
2441
|
},
|
|
2249
2442
|
invalidate() {},
|
|
2250
2443
|
};
|
|
@@ -2503,10 +2696,14 @@ function installGlobalToolRendering(
|
|
|
2503
2696
|
},
|
|
2504
2697
|
getRenderShell: function (this: any, ...args: any[]) {
|
|
2505
2698
|
if (!patch.active) return patch.downstream.getRenderShell.apply(this, args);
|
|
2699
|
+
const useSelfShell = shouldUseSelfShell(this, patch);
|
|
2700
|
+
const useCcstyle = shouldGloballyStyleTool(this, patch);
|
|
2506
2701
|
const shell =
|
|
2507
|
-
|
|
2702
|
+
useSelfShell || (useCcstyle && !this.expanded)
|
|
2508
2703
|
? "self"
|
|
2509
|
-
:
|
|
2704
|
+
: useCcstyle
|
|
2705
|
+
? "default"
|
|
2706
|
+
: patch.downstream.getRenderShell.apply(this, args);
|
|
2510
2707
|
syncToolShell(this, shell);
|
|
2511
2708
|
return shell;
|
|
2512
2709
|
},
|
package/extensions/context.ts
CHANGED
|
@@ -10,12 +10,6 @@ import {
|
|
|
10
10
|
visibleWidth,
|
|
11
11
|
wrapTextWithAnsi,
|
|
12
12
|
} from "@earendil-works/pi-tui";
|
|
13
|
-
import {
|
|
14
|
-
centeredDialogBounds,
|
|
15
|
-
isDialogCloseClick,
|
|
16
|
-
renderDialogHeader,
|
|
17
|
-
renderDialogTopBorder,
|
|
18
|
-
} from "./closable-dialog.ts";
|
|
19
13
|
|
|
20
14
|
export type ContextPart = {
|
|
21
15
|
label: string;
|
|
@@ -71,8 +65,6 @@ export async function showTextPreview(
|
|
|
71
65
|
let scrollOffset = 0;
|
|
72
66
|
let pageSize = 1;
|
|
73
67
|
let totalLines = 1;
|
|
74
|
-
let lastWidth = 0;
|
|
75
|
-
let lastHeight = 0;
|
|
76
68
|
const wrappedContent = createWrappedTextCache(content);
|
|
77
69
|
|
|
78
70
|
const scrollTo = (nextOffset: number): void => {
|
|
@@ -85,10 +77,6 @@ export async function showTextPreview(
|
|
|
85
77
|
wrappedContent.invalidate();
|
|
86
78
|
},
|
|
87
79
|
handleInput(data: string) {
|
|
88
|
-
if (isDialogCloseClick(data, centeredDialogBounds(tui.terminal, lastWidth, lastHeight))) {
|
|
89
|
-
done(undefined);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
80
|
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
93
81
|
done(undefined);
|
|
94
82
|
return;
|
|
@@ -150,24 +138,15 @@ export async function showTextPreview(
|
|
|
150
138
|
const end = Math.min(totalLines, scrollOffset + pageSize);
|
|
151
139
|
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · Esc close`;
|
|
152
140
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
` ${title}`,
|
|
157
|
-
width,
|
|
158
|
-
border,
|
|
159
|
-
(text) => theme.bold(theme.fg("accent", text)),
|
|
160
|
-
(text) => theme.fg("muted", text),
|
|
161
|
-
),
|
|
141
|
+
return [
|
|
142
|
+
border(`╭${"─".repeat(inner)}╮`),
|
|
143
|
+
`${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`)}${border("│")}`,
|
|
162
144
|
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
163
145
|
...bodyRows,
|
|
164
146
|
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
165
147
|
`${border("│")}${padLine(theme.fg("dim", ` ${status}`))}${border("│")}`,
|
|
166
148
|
border(`╰${"─".repeat(inner)}╯`),
|
|
167
149
|
];
|
|
168
|
-
lastWidth = width;
|
|
169
|
-
lastHeight = lines.length;
|
|
170
|
-
return lines;
|
|
171
150
|
},
|
|
172
151
|
};
|
|
173
152
|
},
|
package/package.json
CHANGED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
|
|
3
|
-
export type DialogBounds = { left: number; top: number; width: number };
|
|
4
|
-
|
|
5
|
-
export function renderDialogTopBorder(width: number, color: (text: string) => string): string {
|
|
6
|
-
const safeWidth = Math.max(4, Math.floor(width));
|
|
7
|
-
return color(`╭${"─".repeat(safeWidth - 2)}╮`);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Render title and a larger [ × ] target on the row below the top border. */
|
|
11
|
-
export function renderDialogHeader(
|
|
12
|
-
title: string,
|
|
13
|
-
width: number,
|
|
14
|
-
color: (text: string) => string,
|
|
15
|
-
titleColor: (text: string) => string,
|
|
16
|
-
closeColor: (text: string) => string,
|
|
17
|
-
): string {
|
|
18
|
-
const safeWidth = Math.max(8, Math.floor(width));
|
|
19
|
-
const innerWidth = safeWidth - 2;
|
|
20
|
-
const button = "[ × ]";
|
|
21
|
-
const titleWidth = Math.max(0, innerWidth - visibleWidth(button) - 2);
|
|
22
|
-
const shownTitle = truncateToWidth(title, titleWidth, "…");
|
|
23
|
-
const gap = " ".repeat(Math.max(1, innerWidth - visibleWidth(shownTitle) - visibleWidth(button)));
|
|
24
|
-
return `${color("│")}${titleColor(shownTitle)}${gap}${closeColor(button)}${color("│")}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Match a left-button press anywhere on the [ × ] target. */
|
|
28
|
-
export function isDialogCloseClick(data: string, bounds: DialogBounds): boolean {
|
|
29
|
-
const match = data.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/);
|
|
30
|
-
if (!match || match[4] !== "M") return false;
|
|
31
|
-
const code = Number(match[1]);
|
|
32
|
-
if ((code & 3) !== 0 || (code & 32) !== 0) return false;
|
|
33
|
-
const col = Number(match[2]);
|
|
34
|
-
const firstButtonCol = bounds.left + bounds.width - 7;
|
|
35
|
-
const lastButtonCol = bounds.left + bounds.width - 3;
|
|
36
|
-
return col >= firstButtonCol && col <= lastButtonCol && Number(match[3]) === bounds.top + 1;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function centeredDialogBounds(
|
|
40
|
-
terminal: { columns: number; rows: number },
|
|
41
|
-
width: number,
|
|
42
|
-
height: number,
|
|
43
|
-
): DialogBounds {
|
|
44
|
-
return {
|
|
45
|
-
left: Math.floor((terminal.columns - width) / 2) + 1,
|
|
46
|
-
top: Math.floor((terminal.rows - height) / 2) + 1,
|
|
47
|
-
width,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function bottomDialogBounds(
|
|
52
|
-
terminal: { rows: number },
|
|
53
|
-
width: number,
|
|
54
|
-
height: number,
|
|
55
|
-
): DialogBounds {
|
|
56
|
-
return { left: 1, top: Math.max(1, terminal.rows - height + 1), width };
|
|
57
|
-
}
|