pi-cc-extensions 0.8.70 → 0.8.72
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/README.en.md +3 -1
- package/README.md +3 -1
- package/extensions/config/config.ts +4 -0
- package/extensions/config/panel.ts +16 -0
- package/extensions/feature/shell/footer.ts +413 -0
- package/extensions/index.ts +2 -0
- package/extensions/renderer/compact-mode.ts +154 -9
- package/extensions/renderer/default-mode.ts +82 -0
- package/extensions/renderer/mouse/interaction.ts +11 -6
- package/extensions/renderer/mouse/packets.ts +1 -0
- package/extensions/renderer/mouse/scroll.ts +23 -1
- package/extensions/renderer/tool/diff/diff-edit-render.ts +27 -7
- package/extensions/renderer/tool/diff/diff-inline.ts +13 -1
- package/extensions/renderer/tool/diff/diff-layout.ts +37 -5
- package/extensions/renderer/tool/diff/diff-parse.ts +18 -4
- package/extensions/renderer/tool/grouping.ts +49 -8
- package/extensions/renderer/tool/result.ts +6 -2
- package/extensions/utils/patch-keys.ts +1 -0
- package/extensions/utils/sgr-mouse.ts +5 -0
- package/package.json +1 -1
|
@@ -327,6 +327,7 @@ function layoutExpandedToolCard(
|
|
|
327
327
|
theme: any,
|
|
328
328
|
children: any[],
|
|
329
329
|
width: number,
|
|
330
|
+
paints?: string[][],
|
|
330
331
|
): { lines: string[]; hits: Array<{ child: any; start: number; end: number }> } {
|
|
331
332
|
const slot = "userMessageBg";
|
|
332
333
|
const toolBgAnsi = darkenBgAnsi(theme, slot);
|
|
@@ -337,8 +338,10 @@ function layoutExpandedToolCard(
|
|
|
337
338
|
lines.push(paddedBackgroundRow(theme, slot, "", width));
|
|
338
339
|
// 展开不再显示摘要行:跳过第一个 child 的首行空白,避免白占一行
|
|
339
340
|
let skipLeadingBlank = true;
|
|
340
|
-
for (
|
|
341
|
-
const
|
|
341
|
+
for (let childIndex = 0; childIndex < children.length; childIndex++) {
|
|
342
|
+
const child = children[childIndex];
|
|
343
|
+
const painted = paints?.[childIndex];
|
|
344
|
+
const childLines = Array.isArray(painted) ? painted : child.render(innerWidth);
|
|
342
345
|
// 展开的 thinking 与 tool 一样走嵌套深色卡,避免贴在外卡同色底上看不见。
|
|
343
346
|
const nest = child.__ccToolCard || (isThinkingPreview(child) && child.expanded === true);
|
|
344
347
|
if (!nest) {
|
|
@@ -401,21 +404,56 @@ function compactRoundCard(
|
|
|
401
404
|
});
|
|
402
405
|
}
|
|
403
406
|
}
|
|
407
|
+
let paint:
|
|
408
|
+
| {
|
|
409
|
+
width: number;
|
|
410
|
+
theme: unknown;
|
|
411
|
+
paints: unknown[];
|
|
412
|
+
lines: string[];
|
|
413
|
+
hits: Array<{ child: any; start: number; end: number }>;
|
|
414
|
+
}
|
|
415
|
+
| undefined;
|
|
416
|
+
const layout = (width: number) => {
|
|
417
|
+
const theme = themeOf();
|
|
418
|
+
const innerWidth = Math.max(0, width - 2);
|
|
419
|
+
const paints = children.map((child) => {
|
|
420
|
+
const lines = child.render?.(innerWidth);
|
|
421
|
+
return Array.isArray(lines) ? lines : [];
|
|
422
|
+
});
|
|
423
|
+
if (
|
|
424
|
+
paint &&
|
|
425
|
+
paint.width === width &&
|
|
426
|
+
paint.theme === theme &&
|
|
427
|
+
paint.paints.length === paints.length &&
|
|
428
|
+
paint.paints.every((item, index) => item === paints[index])
|
|
429
|
+
) {
|
|
430
|
+
return paint;
|
|
431
|
+
}
|
|
432
|
+
const laid = layoutExpandedToolCard(theme, children, width, paints);
|
|
433
|
+
paint = {
|
|
434
|
+
width,
|
|
435
|
+
theme,
|
|
436
|
+
paints,
|
|
437
|
+
lines: ["", ...laid.lines],
|
|
438
|
+
hits: laid.hits,
|
|
439
|
+
};
|
|
440
|
+
return paint;
|
|
441
|
+
};
|
|
404
442
|
return {
|
|
405
443
|
children,
|
|
406
444
|
render(width: number): string[] {
|
|
407
|
-
return
|
|
445
|
+
return layout(width).lines;
|
|
408
446
|
},
|
|
409
447
|
childAtRow(localRow: number, width: number) {
|
|
410
448
|
if (localRow < 1) return null;
|
|
411
449
|
const row = localRow - 1;
|
|
412
|
-
const
|
|
413
|
-
for (const hit of hits) {
|
|
450
|
+
for (const hit of layout(width).hits) {
|
|
414
451
|
if (row >= hit.start && row < hit.end) return hit.child;
|
|
415
452
|
}
|
|
416
453
|
return null;
|
|
417
454
|
},
|
|
418
455
|
invalidate() {
|
|
456
|
+
paint = undefined;
|
|
419
457
|
for (const child of children) child.invalidate?.();
|
|
420
458
|
},
|
|
421
459
|
};
|
|
@@ -553,14 +591,77 @@ const compactRichDiffCache = new WeakMap<
|
|
|
553
591
|
{ result: unknown; collapsed?: unknown; expanded?: unknown }
|
|
554
592
|
>();
|
|
555
593
|
|
|
594
|
+
type CompactEditPaintHit = {
|
|
595
|
+
width: number;
|
|
596
|
+
theme: unknown;
|
|
597
|
+
expanded: boolean;
|
|
598
|
+
result: unknown;
|
|
599
|
+
isPartial: boolean;
|
|
600
|
+
args: unknown;
|
|
601
|
+
hover: boolean;
|
|
602
|
+
ioHover: unknown;
|
|
603
|
+
lines: string[];
|
|
604
|
+
};
|
|
605
|
+
const compactEditPaintCache = new WeakMap<object, CompactEditPaintHit>();
|
|
606
|
+
|
|
607
|
+
function compactEditIoHover(component: any): unknown {
|
|
608
|
+
const view = component?.resultRendererComponent;
|
|
609
|
+
return typeof view?.getHoveredSection === "function" ? view.getHoveredSection() : null;
|
|
610
|
+
}
|
|
611
|
+
|
|
556
612
|
/**
|
|
557
613
|
* compact edit/write:标题行 + mode=on 同一套 rich diff。
|
|
558
614
|
* 折叠/展开都走 `renderRichToolResult`,limits 不另开一套。
|
|
615
|
+
* 已完成的结果跨帧复用行;pending 不缓存,避免 loader 动画冻住。
|
|
559
616
|
*/
|
|
560
617
|
function compactEditWriteLines(
|
|
561
618
|
component: any,
|
|
562
619
|
width: number,
|
|
563
620
|
writeMetadata?: WriteExecutionMetadataStore,
|
|
621
|
+
): string[] {
|
|
622
|
+
const expanded = component.expanded === true;
|
|
623
|
+
const isPartial = component.isPartial === true;
|
|
624
|
+
const pending = !component.result || isPartial;
|
|
625
|
+
const hover = isToolCallHovered(component.toolCallId);
|
|
626
|
+
const ioHover = compactEditIoHover(component);
|
|
627
|
+
const theme = themeOf();
|
|
628
|
+
if (!pending) {
|
|
629
|
+
const hit = compactEditPaintCache.get(component);
|
|
630
|
+
if (
|
|
631
|
+
hit &&
|
|
632
|
+
hit.width === width &&
|
|
633
|
+
hit.theme === theme &&
|
|
634
|
+
hit.expanded === expanded &&
|
|
635
|
+
hit.result === component.result &&
|
|
636
|
+
hit.isPartial === isPartial &&
|
|
637
|
+
hit.args === component.args &&
|
|
638
|
+
hit.hover === hover &&
|
|
639
|
+
hit.ioHover === ioHover
|
|
640
|
+
) {
|
|
641
|
+
return hit.lines;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const lines = paintCompactEditWrite(component, width, writeMetadata);
|
|
645
|
+
if (!pending) {
|
|
646
|
+
compactEditPaintCache.set(component, {
|
|
647
|
+
width,
|
|
648
|
+
theme,
|
|
649
|
+
expanded,
|
|
650
|
+
result: component.result,
|
|
651
|
+
isPartial,
|
|
652
|
+
args: component.args,
|
|
653
|
+
hover,
|
|
654
|
+
ioHover: compactEditIoHover(component),
|
|
655
|
+
lines,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return lines;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function paintCompactEditWrite(
|
|
662
|
+
component: any,
|
|
663
|
+
width: number,
|
|
664
|
+
writeMetadata?: WriteExecutionMetadataStore,
|
|
564
665
|
): string[] {
|
|
565
666
|
const theme = themeOf();
|
|
566
667
|
const result = component.result;
|
|
@@ -656,15 +757,45 @@ function compactAssistantLineComponent(
|
|
|
656
757
|
options: { hint?: boolean; leadingBlank?: boolean; pad?: number } = {},
|
|
657
758
|
): any {
|
|
658
759
|
const self = component as any;
|
|
760
|
+
let paint:
|
|
761
|
+
| {
|
|
762
|
+
width: number;
|
|
763
|
+
theme: unknown;
|
|
764
|
+
resolved: string;
|
|
765
|
+
hover: boolean;
|
|
766
|
+
hint: boolean;
|
|
767
|
+
pad: number;
|
|
768
|
+
frame: number;
|
|
769
|
+
leadingBlank: boolean;
|
|
770
|
+
lines: string[];
|
|
771
|
+
}
|
|
772
|
+
| undefined;
|
|
659
773
|
return {
|
|
660
774
|
render(width: number): string[] {
|
|
661
775
|
const theme = themeOf();
|
|
662
776
|
const pad = Math.max(0, options.pad ?? (Number(self.outputPad) || 0));
|
|
663
777
|
const available = Math.max(0, width - pad);
|
|
664
|
-
const
|
|
778
|
+
const hint = options.hint !== false;
|
|
779
|
+
const hintText = hint ? ` • ${showMoreHintText()}` : "";
|
|
665
780
|
const summaryWidth = Math.max(0, available - visibleWidth(hintText));
|
|
666
781
|
const resolved = typeof summary === "function" ? summary() : summary;
|
|
667
782
|
const runningActive = resolved.startsWith("Running...");
|
|
783
|
+
const hover = hoveredAssistantComponent === component;
|
|
784
|
+
const frame = runningActive ? (query?.getThinkingAnimationFrame?.() ?? 0) : 0;
|
|
785
|
+
const leadingBlank = options.leadingBlank !== false;
|
|
786
|
+
if (
|
|
787
|
+
paint &&
|
|
788
|
+
paint.width === width &&
|
|
789
|
+
paint.theme === theme &&
|
|
790
|
+
paint.resolved === resolved &&
|
|
791
|
+
paint.hover === hover &&
|
|
792
|
+
paint.hint === hint &&
|
|
793
|
+
paint.pad === pad &&
|
|
794
|
+
paint.frame === frame &&
|
|
795
|
+
paint.leadingBlank === leadingBlank
|
|
796
|
+
) {
|
|
797
|
+
return paint.lines;
|
|
798
|
+
}
|
|
668
799
|
const plainText = truncateToWidth(resolved, summaryWidth, "…");
|
|
669
800
|
let text = theme.fg("muted", plainText);
|
|
670
801
|
if (runningActive || plainText.startsWith("Ran for ")) {
|
|
@@ -684,12 +815,26 @@ function compactAssistantLineComponent(
|
|
|
684
815
|
text = `${styleCompactThinkingText(heading, theme)}${theme.fg("muted", tools)}`;
|
|
685
816
|
}
|
|
686
817
|
}
|
|
687
|
-
const hintColor =
|
|
818
|
+
const hintColor = hover ? "text" : "dim";
|
|
688
819
|
const line = `${text}${hintText ? theme.fg(hintColor, hintText) : ""}`;
|
|
689
820
|
const rendered = `${" ".repeat(pad)}${truncateToWidth(line, available, "")}`;
|
|
690
|
-
|
|
821
|
+
const lines = leadingBlank ? ["", rendered] : [rendered];
|
|
822
|
+
paint = {
|
|
823
|
+
width,
|
|
824
|
+
theme,
|
|
825
|
+
resolved,
|
|
826
|
+
hover,
|
|
827
|
+
hint,
|
|
828
|
+
pad,
|
|
829
|
+
frame,
|
|
830
|
+
leadingBlank,
|
|
831
|
+
lines,
|
|
832
|
+
};
|
|
833
|
+
return lines;
|
|
834
|
+
},
|
|
835
|
+
invalidate() {
|
|
836
|
+
paint = undefined;
|
|
691
837
|
},
|
|
692
|
-
invalidate() {},
|
|
693
838
|
};
|
|
694
839
|
}
|
|
695
840
|
|
|
@@ -58,6 +58,17 @@ type ToolRenderMethods = {
|
|
|
58
58
|
getResultRenderer: (...args: any[]) => any;
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
type ToolPaintHit = {
|
|
62
|
+
width: number;
|
|
63
|
+
expanded: boolean;
|
|
64
|
+
isPartial: boolean;
|
|
65
|
+
result: unknown;
|
|
66
|
+
args: unknown;
|
|
67
|
+
callHover: boolean;
|
|
68
|
+
ioHover: string | null;
|
|
69
|
+
lines: string[];
|
|
70
|
+
};
|
|
71
|
+
|
|
61
72
|
type GlobalToolRenderPatch = {
|
|
62
73
|
version: 2;
|
|
63
74
|
prototype: any;
|
|
@@ -68,6 +79,11 @@ type GlobalToolRenderPatch = {
|
|
|
68
79
|
byName: Map<string, any>;
|
|
69
80
|
downstream: ToolRenderMethods;
|
|
70
81
|
installed: ToolRenderMethods;
|
|
82
|
+
originalRender?: (width: number) => string[];
|
|
83
|
+
originalInvalidate?: () => void;
|
|
84
|
+
renderPaint?: (width: number) => string[];
|
|
85
|
+
invalidatePaint?: () => void;
|
|
86
|
+
paintCache?: WeakMap<object, ToolPaintHit>;
|
|
71
87
|
};
|
|
72
88
|
|
|
73
89
|
type ToolExpandedBackgroundPatch = {
|
|
@@ -478,12 +494,38 @@ function disconnectGlobalToolRenderPatch(patch: GlobalToolRenderPatch | undefine
|
|
|
478
494
|
patch.byName.clear();
|
|
479
495
|
}
|
|
480
496
|
|
|
497
|
+
function ioHoverOf(tool: any): string | null {
|
|
498
|
+
const view = tool?.resultRendererComponent;
|
|
499
|
+
if (!view || typeof view.getHoveredSection !== "function") return null;
|
|
500
|
+
return view.getHoveredSection();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function toolPaintMatches(hit: ToolPaintHit, tool: any, width: number): boolean {
|
|
504
|
+
return (
|
|
505
|
+
hit.width === width &&
|
|
506
|
+
hit.expanded === Boolean(tool.expanded) &&
|
|
507
|
+
hit.isPartial === Boolean(tool.isPartial) &&
|
|
508
|
+
hit.result === tool.result &&
|
|
509
|
+
hit.args === tool.args &&
|
|
510
|
+
hit.callHover === isToolCallHovered(tool.toolCallId) &&
|
|
511
|
+
hit.ioHover === ioHoverOf(tool)
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
481
515
|
function installGlobalToolRendering(
|
|
482
516
|
writeExecutionMetadata: WriteExecutionMetadataStore,
|
|
483
517
|
): GlobalToolRenderPatch {
|
|
484
518
|
const prototype = (ToolExecutionComponent as any).prototype;
|
|
485
519
|
const previous = patchRegistry.get<GlobalToolRenderPatch>(GLOBAL_TOOL_RENDER_PATCH);
|
|
486
520
|
const downstream = downstreamForGlobalToolInstall(prototype, previous);
|
|
521
|
+
const originalRender =
|
|
522
|
+
previous?.originalRender && prototype.render === previous.renderPaint
|
|
523
|
+
? previous.originalRender
|
|
524
|
+
: prototype.render;
|
|
525
|
+
const originalInvalidate =
|
|
526
|
+
previous?.originalInvalidate && prototype.invalidate === previous.invalidatePaint
|
|
527
|
+
? previous.originalInvalidate
|
|
528
|
+
: prototype.invalidate;
|
|
487
529
|
if (isOwnershipAwarePatch(previous)) disconnectGlobalToolRenderPatch(previous);
|
|
488
530
|
|
|
489
531
|
const patch: GlobalToolRenderPatch = {
|
|
@@ -496,6 +538,9 @@ function installGlobalToolRendering(
|
|
|
496
538
|
byName: new Map(),
|
|
497
539
|
downstream,
|
|
498
540
|
installed: undefined as any,
|
|
541
|
+
originalRender,
|
|
542
|
+
originalInvalidate,
|
|
543
|
+
paintCache: new WeakMap(),
|
|
499
544
|
};
|
|
500
545
|
|
|
501
546
|
patch.installed = {
|
|
@@ -532,6 +577,33 @@ function installGlobalToolRendering(
|
|
|
532
577
|
prototype.getRenderShell = patch.installed.getRenderShell;
|
|
533
578
|
prototype.getCallRenderer = patch.installed.getCallRenderer;
|
|
534
579
|
prototype.getResultRenderer = patch.installed.getResultRenderer;
|
|
580
|
+
// fullscreen 滚动/选区每帧整树 render;内容未变时复用上一帧行,避免重做 diff/Box。
|
|
581
|
+
patch.renderPaint = function (this: any, width: number): string[] {
|
|
582
|
+
if (!patch.active || patch.mode() === "off") {
|
|
583
|
+
return originalRender.call(this, width);
|
|
584
|
+
}
|
|
585
|
+
const cache = patch.paintCache;
|
|
586
|
+
const hit = cache?.get(this);
|
|
587
|
+
if (hit && toolPaintMatches(hit, this, width)) return hit.lines;
|
|
588
|
+
const lines = originalRender.call(this, width);
|
|
589
|
+
cache?.set(this, {
|
|
590
|
+
width,
|
|
591
|
+
expanded: Boolean(this.expanded),
|
|
592
|
+
isPartial: Boolean(this.isPartial),
|
|
593
|
+
result: this.result,
|
|
594
|
+
args: this.args,
|
|
595
|
+
callHover: isToolCallHovered(this.toolCallId),
|
|
596
|
+
ioHover: ioHoverOf(this),
|
|
597
|
+
lines,
|
|
598
|
+
});
|
|
599
|
+
return lines;
|
|
600
|
+
};
|
|
601
|
+
patch.invalidatePaint = function (this: any): void {
|
|
602
|
+
patch.paintCache?.delete(this);
|
|
603
|
+
originalInvalidate.call(this);
|
|
604
|
+
};
|
|
605
|
+
prototype.render = patch.renderPaint;
|
|
606
|
+
prototype.invalidate = patch.invalidatePaint;
|
|
535
607
|
patchRegistry.install(GLOBAL_TOOL_RENDER_PATCH, patch);
|
|
536
608
|
return patch;
|
|
537
609
|
}
|
|
@@ -552,6 +624,16 @@ function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void {
|
|
|
552
624
|
if (prototype.getResultRenderer === patch.installed.getResultRenderer) {
|
|
553
625
|
prototype.getResultRenderer = patch.downstream.getResultRenderer;
|
|
554
626
|
}
|
|
627
|
+
if (patch.renderPaint && prototype.render === patch.renderPaint && patch.originalRender) {
|
|
628
|
+
prototype.render = patch.originalRender;
|
|
629
|
+
}
|
|
630
|
+
if (
|
|
631
|
+
patch.invalidatePaint &&
|
|
632
|
+
prototype.invalidate === patch.invalidatePaint &&
|
|
633
|
+
patch.originalInvalidate
|
|
634
|
+
) {
|
|
635
|
+
prototype.invalidate = patch.originalInvalidate;
|
|
636
|
+
}
|
|
555
637
|
}
|
|
556
638
|
|
|
557
639
|
/** 展开面板背景统一为 user message 背景色;折叠行保持原生状态色。
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
collectToolComponents,
|
|
20
20
|
extractToolFramePlacements,
|
|
21
|
+
isSgrIdleMotion,
|
|
21
22
|
isSgrLeftPress,
|
|
22
23
|
isSgrLeftRelease,
|
|
23
24
|
isToolExecutionComponent,
|
|
@@ -37,9 +38,10 @@ import {
|
|
|
37
38
|
isScrollbarColumnAt,
|
|
38
39
|
} from "./layout.ts";
|
|
39
40
|
import {
|
|
40
|
-
disableOfficialScrollToEnd,
|
|
41
41
|
fullscreenLazyTui,
|
|
42
42
|
hideScrollButton,
|
|
43
|
+
restoreOfficialScrollToEnd,
|
|
44
|
+
syncOfficialScrollToEnd,
|
|
43
45
|
isScrollBottomInput,
|
|
44
46
|
renderScrollButton,
|
|
45
47
|
resetScrollButtonState,
|
|
@@ -137,7 +139,7 @@ function tryOpenToolIoShowMore(region: InteractionRegion): boolean {
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
140
|
-
if ((packet
|
|
142
|
+
if (!isSgrIdleMotion(packet)) return;
|
|
141
143
|
const region = interactionRegionAt(packet);
|
|
142
144
|
const nextScrollButtonHovered = region?.kind === "scroll-bottom";
|
|
143
145
|
const scrollButtonChanged = setScrollButtonHovered(nextScrollButtonHovered);
|
|
@@ -403,7 +405,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
403
405
|
* expanded 卡截断头 show-more、回到底部按钮。motion 不 consume,官方链照常。
|
|
404
406
|
*/
|
|
405
407
|
function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
406
|
-
if (packet
|
|
408
|
+
if (!isSgrIdleMotion(packet)) return;
|
|
407
409
|
const layout = tui.currentLayout;
|
|
408
410
|
if (!layout?.root) return;
|
|
409
411
|
const x = packet.col - 1;
|
|
@@ -501,7 +503,8 @@ function patchFullscreenViewportInput(tui: any): void {
|
|
|
501
503
|
if (isSgrLeftPress(packet) && handleFullscreenToolClick(tui, packet)) {
|
|
502
504
|
return { consume: true };
|
|
503
505
|
}
|
|
504
|
-
|
|
506
|
+
// 仅无按键移动走 hover;左键拖动(文本多选)放行官方选区,避免每像素命中+重绘。
|
|
507
|
+
if (isSgrIdleMotion(packet)) {
|
|
505
508
|
handleFullscreenToolHover(tui, packet);
|
|
506
509
|
}
|
|
507
510
|
}
|
|
@@ -878,6 +881,7 @@ export function teardownToolMouseInteraction(
|
|
|
878
881
|
}
|
|
879
882
|
restoreToolMouseRenderPatch();
|
|
880
883
|
restoreFullscreenViewportInput(getToolMouseTui());
|
|
884
|
+
restoreOfficialScrollToEnd(getToolMouseTui());
|
|
881
885
|
resetScrollButtonState();
|
|
882
886
|
setToolMouseTui(null);
|
|
883
887
|
toolMouseUi = null;
|
|
@@ -893,6 +897,7 @@ export function resetToolHoverState(): void {
|
|
|
893
897
|
setHoveredCompactAssistant(null);
|
|
894
898
|
setScrollButtonVisible(false);
|
|
895
899
|
setScrollButtonHovered(false);
|
|
900
|
+
restoreOfficialScrollToEnd(getToolMouseTui());
|
|
896
901
|
releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
897
902
|
}
|
|
898
903
|
|
|
@@ -917,12 +922,12 @@ export function installToolMouseInteraction(
|
|
|
917
922
|
if (isLazyProxyTui(tui)) {
|
|
918
923
|
patchFullscreenViewportInput(tui);
|
|
919
924
|
ensureFullscreenToolMouseMotion(tui);
|
|
920
|
-
|
|
925
|
+
syncOfficialScrollToEnd(tui);
|
|
921
926
|
setScrollButtonWidget({
|
|
922
927
|
render: (width: number) => {
|
|
923
928
|
patchFullscreenViewportInput(tui);
|
|
924
929
|
ensureFullscreenToolMouseMotion(tui);
|
|
925
|
-
|
|
930
|
+
syncOfficialScrollToEnd(tui);
|
|
926
931
|
return renderScrollButton(width, theme);
|
|
927
932
|
},
|
|
928
933
|
invalidate() {},
|
|
@@ -9,6 +9,7 @@ import { config } from "../../config/config.ts";
|
|
|
9
9
|
import { isLazyProxyTui } from "../../utils/fullscreen-detect.ts";
|
|
10
10
|
import { parseSgrMousePackets } from "./packets.ts";
|
|
11
11
|
import {
|
|
12
|
+
OFFICIAL_SCROLL_TO_END_KEY,
|
|
12
13
|
patchRegistry,
|
|
13
14
|
SCROLL_BUTTON_STATE_SLOT,
|
|
14
15
|
TOOL_MOUSE_TUI_SLOT,
|
|
@@ -81,7 +82,28 @@ export function fullscreenLazyTui(tui: any): boolean {
|
|
|
81
82
|
|
|
82
83
|
/** 关掉 pi 0.85 Jump to latest overlay,避免和本仓库 dock 按钮叠两层。 */
|
|
83
84
|
export function disableOfficialScrollToEnd(tui: any): void {
|
|
84
|
-
if (
|
|
85
|
+
if (!tui) return;
|
|
86
|
+
const current = tui.scrollToEndIndicator;
|
|
87
|
+
if (typeof current !== "function") return;
|
|
88
|
+
// 原函数放 object 里,避免惰性 Proxy 对 function 属性再包一层。
|
|
89
|
+
if (!tui[OFFICIAL_SCROLL_TO_END_KEY]) {
|
|
90
|
+
tui[OFFICIAL_SCROLL_TO_END_KEY] = { original: current };
|
|
91
|
+
}
|
|
92
|
+
tui.scrollToEndIndicator = undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** /ccstyle off 或 teardown 时还回官方 overlay。 */
|
|
96
|
+
export function restoreOfficialScrollToEnd(tui: any): void {
|
|
97
|
+
if (!tui) return;
|
|
98
|
+
const original = tui[OFFICIAL_SCROLL_TO_END_KEY]?.original;
|
|
99
|
+
if (typeof original === "function") tui.scrollToEndIndicator = original;
|
|
100
|
+
tui[OFFICIAL_SCROLL_TO_END_KEY] = undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** on/compact 用 dock 按钮;off 还回 pi 原生 Jump to latest。 */
|
|
104
|
+
export function syncOfficialScrollToEnd(tui: any): void {
|
|
105
|
+
if (toolMouseInteractionActive()) disableOfficialScrollToEnd(tui);
|
|
106
|
+
else restoreOfficialScrollToEnd(tui);
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
/** 官方 fullscreen:是否已跟随 transcript 底部(按钮隐藏条件)。 */
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { EditToolDetails } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { sanitizeToolResultText } from "../../../utils/tool-result-sanitize.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getLineNumberWidth,
|
|
6
|
+
parseDiff,
|
|
7
|
+
type ParsedDiff,
|
|
8
|
+
type ParsedDiffEntry,
|
|
9
|
+
} from "./diff-parse.ts";
|
|
5
10
|
import { buildInlineHighlightMap, buildSplitRows } from "./diff-inline.ts";
|
|
6
11
|
import { resolveDiffPalette, type DiffTheme } from "./diff-palette.ts";
|
|
7
12
|
import { createCodeLineHighlighter, resolveLanguageFromPath } from "./diff-highlight.ts";
|
|
@@ -44,6 +49,20 @@ function safeGetDiff(details: unknown): string {
|
|
|
44
49
|
return typeof typed.diff === "string" ? typed.diff : "";
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
// Pi appends an omission marker after final context to say the file continues.
|
|
53
|
+
// It does not separate visible diff regions, so keep only omissions before a rendered line.
|
|
54
|
+
function omitTerminalOmissions(entries: ParsedDiffEntry[]): ParsedDiffEntry[] {
|
|
55
|
+
let lastLineIndex = -1;
|
|
56
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
57
|
+
if (entries[index]?.kind === "line") {
|
|
58
|
+
lastLineIndex = index;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return entries.filter((entry, index) => entry.kind !== "omission" || index < lastLineIndex);
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
/**
|
|
48
67
|
* edit 变更行统计(compact 单行 `(+A -D)` 用):复用现有 parser,diff 文件头不计入。
|
|
49
68
|
* 无 diff 或解析失败时返回 undefined,不把未知状态误报为零变更。
|
|
@@ -85,22 +104,23 @@ export function renderEditDiffResult(
|
|
|
85
104
|
return new Text(theme.fg("warning", `↳ unable to render diff: ${message}`), 0, 0);
|
|
86
105
|
}
|
|
87
106
|
|
|
88
|
-
|
|
107
|
+
const renderEntries = omitTerminalOmissions(parsed.entries);
|
|
108
|
+
if (renderEntries.length === 0) {
|
|
89
109
|
return new Text(theme.fg("muted", "↳ no diff data"), 0, 0);
|
|
90
110
|
}
|
|
91
111
|
|
|
92
|
-
const splitRows = buildSplitRows(
|
|
112
|
+
const splitRows = buildSplitRows(renderEntries);
|
|
93
113
|
const showHashlineAnchors =
|
|
94
114
|
options.expanded === true &&
|
|
95
|
-
|
|
96
|
-
const lineNumberWidth = getLineNumberWidth(
|
|
115
|
+
renderEntries.some((entry) => entry.kind === "line" && !!entry.hashlineAnchorContent);
|
|
116
|
+
const lineNumberWidth = getLineNumberWidth(renderEntries, showHashlineAnchors);
|
|
97
117
|
const palette = resolveDiffPalette(theme);
|
|
98
118
|
// Rich diffs use ccstyle's self shell. Keep the panel transparent so the
|
|
99
119
|
// separator cannot leak toolSuccessBg across the entire new column.
|
|
100
120
|
const containerBgAnsi = undefined;
|
|
101
121
|
const language = resolveLanguageFromPath(options.filePath);
|
|
102
122
|
const cache = createDiffRenderCache();
|
|
103
|
-
const highlightLine = createCodeLineHighlighter(language, theme,
|
|
123
|
+
const highlightLine = createCodeLineHighlighter(language, theme, renderEntries, () => {
|
|
104
124
|
cache.invalidate();
|
|
105
125
|
options.invalidate?.();
|
|
106
126
|
});
|
|
@@ -149,7 +169,7 @@ export function renderEditDiffResult(
|
|
|
149
169
|
const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
|
|
150
170
|
// Only highlight/render a prefix that can fill the display limit; full-diff
|
|
151
171
|
// LCS + syntax highlight on thousands of hidden lines is pure waste when collapsed.
|
|
152
|
-
const entryBudget = takeEntriesForLineBudget(
|
|
172
|
+
const entryBudget = takeEntriesForLineBudget(renderEntries, processBudget);
|
|
153
173
|
const splitBudget = takeSplitRowsForBudget(splitRows, processBudget);
|
|
154
174
|
const inlineHighlights = buildInlineHighlightMap(splitBudget.rows);
|
|
155
175
|
const renderCtx: DiffRenderContext = {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { normalizeCodeWhitespace } from "./diff-text.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
DiffLineEntry,
|
|
4
|
+
DiffMetaEntry,
|
|
5
|
+
DiffOmissionEntry,
|
|
6
|
+
ParsedDiffEntry,
|
|
7
|
+
} from "./diff-parse.ts";
|
|
3
8
|
|
|
4
9
|
export interface DiffSpan {
|
|
5
10
|
start: number;
|
|
@@ -9,6 +14,7 @@ export interface DiffSpan {
|
|
|
9
14
|
export interface SplitDiffRow {
|
|
10
15
|
left?: DiffLineEntry;
|
|
11
16
|
right?: DiffLineEntry;
|
|
17
|
+
omission?: DiffOmissionEntry;
|
|
12
18
|
meta?: DiffMetaEntry;
|
|
13
19
|
hunkIndex: number | null;
|
|
14
20
|
}
|
|
@@ -240,6 +246,12 @@ export function buildSplitRows(entries: ParsedDiffEntry[]): SplitDiffRow[] {
|
|
|
240
246
|
break;
|
|
241
247
|
}
|
|
242
248
|
|
|
249
|
+
if (entry.kind === "omission") {
|
|
250
|
+
rows.push({ omission: entry, hunkIndex: entry.hunkIndex || null });
|
|
251
|
+
index++;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
243
255
|
if (entry.kind !== "line") {
|
|
244
256
|
rows.push({ meta: entry, hunkIndex: entry.hunkIndex || null });
|
|
245
257
|
index++;
|