pi-cc-extensions 0.8.70 → 0.8.71

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.
@@ -37,9 +37,10 @@ import {
37
37
  isScrollbarColumnAt,
38
38
  } from "./layout.ts";
39
39
  import {
40
- disableOfficialScrollToEnd,
41
40
  fullscreenLazyTui,
42
41
  hideScrollButton,
42
+ restoreOfficialScrollToEnd,
43
+ syncOfficialScrollToEnd,
43
44
  isScrollBottomInput,
44
45
  renderScrollButton,
45
46
  resetScrollButtonState,
@@ -878,6 +879,7 @@ export function teardownToolMouseInteraction(
878
879
  }
879
880
  restoreToolMouseRenderPatch();
880
881
  restoreFullscreenViewportInput(getToolMouseTui());
882
+ restoreOfficialScrollToEnd(getToolMouseTui());
881
883
  resetScrollButtonState();
882
884
  setToolMouseTui(null);
883
885
  toolMouseUi = null;
@@ -893,6 +895,7 @@ export function resetToolHoverState(): void {
893
895
  setHoveredCompactAssistant(null);
894
896
  setScrollButtonVisible(false);
895
897
  setScrollButtonHovered(false);
898
+ restoreOfficialScrollToEnd(getToolMouseTui());
896
899
  releaseFullscreenToolMouseMotion(getToolMouseTui());
897
900
  }
898
901
 
@@ -917,12 +920,12 @@ export function installToolMouseInteraction(
917
920
  if (isLazyProxyTui(tui)) {
918
921
  patchFullscreenViewportInput(tui);
919
922
  ensureFullscreenToolMouseMotion(tui);
920
- disableOfficialScrollToEnd(tui);
923
+ syncOfficialScrollToEnd(tui);
921
924
  setScrollButtonWidget({
922
925
  render: (width: number) => {
923
926
  patchFullscreenViewportInput(tui);
924
927
  ensureFullscreenToolMouseMotion(tui);
925
- disableOfficialScrollToEnd(tui);
928
+ syncOfficialScrollToEnd(tui);
926
929
  return renderScrollButton(width, theme);
927
930
  },
928
931
  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 (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++;
@@ -16,6 +16,7 @@ import {
16
16
  type DiffLineEntry,
17
17
  type DiffLineKind,
18
18
  type DiffMetaEntry,
19
+ type DiffOmissionEntry,
19
20
  type ParsedDiffEntry,
20
21
  } from "./diff-parse.ts";
21
22
  import { getCellLineNumber, type DiffSpan, type SplitDiffRow } from "./diff-inline.ts";
@@ -27,6 +28,8 @@ export interface RenderedRow {
27
28
  hunkIndex: number | null;
28
29
  }
29
30
 
31
+ type MaterializedDiffEntry = DiffLineEntry | DiffMetaEntry;
32
+
30
33
  interface LineCellRenderParams {
31
34
  kind: DiffLineKind;
32
35
  code: string;
@@ -51,6 +54,7 @@ export interface DiffRenderContext {
51
54
  }
52
55
 
53
56
  const SPLIT_SEPARATOR = " │ ";
57
+ const OMISSION_GLYPH = "⋮";
54
58
  const MIN_SPLIT_COLUMN_WIDTH = 24;
55
59
 
56
60
  function resolveIndicatorGlyph(
@@ -364,6 +368,29 @@ function renderLineCell(
364
368
  );
365
369
  }
366
370
 
371
+ // Reuse line-cell layout without treating omissions as parsed source lines.
372
+ function createOmissionRenderLine(entry: DiffOmissionEntry, compact = false): DiffLineEntry {
373
+ return {
374
+ kind: "line",
375
+ lineKind: "context",
376
+ oldLineNumber: null,
377
+ newLineNumber: null,
378
+ fallbackLineNumber: compact ? "" : OMISSION_GLYPH,
379
+ content: compact ? OMISSION_GLYPH : "",
380
+ raw: entry.raw,
381
+ hunkIndex: entry.hunkIndex,
382
+ };
383
+ }
384
+
385
+ function materializeOmissionEntries(
386
+ entries: ParsedDiffEntry[],
387
+ compact = false,
388
+ ): MaterializedDiffEntry[] {
389
+ return entries.map((entry) =>
390
+ entry.kind === "omission" ? createOmissionRenderLine(entry, compact) : entry,
391
+ );
392
+ }
393
+
367
394
  function pushDiffLineRows(rows: RenderedRow[], lines: string[], entry: DiffLineEntry): void {
368
395
  rows.push(
369
396
  ...lines.map((text) => ({
@@ -374,7 +401,7 @@ function pushDiffLineRows(rows: RenderedRow[], lines: string[], entry: DiffLineE
374
401
  }
375
402
 
376
403
  function processDiffEntries(
377
- entries: ParsedDiffEntry[],
404
+ entries: MaterializedDiffEntry[],
378
405
  ctx: DiffRenderContext,
379
406
  processLine: (entry: DiffLineEntry) => string[],
380
407
  ): RenderedRow[] {
@@ -395,7 +422,7 @@ export function renderUnified(
395
422
  ctx: DiffRenderContext,
396
423
  lineNumberWidth: number,
397
424
  ): RenderedRow[] {
398
- return processDiffEntries(entries, ctx, (entry) => {
425
+ return processDiffEntries(materializeOmissionEntries(entries), ctx, (entry) => {
399
426
  const lineNumber =
400
427
  entry.lineKind === "add"
401
428
  ? formatLineNumberLabel(
@@ -445,6 +472,10 @@ function toUnifiedFallbackRows(
445
472
  ): RenderedRow[] {
446
473
  const flattened: ParsedDiffEntry[] = [];
447
474
  for (const row of rows) {
475
+ if (row.omission) {
476
+ flattened.push(row.omission);
477
+ continue;
478
+ }
448
479
  if (row.meta) {
449
480
  flattened.push(row.meta);
450
481
  continue;
@@ -460,7 +491,7 @@ function toUnifiedFallbackRows(
460
491
  }
461
492
 
462
493
  export function renderCompact(entries: ParsedDiffEntry[], ctx: DiffRenderContext): RenderedRow[] {
463
- return processDiffEntries(entries, ctx, (entry) => {
494
+ return processDiffEntries(materializeOmissionEntries(entries, true), ctx, (entry) => {
464
495
  const codeText = normalizeCodeWhitespace(
465
496
  getCompactLineRenderContent(entry, ctx.showHashlineAnchors),
466
497
  );
@@ -679,8 +710,9 @@ export function renderSplit(
679
710
  continue;
680
711
  }
681
712
 
713
+ const omissionLine = row.omission ? createOmissionRenderLine(row.omission) : undefined;
682
714
  const leftCells = renderSplitCell(
683
- row.left,
715
+ omissionLine ?? row.left,
684
716
  "left",
685
717
  leftWidth,
686
718
  splitLineNumberWidth,
@@ -694,7 +726,7 @@ export function renderSplit(
694
726
  showHashlineAnchors,
695
727
  );
696
728
  const rightCells = renderSplitCell(
697
- row.right,
729
+ omissionLine ?? row.right,
698
730
  "right",
699
731
  rightWidth,
700
732
  splitLineNumberWidth,
@@ -2,7 +2,7 @@ import { visibleWidth } from "@earendil-works/pi-tui";
2
2
  import { fitToWidth } from "./diff-text.ts";
3
3
 
4
4
  export type DiffLineKind = "add" | "remove" | "context";
5
- export type DiffEntryKind = "line" | "meta" | "hunk" | "file";
5
+ export type DiffEntryKind = "line" | "omission" | "meta" | "hunk" | "file";
6
6
 
7
7
  export interface DiffLineEntry {
8
8
  kind: "line";
@@ -16,13 +16,19 @@ export interface DiffLineEntry {
16
16
  hunkIndex: number;
17
17
  }
18
18
 
19
+ export interface DiffOmissionEntry {
20
+ kind: "omission";
21
+ raw: string;
22
+ hunkIndex: number;
23
+ }
24
+
19
25
  export interface DiffMetaEntry {
20
- kind: Exclude<DiffEntryKind, "line">;
26
+ kind: Exclude<DiffEntryKind, "line" | "omission">;
21
27
  raw: string;
22
28
  hunkIndex: number;
23
29
  }
24
30
 
25
- export type ParsedDiffEntry = DiffLineEntry | DiffMetaEntry;
31
+ export type ParsedDiffEntry = DiffLineEntry | DiffOmissionEntry | DiffMetaEntry;
26
32
 
27
33
  export interface ParsedDiff {
28
34
  entries: ParsedDiffEntry[];
@@ -163,6 +169,14 @@ function pushParsedLineEntry(
163
169
  });
164
170
  }
165
171
 
172
+ function createOmissionEntry(raw: string, hunkIndex: number): DiffOmissionEntry {
173
+ return {
174
+ kind: "omission",
175
+ raw,
176
+ hunkIndex,
177
+ };
178
+ }
179
+
166
180
  function createMetaEntry(raw: string, hunkIndex: number): DiffMetaEntry {
167
181
  return {
168
182
  kind: classifyMetaLine(raw),
@@ -228,7 +242,7 @@ export function parseDiff(diffText: string): ParsedDiff {
228
242
 
229
243
  // Pi pads omitted context with a blank line number; it is not a source row.
230
244
  if (!hasHunkHeader && PI_OMISSION_LINE_PATTERN.test(rawLine)) {
231
- entries.push(createMetaEntry(rawLine, hunkIndex));
245
+ entries.push(createOmissionEntry(rawLine, hunkIndex));
232
246
  continue;
233
247
  }
234
248
 
@@ -42,6 +42,7 @@ export const TOOL_GROUPING_GENERATION_KEY = Symbol.for("pi.ccstyle.tool-grouping
42
42
  export const TOOL_MOUSE_OWNER_KEY = Symbol.for("pi.ccstyle.tool-mouse-owner");
43
43
  export const TOOL_MOUSE_TUI_SLOT = Symbol.for("pi.ccstyle.tool-mouse-tui");
44
44
  export const SCROLL_BUTTON_STATE_SLOT = Symbol.for("pi.ccstyle.scroll-button-state");
45
+ export const OFFICIAL_SCROLL_TO_END_KEY = Symbol.for("pi.ccstyle.official-scroll-to-end");
45
46
  export const FLUSH_DOCKED_BASH_PATCH = Symbol.for("pi.ccstyle.flush-docked-bash-patch");
46
47
  export const TOOL_HOVER_STATE_KEY = Symbol.for("pi.ccstyle.tool-hover-state");
47
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.70",
3
+ "version": "0.8.71",
4
4
  "license": "MIT",
5
5
  "description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
6
6
  "type": "module",