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.
@@ -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 (const child of children) {
341
- const childLines = child.render(innerWidth);
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 ["", ...layoutExpandedToolCard(themeOf(), children, width).lines];
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 { hits } = layoutExpandedToolCard(themeOf(), children, width);
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 hintText = options.hint === false ? "" : ` • ${showMoreHintText()}`;
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 = hoveredAssistantComponent === component ? "text" : "dim";
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
- return options.leadingBlank === false ? [rendered] : ["", rendered];
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.code & 32) === 0 || packet.final !== "M") return;
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.final !== "M") return;
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
- if ((packet.code & 32) !== 0 && packet.final === "M") {
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
- disableOfficialScrollToEnd(tui);
925
+ syncOfficialScrollToEnd(tui);
921
926
  setScrollButtonWidget({
922
927
  render: (width: number) => {
923
928
  patchFullscreenViewportInput(tui);
924
929
  ensureFullscreenToolMouseMotion(tui);
925
- disableOfficialScrollToEnd(tui);
930
+ syncOfficialScrollToEnd(tui);
926
931
  return renderScrollButton(width, theme);
927
932
  },
928
933
  invalidate() {},
@@ -8,6 +8,7 @@ export {
8
8
  parseSgrMousePackets,
9
9
  isSgrLeftPress,
10
10
  isSgrLeftRelease,
11
+ isSgrIdleMotion,
11
12
  type SgrMousePacket,
12
13
  } from "../../utils/sgr-mouse.ts";
13
14
 
@@ -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 (typeof tui?.scrollToEndIndicator === "function") tui.scrollToEndIndicator = undefined;
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 { getLineNumberWidth, parseDiff, type ParsedDiff } from "./diff-parse.ts";
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
- if (parsed.entries.length === 0) {
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(parsed.entries);
112
+ const splitRows = buildSplitRows(renderEntries);
93
113
  const showHashlineAnchors =
94
114
  options.expanded === true &&
95
- parsed.entries.some((entry) => entry.kind === "line" && !!entry.hashlineAnchorContent);
96
- const lineNumberWidth = getLineNumberWidth(parsed.entries, showHashlineAnchors);
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, parsed.entries, () => {
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(parsed.entries, processBudget);
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 { DiffLineEntry, DiffMetaEntry, ParsedDiffEntry } from "./diff-parse.ts";
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++;