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.
@@ -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
 
@@ -173,6 +173,15 @@ type SettledGroupCache = {
173
173
  lines: string[];
174
174
  };
175
175
 
176
+ type ExpandedGroupCache = {
177
+ width: number;
178
+ hover: boolean;
179
+ theme: unknown;
180
+ fullscreen: boolean;
181
+ paints: readonly unknown[];
182
+ lines: string[];
183
+ };
184
+
176
185
  export class ToolGroupComponent extends Container {
177
186
  readonly toolCallId = `ccstyle-tool-group-${nextGroupId++}`;
178
187
  readonly toolName = "Tool group";
@@ -183,8 +192,10 @@ export class ToolGroupComponent extends Container {
183
192
  }
184
193
  private hintHovered = false;
185
194
  private readonly patch: Patch;
186
- /** 仅缓存已完成且折叠的分组;pending / expanded 每帧现算。 */
195
+ /** 仅缓存已完成且折叠的分组;pending 每帧现算。 */
187
196
  private settledCache: SettledGroupCache | undefined;
197
+ /** 展开分组:子工具 paint 引用未变则复用整卡行。 */
198
+ private expandedPaintCache: ExpandedGroupCache | undefined;
188
199
 
189
200
  constructor(patch: Patch) {
190
201
  super();
@@ -192,14 +203,19 @@ export class ToolGroupComponent extends Container {
192
203
  patch.groups.add(this);
193
204
  }
194
205
 
195
- addTool(tool: any): void {
206
+ private clearPaintCache(): void {
196
207
  this.settledCache = undefined;
208
+ this.expandedPaintCache = undefined;
209
+ }
210
+
211
+ addTool(tool: any): void {
212
+ this.clearPaintCache();
197
213
  this.children.push(tool);
198
214
  tool[PARENT_KEY] = this;
199
215
  }
200
216
 
201
217
  releaseTools(): any[] {
202
- this.settledCache = undefined;
218
+ this.clearPaintCache();
203
219
  const tools = [...this.children];
204
220
  this.children.length = 0;
205
221
  this.patch.groups.delete(this);
@@ -207,21 +223,21 @@ export class ToolGroupComponent extends Container {
207
223
  }
208
224
 
209
225
  removeTool(tool: any): void {
210
- this.settledCache = undefined;
226
+ this.clearPaintCache();
211
227
  const index = this.children.indexOf(tool);
212
228
  if (index >= 0) this.children.splice(index, 1);
213
229
  if (tool?.[PARENT_KEY] === this) delete tool[PARENT_KEY];
214
230
  }
215
231
 
216
232
  setExpanded(expanded: boolean): void {
217
- if (this._expanded !== expanded) this.settledCache = undefined;
233
+ if (this._expanded !== expanded) this.clearPaintCache();
218
234
  this._expanded = expanded;
219
235
  for (const tool of this.children)
220
236
  (tool as Component & { setExpanded?: (expanded: boolean) => void }).setExpanded?.(expanded);
221
237
  }
222
238
 
223
239
  setHintHovered(hovered: boolean): void {
224
- if (this.hintHovered !== hovered) this.settledCache = undefined;
240
+ if (this.hintHovered !== hovered) this.clearPaintCache();
225
241
  this.hintHovered = hovered;
226
242
  }
227
243
 
@@ -250,7 +266,7 @@ export class ToolGroupComponent extends Container {
250
266
  }
251
267
 
252
268
  invalidate(): void {
253
- this.settledCache = undefined;
269
+ this.clearPaintCache();
254
270
  for (const tool of this.children) tool.invalidate?.();
255
271
  }
256
272
 
@@ -330,6 +346,23 @@ export class ToolGroupComponent extends Container {
330
346
  ),
331
347
  ];
332
348
  const total = this.children.length;
349
+ const childPaints = this._expanded
350
+ ? (this.children as any[]).map((tool) => tool.render?.(Math.max(1, width - 2)))
351
+ : undefined;
352
+ if (this._expanded && childPaints) {
353
+ const expandedHit = this.expandedPaintCache;
354
+ if (
355
+ expandedHit &&
356
+ expandedHit.width === width &&
357
+ expandedHit.hover === this.hintHovered &&
358
+ expandedHit.theme === this.patch.theme &&
359
+ expandedHit.fullscreen === isToolTuiFullscreen() &&
360
+ expandedHit.paints.length === childPaints.length &&
361
+ expandedHit.paints.every((paint, index) => paint === childPaints[index])
362
+ ) {
363
+ return expandedHit.lines;
364
+ }
365
+ }
333
366
  const expandedLines: string[] = [];
334
367
  for (let index = 0; index < total; index++) {
335
368
  const tool = this.children[index];
@@ -351,7 +384,7 @@ export class ToolGroupComponent extends Container {
351
384
  );
352
385
  continue;
353
386
  }
354
- const rendered = visibleLines(tool.render(Math.max(1, width - 2)));
387
+ const rendered = visibleLines(Array.isArray(childPaints?.[index]) ? childPaints[index] : []);
355
388
  if (rendered.length) {
356
389
  rendered[0] = stripLeadingStatusIcon(rendered[0])
357
390
  .replace(/^ +/, "")
@@ -376,6 +409,14 @@ export class ToolGroupComponent extends Container {
376
409
  lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
377
410
  }
378
411
  lines.push(paddedBackgroundRow(theme, backgroundSlot, "", width));
412
+ this.expandedPaintCache = {
413
+ width,
414
+ hover: this.hintHovered,
415
+ theme: this.patch.theme,
416
+ fullscreen: isToolTuiFullscreen(),
417
+ paints: childPaints ?? [],
418
+ lines,
419
+ };
379
420
  } else if (counts.pending === 0) {
380
421
  this.storeSettledCache(width, lines);
381
422
  }
@@ -15,9 +15,9 @@ export function toolViewportWidth(width: number): number {
15
15
  export function insetComponent(component: any): any {
16
16
  return {
17
17
  render: (width: number) =>
18
- component.render(Math.max(1, width - 1)).map((line: string) => {
18
+ component.render(Math.max(1, width - 3)).map((line: string) => {
19
19
  const nestedMarker = line.replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*)↳/, "$1 ↳");
20
- return ` ${nestedMarker}`;
20
+ return truncateToWidth(` ${nestedMarker}`, Math.max(0, width), "");
21
21
  }),
22
22
  invalidate: () => component.invalidate?.(),
23
23
  };
@@ -309,6 +309,10 @@ export class ExpandedToolIoView {
309
309
  this.invalidate();
310
310
  }
311
311
 
312
+ getHoveredSection(): ToolIoSection | null {
313
+ return this.hoveredSection;
314
+ }
315
+
312
316
  /** True when the plain truncation footer carries show-more. Input 续行带 │,Output 不带。 */
313
317
  matchShowMoreLine(plainLine: string): ToolIoSection | null {
314
318
  const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
@@ -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
 
@@ -59,6 +59,11 @@ export function isSgrLeftRelease(packet: SgrMousePacket): boolean {
59
59
  return packet.final === "m" && baseButton === 0 && (packet.code & 32) === 0;
60
60
  }
61
61
 
62
+ /** 无按键按住的指针移动(DECSET 1003 的 35)。左键拖动(32,文本选区)不是 hover。 */
63
+ export function isSgrIdleMotion(packet: SgrMousePacket): boolean {
64
+ return packet.final === "M" && (packet.code & 32) !== 0 && mouseBaseButton(packet.code) === 3;
65
+ }
66
+
62
67
  /** 剥离修饰键位(4/8/16/32:shift/meta/ctrl/motion),得到基础按键码。 */
63
68
  export function mouseBaseButton(code: number): number {
64
69
  return code & ~(4 | 8 | 16 | 32);
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.72",
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",