pi-cc-extensions 0.5.4 → 0.5.6
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 +212 -33
- package/package.json +1 -1
|
@@ -318,6 +318,8 @@ const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
|
|
|
318
318
|
const TOOL_MOUSE_ENABLE = "\x1b[?1000h\x1b[?1006h";
|
|
319
319
|
const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1000l";
|
|
320
320
|
const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
|
|
321
|
+
const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
|
|
322
|
+
const SCROLL_BOTTOM_SHORTCUT = "ctrl+end";
|
|
321
323
|
let toolMouseTui: any = null;
|
|
322
324
|
let toolMouseUi: any = null;
|
|
323
325
|
let toolMouseInputUnsubscribe: (() => void) | null = null;
|
|
@@ -326,6 +328,9 @@ let toolMouseInputPatchOriginalHandle: ((...args: any[]) => any) | null = null;
|
|
|
326
328
|
let toolMouseInputPatchWrapper: ((...args: any[]) => any) | null = null;
|
|
327
329
|
let scrollButtonVisible = false;
|
|
328
330
|
let scrollButtonWidget: any = null;
|
|
331
|
+
let pendingScrollMessages = 0;
|
|
332
|
+
let assistantMessageActive = false;
|
|
333
|
+
let scrollButtonSyncScheduled = false;
|
|
329
334
|
|
|
330
335
|
function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
|
|
331
336
|
const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
@@ -412,49 +417,84 @@ function collectToolRenderHits(
|
|
|
412
417
|
return start + count;
|
|
413
418
|
}
|
|
414
419
|
|
|
420
|
+
function collectToolComponents(component: any, tools: any[], seen = new Set<any>()): void {
|
|
421
|
+
if (!component || typeof component !== "object" || seen.has(component)) return;
|
|
422
|
+
seen.add(component);
|
|
423
|
+
if (isToolExecutionComponent(component)) {
|
|
424
|
+
tools.push(component);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (!Array.isArray(component.children)) return;
|
|
428
|
+
for (const child of component.children) collectToolComponents(child, tools, seen);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function fixedEditorLineMatch(rendered: string, visible: string): boolean {
|
|
432
|
+
return rendered === visible
|
|
433
|
+
|| (visible.length >= 8 && (rendered.includes(visible) || visible.includes(rendered)));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function fixedEditorContextScore(
|
|
437
|
+
renderedLines: string[],
|
|
438
|
+
renderedRow: number,
|
|
439
|
+
visibleLines: string[],
|
|
440
|
+
visibleRow: number,
|
|
441
|
+
): number {
|
|
442
|
+
let score = renderedLines[renderedRow] === visibleLines[visibleRow] ? 100 : 50;
|
|
443
|
+
for (const direction of [-1, 1]) {
|
|
444
|
+
for (let distance = 1; distance <= 4; distance++) {
|
|
445
|
+
const candidate = renderedLines[renderedRow + direction * distance];
|
|
446
|
+
const visible = visibleLines[visibleRow + direction * distance];
|
|
447
|
+
if (candidate === undefined || visible === undefined || candidate !== visible) break;
|
|
448
|
+
score += 5 - distance;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return score;
|
|
452
|
+
}
|
|
453
|
+
|
|
415
454
|
function findToolAtFixedEditorRow(
|
|
416
455
|
tui: any,
|
|
417
|
-
|
|
456
|
+
visibleRow: number,
|
|
418
457
|
previousLines: string[],
|
|
419
458
|
width: number,
|
|
420
459
|
): ToolRenderHit | null {
|
|
421
|
-
if (
|
|
422
|
-
const
|
|
460
|
+
if (visibleRow < 0 || visibleRow >= previousLines.length) return null;
|
|
461
|
+
const visibleLines = previousLines.map((line) => stripTerminalSequences(String(line)));
|
|
462
|
+
const clickedLine = visibleLines[visibleRow] ?? "";
|
|
423
463
|
if (!clickedLine) return null;
|
|
424
464
|
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
.filter((line) => stripTerminalSequences(String(line)) === clickedLine)
|
|
430
|
-
.length;
|
|
431
|
-
let matchingOccurrence = 0;
|
|
432
|
-
|
|
433
|
-
for (const hit of hits) {
|
|
434
|
-
const component = hit.component;
|
|
465
|
+
const tools: any[] = [];
|
|
466
|
+
collectToolComponents(tui, tools);
|
|
467
|
+
let best: { hit: ToolRenderHit; score: number } | null = null;
|
|
468
|
+
for (const component of tools) {
|
|
435
469
|
const expanded = Boolean(component.expanded);
|
|
436
470
|
if (!expanded && !/(?:expand|\/ click)/i.test(clickedLine)) continue;
|
|
437
|
-
const renderedLines = renderComponentTree(component, width)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
471
|
+
const renderedLines = renderComponentTree(component, width)
|
|
472
|
+
.map((line) => stripTerminalSequences(String(line)));
|
|
473
|
+
for (let renderedRow = 0; renderedRow < renderedLines.length; renderedRow++) {
|
|
474
|
+
if (!fixedEditorLineMatch(renderedLines[renderedRow] ?? "", clickedLine)) continue;
|
|
475
|
+
const score = fixedEditorContextScore(renderedLines, renderedRow, visibleLines, visibleRow);
|
|
476
|
+
if (!best || score > best.score) {
|
|
477
|
+
best = {
|
|
478
|
+
hit: { component, start: visibleRow, end: visibleRow + renderedLines.length },
|
|
479
|
+
score,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
446
483
|
}
|
|
447
|
-
return null;
|
|
484
|
+
return best?.hit ?? null;
|
|
448
485
|
}
|
|
449
486
|
|
|
450
487
|
function findToolAtScreenRow(tui: any, screenRow: number): ToolRenderHit | null {
|
|
451
488
|
const previousLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
|
|
452
|
-
const viewportTop = Number.isFinite(tui?.previousViewportTop) ? tui.previousViewportTop : 0;
|
|
453
|
-
const bufferRow = viewportTop + screenRow - 1;
|
|
454
489
|
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
455
490
|
if (isFixedEditorTui(tui)) {
|
|
456
|
-
|
|
491
|
+
// Zentui replaces Pi's root render with the already-sliced visible
|
|
492
|
+
// transcript. previousViewportTop remains cursor bookkeeping and must not
|
|
493
|
+
// be added to a physical mouse row here.
|
|
494
|
+
return findToolAtFixedEditorRow(tui, screenRow - 1, previousLines, width);
|
|
457
495
|
}
|
|
496
|
+
const viewportTop = Number.isFinite(tui?.previousViewportTop) ? tui.previousViewportTop : 0;
|
|
497
|
+
const bufferRow = viewportTop + screenRow - 1;
|
|
458
498
|
if (bufferRow < 0 || bufferRow >= previousLines.length) return null;
|
|
459
499
|
|
|
460
500
|
const hits: ToolRenderHit[] = [];
|
|
@@ -485,6 +525,96 @@ function isFixedEditorTui(tui: any): boolean {
|
|
|
485
525
|
return typeof ownRows?.get === "function" && ownRows.get !== inheritedRows?.get;
|
|
486
526
|
}
|
|
487
527
|
|
|
528
|
+
function formatShortcut(shortcut: string): string {
|
|
529
|
+
return shortcut
|
|
530
|
+
.split("+")
|
|
531
|
+
.map((part) => part.length <= 1 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
|
|
532
|
+
.join("+");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function isScrollBottomInput(data: string): boolean {
|
|
536
|
+
return matchesKey(data, SCROLL_BOTTOM_SHORTCUT);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function isScrollNavigationInput(data: string): boolean {
|
|
540
|
+
if (
|
|
541
|
+
matchesKey(data, "pageUp")
|
|
542
|
+
|| matchesKey(data, "pageDown")
|
|
543
|
+
|| ZENTUI_PAGE_UP_INPUT.test(data)
|
|
544
|
+
|| ZENTUI_PAGE_DOWN_INPUT.test(data)
|
|
545
|
+
) {
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
const packets = parseSgrMousePackets(data);
|
|
549
|
+
return Boolean(packets?.some((packet) => {
|
|
550
|
+
const baseButton = packet.code & ~(4 | 8 | 16 | 32);
|
|
551
|
+
return packet.final === "M" && (baseButton === 64 || baseButton === 65);
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function directRenderLines(component: any, width: number): string[] {
|
|
556
|
+
try {
|
|
557
|
+
const lines = component?.render?.(width);
|
|
558
|
+
return Array.isArray(lines) ? lines : [];
|
|
559
|
+
} catch {
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Render only the transcript portion that fixed-editor leaves scrollable. */
|
|
565
|
+
function renderFixedScrollableRoot(tui: any, width: number): string[] {
|
|
566
|
+
const children = Array.isArray(tui?.children) ? tui.children : [];
|
|
567
|
+
const editorIndex = children.findIndex((child: any) =>
|
|
568
|
+
containsEditorLike(child, tui.focusedComponent),
|
|
569
|
+
);
|
|
570
|
+
const end = editorIndex >= 2 ? editorIndex - 2 : children.length;
|
|
571
|
+
return children.slice(0, end).flatMap((child: any) => directRenderLines(child, width));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function isFixedEditorAtBottom(tui: any): boolean {
|
|
575
|
+
if (!isFixedEditorTui(tui)) return true;
|
|
576
|
+
const visibleLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
|
|
577
|
+
if (visibleLines.length === 0) return true;
|
|
578
|
+
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
579
|
+
const rootLines = renderFixedScrollableRoot(tui, width);
|
|
580
|
+
if (rootLines.length <= visibleLines.length) return true;
|
|
581
|
+
const tail = rootLines.slice(-visibleLines.length);
|
|
582
|
+
return tail.every((line, index) =>
|
|
583
|
+
stripTerminalSequences(String(line)) === stripTerminalSequences(String(visibleLines[index] ?? "")),
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function hideScrollButton(tui: any): void {
|
|
588
|
+
const changed = scrollButtonVisible || pendingScrollMessages > 0;
|
|
589
|
+
scrollButtonVisible = false;
|
|
590
|
+
pendingScrollMessages = 0;
|
|
591
|
+
if (changed) tui.requestRender?.();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function scheduleScrollButtonSync(tui: any, data: string): void {
|
|
595
|
+
if (!scrollButtonVisible || !isScrollNavigationInput(data) || scrollButtonSyncScheduled) return;
|
|
596
|
+
scrollButtonSyncScheduled = true;
|
|
597
|
+
const previousLines = tui.previousLines;
|
|
598
|
+
const check = (attempt: number) => {
|
|
599
|
+
scrollButtonSyncScheduled = false;
|
|
600
|
+
if (toolMouseTui !== tui || !scrollButtonVisible) return;
|
|
601
|
+
// Pi renders on its own frame timer. Do not inspect the old visible window
|
|
602
|
+
// before Zentui has applied the new scroll offset, or PageUp at bottom would
|
|
603
|
+
// immediately hide the button it just requested.
|
|
604
|
+
const rendered = tui.previousLines !== previousLines;
|
|
605
|
+
if (!rendered && attempt < 4) {
|
|
606
|
+
scrollButtonSyncScheduled = true;
|
|
607
|
+
const timer = setTimeout(() => check(attempt + 1), 16);
|
|
608
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
609
|
+
(timer as { unref: () => void }).unref();
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (isFixedEditorAtBottom(tui)) hideScrollButton(tui);
|
|
614
|
+
};
|
|
615
|
+
process.nextTick(() => check(0));
|
|
616
|
+
}
|
|
617
|
+
|
|
488
618
|
function updateScrollButtonFromInput(tui: any, data: string): void {
|
|
489
619
|
if (!isFixedEditorTui(tui)) return;
|
|
490
620
|
|
|
@@ -497,8 +627,15 @@ function updateScrollButtonFromInput(tui: any, data: string): void {
|
|
|
497
627
|
});
|
|
498
628
|
}
|
|
499
629
|
|
|
500
|
-
const jumpedBottom =
|
|
501
|
-
|
|
630
|
+
const jumpedBottom =
|
|
631
|
+
matchesKey(data, "enter")
|
|
632
|
+
|| matchesKey(data, "return")
|
|
633
|
+
|| isScrollBottomInput(data);
|
|
634
|
+
if (jumpedBottom) {
|
|
635
|
+
hideScrollButton(tui);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const nextVisible = movedUp ? true : scrollButtonVisible;
|
|
502
639
|
if (nextVisible !== scrollButtonVisible) {
|
|
503
640
|
scrollButtonVisible = nextVisible;
|
|
504
641
|
tui.requestRender?.();
|
|
@@ -657,8 +794,7 @@ function jumpToBottomWithoutSubmit(tui: any): boolean {
|
|
|
657
794
|
} finally {
|
|
658
795
|
tui.focusedComponent = focused;
|
|
659
796
|
}
|
|
660
|
-
|
|
661
|
-
tui.requestRender?.();
|
|
797
|
+
hideScrollButton(tui);
|
|
662
798
|
return true;
|
|
663
799
|
}
|
|
664
800
|
|
|
@@ -680,7 +816,11 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
680
816
|
|
|
681
817
|
function renderScrollButton(width: number, theme: any): string[] {
|
|
682
818
|
if (!scrollButtonVisible || !isFixedEditorTui(toolMouseTui)) return [];
|
|
683
|
-
const
|
|
819
|
+
const shortcut = formatShortcut(SCROLL_BOTTOM_SHORTCUT);
|
|
820
|
+
const messageText = pendingScrollMessages > 0
|
|
821
|
+
? `${pendingScrollMessages} new message${pendingScrollMessages === 1 ? "" : "s"}`
|
|
822
|
+
: "Back to bottom";
|
|
823
|
+
const label = theme.fg("accent", `[ ↓ ${messageText} · ${shortcut} · click ]`);
|
|
684
824
|
const leftPad = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));
|
|
685
825
|
return [`${" ".repeat(leftPad)}${truncateToWidth(label, width, "…")}`];
|
|
686
826
|
}
|
|
@@ -702,6 +842,7 @@ function patchToolMouseInputCapture(tui: any): void {
|
|
|
702
842
|
const data = args[0];
|
|
703
843
|
if (typeof data === "string") {
|
|
704
844
|
updateScrollButtonFromInput(this, data);
|
|
845
|
+
if (isFixedEditorTui(this) && isScrollBottomInput(data) && jumpToBottomWithoutSubmit(this)) return;
|
|
705
846
|
const packets = parseSgrMousePackets(data);
|
|
706
847
|
if (packets) {
|
|
707
848
|
for (const packet of packets) {
|
|
@@ -710,7 +851,9 @@ function patchToolMouseInputCapture(tui: any): void {
|
|
|
710
851
|
}
|
|
711
852
|
}
|
|
712
853
|
}
|
|
713
|
-
|
|
854
|
+
const result = Reflect.apply(originalHandle, this, args);
|
|
855
|
+
if (typeof data === "string") scheduleScrollButtonSync(this, data);
|
|
856
|
+
return result;
|
|
714
857
|
};
|
|
715
858
|
|
|
716
859
|
try {
|
|
@@ -739,8 +882,14 @@ function restoreToolMouseInputCapture(): void {
|
|
|
739
882
|
function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
740
883
|
if (!toolMouseTui) return undefined;
|
|
741
884
|
updateScrollButtonFromInput(toolMouseTui, data);
|
|
885
|
+
if (isFixedEditorTui(toolMouseTui) && isScrollBottomInput(data) && jumpToBottomWithoutSubmit(toolMouseTui)) {
|
|
886
|
+
return { consume: true };
|
|
887
|
+
}
|
|
742
888
|
const packets = parseSgrMousePackets(data);
|
|
743
|
-
if (!packets)
|
|
889
|
+
if (!packets) {
|
|
890
|
+
scheduleScrollButtonSync(toolMouseTui, data);
|
|
891
|
+
return undefined;
|
|
892
|
+
}
|
|
744
893
|
|
|
745
894
|
let consumed = false;
|
|
746
895
|
for (const packet of packets) {
|
|
@@ -752,6 +901,7 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
|
752
901
|
|
|
753
902
|
// Let scrolling, motion, release, and clicks outside tool results reach the
|
|
754
903
|
// normal TUI input chain (including other extensions such as pi-zentui).
|
|
904
|
+
scheduleScrollButtonSync(toolMouseTui, data);
|
|
755
905
|
return consumed ? { consume: true } : undefined;
|
|
756
906
|
}
|
|
757
907
|
|
|
@@ -771,6 +921,9 @@ function teardownToolMouseInteraction(): void {
|
|
|
771
921
|
restoreToolMouseInputCapture();
|
|
772
922
|
scrollButtonVisible = false;
|
|
773
923
|
scrollButtonWidget = null;
|
|
924
|
+
pendingScrollMessages = 0;
|
|
925
|
+
assistantMessageActive = false;
|
|
926
|
+
scrollButtonSyncScheduled = false;
|
|
774
927
|
toolMouseTui = null;
|
|
775
928
|
toolMouseUi = null;
|
|
776
929
|
}
|
|
@@ -1334,6 +1487,18 @@ function registerWrappedTool(pi: ExtensionAPI, name: keyof ReturnType<typeof cre
|
|
|
1334
1487
|
});
|
|
1335
1488
|
}
|
|
1336
1489
|
|
|
1490
|
+
function notePendingScrollMessage(role: unknown): void {
|
|
1491
|
+
if (!toolMouseTui || !isFixedEditorTui(toolMouseTui) || !scrollButtonVisible) return;
|
|
1492
|
+
if (role === "assistant") {
|
|
1493
|
+
if (assistantMessageActive) return;
|
|
1494
|
+
assistantMessageActive = true;
|
|
1495
|
+
} else if (role !== "toolResult") {
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
pendingScrollMessages += 1;
|
|
1499
|
+
toolMouseTui.requestRender?.();
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1337
1502
|
export default function (pi: ExtensionAPI) {
|
|
1338
1503
|
installGlobalToolRendering();
|
|
1339
1504
|
deactivateLegacyCompactionRendering();
|
|
@@ -1388,10 +1553,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
1388
1553
|
|
|
1389
1554
|
pi.on("session_start", async (_event, ctx) => {
|
|
1390
1555
|
diffPreviewByCall.clear();
|
|
1556
|
+
pendingScrollMessages = 0;
|
|
1557
|
+
assistantMessageActive = false;
|
|
1391
1558
|
updateStatus(ctx);
|
|
1392
1559
|
installToolMouseInteraction(ctx);
|
|
1393
1560
|
});
|
|
1394
1561
|
|
|
1562
|
+
pi.on("message_start", async (event) => {
|
|
1563
|
+
notePendingScrollMessage(event?.message?.role);
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
pi.on("message_update", async (event) => {
|
|
1567
|
+
if (event?.message?.role === "assistant") notePendingScrollMessage("assistant");
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
pi.on("message_end", async (event) => {
|
|
1571
|
+
if (event?.message?.role === "assistant") assistantMessageActive = false;
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1395
1574
|
pi.on("session_shutdown", async () => {
|
|
1396
1575
|
teardownToolMouseInteraction();
|
|
1397
1576
|
deactivateGlobalToolRendering();
|