pi-cc-extensions 0.8.13 → 0.8.15

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.
@@ -277,29 +277,40 @@ function resolveLanguageFromPath(rawPath: string | undefined): string | undefine
277
277
  }
278
278
  }
279
279
 
280
+ const MAX_CODE_HIGHLIGHT_CACHE_ENTRIES = 2048;
281
+
280
282
  function createCodeLineHighlighter(language: string | undefined): CodeLineHighlighter {
281
283
  if (!language) {
282
284
  return (line) => sanitizeAnsiForThemedOutput(line);
283
285
  }
284
286
 
287
+ // Insertion-order Map used as a small LRU so large diffs cannot retain every unique line.
285
288
  const cache = new Map<string, string>();
289
+ const remember = (line: string, value: string): string => {
290
+ if (cache.has(line)) cache.delete(line);
291
+ cache.set(line, value);
292
+ if (cache.size > MAX_CODE_HIGHLIGHT_CACHE_ENTRIES) {
293
+ const oldest = cache.keys().next().value;
294
+ if (oldest !== undefined) cache.delete(oldest);
295
+ }
296
+ return value;
297
+ };
286
298
  return (line) => {
287
299
  if (!line) {
288
300
  return line;
289
301
  }
290
302
  const cached = cache.get(line);
291
303
  if (cached !== undefined) {
304
+ // Refresh recency for LRU.
305
+ cache.delete(line);
306
+ cache.set(line, cached);
292
307
  return cached;
293
308
  }
294
309
  try {
295
310
  const highlighted = highlightCode(line, language)[0] ?? line;
296
- const sanitized = sanitizeAnsiForThemedOutput(highlighted);
297
- cache.set(line, sanitized);
298
- return sanitized;
311
+ return remember(line, sanitizeAnsiForThemedOutput(highlighted));
299
312
  } catch {
300
- const sanitizedFallback = sanitizeAnsiForThemedOutput(line);
301
- cache.set(line, sanitizedFallback);
302
- return sanitizedFallback;
313
+ return remember(line, sanitizeAnsiForThemedOutput(line));
303
314
  }
304
315
  };
305
316
  }
@@ -1301,6 +1312,25 @@ function resolveDiffIndicatorMode(
1301
1312
  return config.diffIndicatorMode ?? DEFAULT_TOOL_DISPLAY_CONFIG.diffIndicatorMode;
1302
1313
  }
1303
1314
 
1315
+ /** Snapshot or live getter — panel changes must apply on the next paint. */
1316
+ export type DisplayConfigInput = ToolDisplayConfig | (() => ToolDisplayConfig);
1317
+
1318
+ function resolveLiveDisplayConfig(input: DisplayConfigInput): ToolDisplayConfig {
1319
+ return typeof input === "function" ? input() : input;
1320
+ }
1321
+
1322
+ /** Cache key fragment so indicator/wrap/limits invalidate without host recreate. */
1323
+ function displayConfigCacheKey(config: ToolDisplayConfig): string {
1324
+ return [
1325
+ config.diffViewMode,
1326
+ config.diffIndicatorMode,
1327
+ String(config.diffSplitMinWidth),
1328
+ String(config.diffCollapsedLines),
1329
+ config.diffWordWrap ? "1" : "0",
1330
+ String(config.expandedPreviewMaxLines),
1331
+ ].join(":");
1332
+ }
1333
+
1304
1334
  function resolveIndicatorGlyph(
1305
1335
  kind: DiffLineKind,
1306
1336
  indicatorMode: DiffIndicatorMode,
@@ -2148,28 +2178,96 @@ function renderDiffSpacerLine(width: number): string {
2148
2178
  return safeWidth > 0 ? " ".repeat(safeWidth) : "";
2149
2179
  }
2150
2180
 
2151
- function applyLineLimit(
2152
- rows: RenderedRow[],
2153
- width: number,
2181
+ function resolveDiffDisplayLimit(
2154
2182
  expanded: boolean,
2155
2183
  maxCollapsedLines: number,
2156
2184
  maxExpandedLines: number,
2157
- totalHunks: number,
2158
- theme: DiffTheme,
2159
- ): string[] {
2185
+ ): number {
2160
2186
  const expandedLimit = Number.isFinite(maxExpandedLines)
2161
2187
  ? maxExpandedLines
2162
2188
  : DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines;
2163
2189
  const collapsedLimit = Number.isFinite(maxCollapsedLines)
2164
2190
  ? maxCollapsedLines
2165
2191
  : DEFAULT_TOOL_DISPLAY_CONFIG.diffCollapsedLines;
2166
- const limit = expanded ? Math.max(0, expandedLimit) : Math.max(1, collapsedLimit);
2167
- if (limit === 0 || rows.length <= limit) {
2192
+ return expanded ? Math.max(0, expandedLimit) : Math.max(1, collapsedLimit);
2193
+ }
2194
+
2195
+ /**
2196
+ * How many source rows/entries to fully highlight+render before applyLineLimit.
2197
+ * Extra headroom covers word-wrap expansion so the display limit can still fill.
2198
+ */
2199
+ function resolveDiffProcessBudget(displayLimit: number, wordWrap: boolean): number {
2200
+ if (!Number.isFinite(displayLimit) || displayLimit <= 0) {
2201
+ return Number.POSITIVE_INFINITY;
2202
+ }
2203
+ return wordWrap ? displayLimit * 2 + 8 : displayLimit + 4;
2204
+ }
2205
+
2206
+ function countDiffLineEntries(entries: readonly ParsedDiffEntry[]): number {
2207
+ let count = 0;
2208
+ for (const entry of entries) {
2209
+ if (entry.kind === "line") count++;
2210
+ }
2211
+ return count;
2212
+ }
2213
+
2214
+ function takeEntriesForLineBudget(
2215
+ entries: ParsedDiffEntry[],
2216
+ maxLineEntries: number,
2217
+ ): { entries: ParsedDiffEntry[]; totalLineEntries: number; processedLineEntries: number } {
2218
+ const totalLineEntries = countDiffLineEntries(entries);
2219
+ if (!Number.isFinite(maxLineEntries) || maxLineEntries >= totalLineEntries) {
2220
+ return { entries, totalLineEntries, processedLineEntries: totalLineEntries };
2221
+ }
2222
+ const limited: ParsedDiffEntry[] = [];
2223
+ let processedLineEntries = 0;
2224
+ for (const entry of entries) {
2225
+ if (entry.kind === "line") {
2226
+ if (processedLineEntries >= maxLineEntries) break;
2227
+ processedLineEntries++;
2228
+ }
2229
+ limited.push(entry);
2230
+ }
2231
+ return { entries: limited, totalLineEntries, processedLineEntries };
2232
+ }
2233
+
2234
+ function takeSplitRowsForBudget(
2235
+ rows: SplitDiffRow[],
2236
+ maxRows: number,
2237
+ ): { rows: SplitDiffRow[]; totalRows: number; processedRows: number } {
2238
+ const totalRows = rows.length;
2239
+ if (!Number.isFinite(maxRows) || maxRows >= totalRows) {
2240
+ return { rows, totalRows, processedRows: totalRows };
2241
+ }
2242
+ const limited = rows.slice(0, maxRows);
2243
+ return { rows: limited, totalRows, processedRows: limited.length };
2244
+ }
2245
+
2246
+ function applyLineLimit(
2247
+ rows: RenderedRow[],
2248
+ width: number,
2249
+ expanded: boolean,
2250
+ maxCollapsedLines: number,
2251
+ maxExpandedLines: number,
2252
+ totalHunks: number,
2253
+ theme: DiffTheme,
2254
+ /**
2255
+ * When the caller only rendered a prefix of the full diff, pass the unprocessed
2256
+ * remainder so the collapse hint still reflects total hidden content.
2257
+ */
2258
+ unprocessedLogicalRows = 0,
2259
+ ): string[] {
2260
+ const limit = resolveDiffDisplayLimit(expanded, maxCollapsedLines, maxExpandedLines);
2261
+ const safeUnprocessed = Math.max(0, unprocessedLogicalRows);
2262
+ if (limit === 0 || (rows.length <= limit && safeUnprocessed === 0)) {
2168
2263
  return rows.map((row) => clampDiffLineToWidth(row.text, width));
2169
2264
  }
2170
2265
 
2171
- const shown = rows.slice(0, limit);
2172
- const remaining = rows.length - shown.length;
2266
+ const shown = rows.length <= limit ? rows : rows.slice(0, limit);
2267
+ const remaining = Math.max(0, rows.length - shown.length) + safeUnprocessed;
2268
+ if (remaining === 0) {
2269
+ return shown.map((row) => clampDiffLineToWidth(row.text, width));
2270
+ }
2173
2271
  const visibleHunks = new Set(
2174
2272
  shown
2175
2273
  .map((row) => row.hunkIndex)
@@ -2271,24 +2369,38 @@ function createDiffRenderCache() {
2271
2369
  let cachedWidth: number | undefined;
2272
2370
  let cachedExpanded: boolean | undefined;
2273
2371
  let cachedMode: DiffPresentationMode | undefined;
2372
+ let cachedConfigKey: string | undefined;
2274
2373
  let cachedLines: string[] | undefined;
2275
2374
 
2276
2375
  return {
2277
- get(width: number, expanded: boolean, mode: DiffPresentationMode): string[] | undefined {
2376
+ get(
2377
+ width: number,
2378
+ expanded: boolean,
2379
+ mode: DiffPresentationMode,
2380
+ configKey: string,
2381
+ ): string[] | undefined {
2278
2382
  if (
2279
2383
  cachedLines &&
2280
2384
  cachedWidth === width &&
2281
2385
  cachedExpanded === expanded &&
2282
- cachedMode === mode
2386
+ cachedMode === mode &&
2387
+ cachedConfigKey === configKey
2283
2388
  ) {
2284
2389
  return cachedLines;
2285
2390
  }
2286
2391
  return undefined;
2287
2392
  },
2288
- set(width: number, expanded: boolean, mode: DiffPresentationMode, lines: string[]): string[] {
2393
+ set(
2394
+ width: number,
2395
+ expanded: boolean,
2396
+ mode: DiffPresentationMode,
2397
+ configKey: string,
2398
+ lines: string[],
2399
+ ): string[] {
2289
2400
  cachedWidth = width;
2290
2401
  cachedExpanded = expanded;
2291
2402
  cachedMode = mode;
2403
+ cachedConfigKey = configKey;
2292
2404
  cachedLines = lines;
2293
2405
  return lines;
2294
2406
  },
@@ -2296,6 +2408,7 @@ function createDiffRenderCache() {
2296
2408
  cachedWidth = undefined;
2297
2409
  cachedExpanded = undefined;
2298
2410
  cachedMode = undefined;
2411
+ cachedConfigKey = undefined;
2299
2412
  cachedLines = undefined;
2300
2413
  },
2301
2414
  };
@@ -2304,7 +2417,7 @@ function createDiffRenderCache() {
2304
2417
  export function renderEditDiffResult(
2305
2418
  details: unknown,
2306
2419
  options: DiffRenderOptions,
2307
- config: ToolDisplayConfig,
2420
+ config: DisplayConfigInput,
2308
2421
  theme: DiffTheme,
2309
2422
  fallbackText: string,
2310
2423
  ): Component {
@@ -2339,16 +2452,19 @@ export function renderEditDiffResult(
2339
2452
  const containerBgAnsi = undefined;
2340
2453
  const language = resolveLanguageFromPath(options.filePath);
2341
2454
  const highlightLine = createCodeLineHighlighter(language);
2342
- const wordWrap = config.diffWordWrap;
2343
- const indicatorMode = resolveDiffIndicatorMode(config);
2344
2455
 
2345
2456
  const cache = createDiffRenderCache();
2346
2457
 
2347
2458
  return {
2348
2459
  render(width: number): string[] {
2460
+ // Live config: panel can change indicator/wrap/limits after this component is created.
2461
+ const live = resolveLiveDisplayConfig(config);
2462
+ const wordWrap = live.diffWordWrap;
2463
+ const indicatorMode = resolveDiffIndicatorMode(live);
2464
+ const configKey = displayConfigCacheKey(live);
2349
2465
  const safeWidth = normalizeDiffRenderWidth(width);
2350
- const mode = resolveDiffPresentationMode(config, safeWidth, canRenderSplitLayout(safeWidth));
2351
- const cached = cache.get(safeWidth, options.expanded, mode);
2466
+ const mode = resolveDiffPresentationMode(live, safeWidth, canRenderSplitLayout(safeWidth));
2467
+ const cached = cache.get(safeWidth, options.expanded, mode, configKey);
2352
2468
  if (cached) {
2353
2469
  return cached;
2354
2470
  }
@@ -2358,6 +2474,7 @@ export function renderEditDiffResult(
2358
2474
  safeWidth,
2359
2475
  options.expanded,
2360
2476
  mode,
2477
+ configKey,
2361
2478
  clampDiffLinesToWidth(
2362
2479
  renderSingleDiffRow(
2363
2480
  buildDiffSummaryText(parsed.stats, safeWidth),
@@ -2371,59 +2488,47 @@ export function renderEditDiffResult(
2371
2488
  }
2372
2489
 
2373
2490
  const headerRows = renderHeaderRows(parsed.stats, mode, safeWidth, theme);
2374
- const inlineHighlights = buildInlineHighlightMap(splitRows);
2491
+ const displayLimit = resolveDiffDisplayLimit(
2492
+ options.expanded,
2493
+ live.diffCollapsedLines,
2494
+ live.expandedPreviewMaxLines,
2495
+ );
2496
+ const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
2497
+ // Only highlight/render a prefix that can fill the display limit; full-diff
2498
+ // LCS + syntax highlight on thousands of hidden lines is pure waste when collapsed.
2499
+ const entryBudget = takeEntriesForLineBudget(parsed.entries, processBudget);
2500
+ const splitBudget = takeSplitRowsForBudget(splitRows, processBudget);
2501
+ const inlineHighlights = buildInlineHighlightMap(splitBudget.rows);
2502
+ const renderCtx: DiffRenderContext = {
2503
+ width: safeWidth,
2504
+ theme,
2505
+ inlineHighlights,
2506
+ palette,
2507
+ highlightLine,
2508
+ containerBgAnsi,
2509
+ wordWrap,
2510
+ indicatorMode,
2511
+ showHashlineAnchors,
2512
+ };
2375
2513
  const bodyRows =
2376
2514
  mode === "split"
2377
- ? renderSplit(
2378
- splitRows,
2379
- {
2380
- width: safeWidth,
2381
- theme,
2382
- inlineHighlights,
2383
- palette,
2384
- highlightLine,
2385
- containerBgAnsi,
2386
- wordWrap,
2387
- indicatorMode,
2388
- showHashlineAnchors,
2389
- },
2390
- lineNumberWidth,
2391
- )
2515
+ ? renderSplit(splitBudget.rows, renderCtx, lineNumberWidth)
2392
2516
  : mode === "compact"
2393
- ? renderCompact(parsed.entries, {
2394
- width: safeWidth,
2395
- theme,
2396
- inlineHighlights,
2397
- palette,
2398
- highlightLine,
2399
- containerBgAnsi,
2400
- wordWrap,
2401
- indicatorMode,
2402
- showHashlineAnchors,
2403
- })
2404
- : renderUnified(
2405
- parsed.entries,
2406
- {
2407
- width: safeWidth,
2408
- theme,
2409
- inlineHighlights,
2410
- palette,
2411
- highlightLine,
2412
- containerBgAnsi,
2413
- wordWrap,
2414
- indicatorMode,
2415
- showHashlineAnchors,
2416
- },
2417
- lineNumberWidth,
2418
- );
2517
+ ? renderCompact(entryBudget.entries, renderCtx)
2518
+ : renderUnified(entryBudget.entries, renderCtx, lineNumberWidth);
2519
+ const unprocessedLogicalRows =
2520
+ mode === "split"
2521
+ ? Math.max(0, splitBudget.totalRows - splitBudget.processedRows)
2522
+ : Math.max(0, entryBudget.totalLineEntries - entryBudget.processedLineEntries);
2419
2523
  const bodyWithLimit = applyLineLimit(
2420
2524
  bodyRows,
2421
2525
  safeWidth,
2422
2526
  options.expanded,
2423
- config.diffCollapsedLines,
2424
- config.expandedPreviewMaxLines,
2527
+ live.diffCollapsedLines,
2528
+ live.expandedPreviewMaxLines,
2425
2529
  parsed.stats.hunks,
2426
2530
  theme,
2531
+ unprocessedLogicalRows,
2427
2532
  );
2428
2533
  const frame = renderDiffFrameLine(safeWidth, theme);
2429
2534
  const renderedLines =
@@ -2432,7 +2537,7 @@ export function renderEditDiffResult(
2432
2537
  : [...headerRows.map((row) => row.text), ...bodyWithLimit];
2433
2538
 
2434
2539
  const clampedLines = clampDiffLinesToWidth(renderedLines, safeWidth);
2435
- return cache.set(safeWidth, options.expanded, mode, clampedLines);
2540
+ return cache.set(safeWidth, options.expanded, mode, configKey, clampedLines);
2436
2541
  },
2437
2542
  invalidate: cache.invalidate,
2438
2543
  };
@@ -2593,6 +2698,8 @@ interface WriteOverwriteGuard {
2593
2698
 
2594
2699
  const MAX_WRITE_OVERWRITE_DIFF_LINES = 4000;
2595
2700
  const MAX_WRITE_OVERWRITE_DIFF_MATRIX_CELLS = 1_000_000;
2701
+ /** Collapsed overwrite previews refuse larger LCS matrices to avoid TUI freezes. */
2702
+ const MAX_COLLAPSED_WRITE_OVERWRITE_DIFF_MATRIX_CELLS = 200_000;
2596
2703
 
2597
2704
  function buildApproximateWriteStats(
2598
2705
  lineCount: number,
@@ -2631,6 +2738,7 @@ function buildWriteDiffData(entries: ParsedDiffEntry[]): WriteDiffData {
2631
2738
  function resolveWriteOverwriteGuard(
2632
2739
  previousLines: string[],
2633
2740
  nextLines: string[],
2741
+ expanded = true,
2634
2742
  ): WriteOverwriteGuard | undefined {
2635
2743
  const previousLineCount = previousLines.length;
2636
2744
  const nextLineCount = nextLines.length;
@@ -2643,7 +2751,10 @@ function resolveWriteOverwriteGuard(
2643
2751
  if (previousLineCount === 0 || nextLineCount === 0) {
2644
2752
  return undefined;
2645
2753
  }
2646
- return previousLineCount * nextLineCount > MAX_WRITE_OVERWRITE_DIFF_MATRIX_CELLS
2754
+ const cellLimit = expanded
2755
+ ? MAX_WRITE_OVERWRITE_DIFF_MATRIX_CELLS
2756
+ : MAX_COLLAPSED_WRITE_OVERWRITE_DIFF_MATRIX_CELLS;
2757
+ return previousLineCount * nextLineCount > cellLimit
2647
2758
  ? { previousLineCount, nextLineCount }
2648
2759
  : undefined;
2649
2760
  }
@@ -2680,7 +2791,7 @@ function renderWriteOverwriteGuardRows(
2680
2791
  export function renderWriteDiffResult(
2681
2792
  content: string | undefined,
2682
2793
  options: DiffRenderOptions,
2683
- config: ToolDisplayConfig,
2794
+ config: DisplayConfigInput,
2684
2795
  theme: DiffTheme,
2685
2796
  fallbackText: string,
2686
2797
  ): Component {
@@ -2704,16 +2815,11 @@ export function renderWriteDiffResult(
2704
2815
  previousLines.length,
2705
2816
  hasComparablePrevious,
2706
2817
  );
2707
- const overwriteGuard = hasComparablePrevious
2708
- ? resolveWriteOverwriteGuard(previousLines, lines)
2709
- : undefined;
2710
2818
  const palette = resolveDiffPalette(theme);
2711
2819
  // Keep the panel transparent; changed rows and inline spans retain their own highlights.
2712
2820
  const containerBgAnsi = undefined;
2713
2821
  const language = resolveLanguageFromPath(filePath);
2714
2822
  const highlightLine = createCodeLineHighlighter(language);
2715
- const wordWrap = config.diffWordWrap;
2716
- const indicatorMode = resolveDiffIndicatorMode(config);
2717
2823
 
2718
2824
  let detailedData: WriteDiffData | undefined;
2719
2825
  const cache = createDiffRenderCache();
@@ -2731,9 +2837,14 @@ export function renderWriteDiffResult(
2731
2837
 
2732
2838
  return {
2733
2839
  render(width: number): string[] {
2840
+ // Live config: panel can change indicator/wrap/limits after this component is created.
2841
+ const live = resolveLiveDisplayConfig(config);
2842
+ const wordWrap = live.diffWordWrap;
2843
+ const indicatorMode = resolveDiffIndicatorMode(live);
2844
+ const configKey = displayConfigCacheKey(live);
2734
2845
  const safeWidth = normalizeDiffRenderWidth(width);
2735
2846
  const resolvedMode = resolveDiffPresentationMode(
2736
- config,
2847
+ live,
2737
2848
  safeWidth,
2738
2849
  canRenderSplitLayout(safeWidth),
2739
2850
  );
@@ -2742,7 +2853,7 @@ export function renderWriteDiffResult(
2742
2853
  : resolvedMode === "split"
2743
2854
  ? "unified"
2744
2855
  : resolvedMode;
2745
- const cached = cache.get(safeWidth, options.expanded, mode);
2856
+ const cached = cache.get(safeWidth, options.expanded, mode, configKey);
2746
2857
  if (cached) {
2747
2858
  return cached;
2748
2859
  }
@@ -2753,11 +2864,16 @@ export function renderWriteDiffResult(
2753
2864
  theme,
2754
2865
  options.headerLabel,
2755
2866
  );
2867
+ // Guard depends on expanded: collapsed previews use a stricter LCS cell budget.
2868
+ const overwriteGuard = hasComparablePrevious
2869
+ ? resolveWriteOverwriteGuard(previousLines, lines, options.expanded)
2870
+ : undefined;
2756
2871
  if (overwriteGuard) {
2757
2872
  return cache.set(
2758
2873
  safeWidth,
2759
2874
  options.expanded,
2760
2875
  mode,
2876
+ configKey,
2761
2877
  clampDiffLinesToWidth(
2762
2878
  [header, ...renderWriteOverwriteGuardRows(overwriteGuard, safeWidth, theme)],
2763
2879
  safeWidth,
@@ -2782,15 +2898,28 @@ export function renderWriteDiffResult(
2782
2898
  safeWidth,
2783
2899
  options.expanded,
2784
2900
  mode,
2901
+ configKey,
2785
2902
  clampDiffLinesToWidth(summaryRows, safeWidth),
2786
2903
  );
2787
2904
  }
2788
2905
 
2789
2906
  const data = getDetailedData();
2907
+ const displayLimit = resolveDiffDisplayLimit(
2908
+ options.expanded,
2909
+ live.diffCollapsedLines,
2910
+ live.expandedPreviewMaxLines,
2911
+ );
2912
+ const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
2913
+ const entryBudget = takeEntriesForLineBudget(data.entries, processBudget);
2914
+ const splitBudget = takeSplitRowsForBudget(data.splitRows, processBudget);
2915
+ const inlineHighlights =
2916
+ splitBudget.processedRows < splitBudget.totalRows
2917
+ ? buildInlineHighlightMap(splitBudget.rows)
2918
+ : data.inlineHighlights;
2790
2919
  const renderCtx: DiffRenderContext = {
2791
2920
  width: safeWidth,
2792
2921
  theme,
2793
- inlineHighlights: data.inlineHighlights,
2922
+ inlineHighlights,
2794
2923
  palette,
2795
2924
  highlightLine,
2796
2925
  containerBgAnsi,
@@ -2799,28 +2928,33 @@ export function renderWriteDiffResult(
2799
2928
  showHashlineAnchors: false,
2800
2929
  };
2801
2930
  const bodyRows: RenderedRow[] =
2802
- data.entries.length === 0
2931
+ entryBudget.entries.length === 0
2803
2932
  ? [{ text: theme.fg("muted", "(empty file)"), hunkIndex: null }]
2804
2933
  : mode === "split"
2805
- ? renderSplit(data.splitRows, renderCtx, data.lineNumberWidth)
2934
+ ? renderSplit(splitBudget.rows, renderCtx, data.lineNumberWidth)
2806
2935
  : mode === "compact"
2807
- ? renderCompact(data.entries, renderCtx)
2808
- : renderUnified(data.entries, renderCtx, data.lineNumberWidth);
2936
+ ? renderCompact(entryBudget.entries, renderCtx)
2937
+ : renderUnified(entryBudget.entries, renderCtx, data.lineNumberWidth);
2938
+ const unprocessedLogicalRows =
2939
+ mode === "split"
2940
+ ? Math.max(0, splitBudget.totalRows - splitBudget.processedRows)
2941
+ : Math.max(0, entryBudget.totalLineEntries - entryBudget.processedLineEntries);
2809
2942
 
2810
2943
  const bodyWithLimit = applyLineLimit(
2811
2944
  bodyRows,
2812
2945
  safeWidth,
2813
2946
  options.expanded,
2814
- config.diffCollapsedLines,
2815
- config.expandedPreviewMaxLines,
2947
+ live.diffCollapsedLines,
2948
+ live.expandedPreviewMaxLines,
2816
2949
  data.hunkCount,
2817
2950
  theme,
2951
+ unprocessedLogicalRows,
2818
2952
  );
2819
2953
  const frame = renderDiffFrameLine(safeWidth, theme);
2820
2954
  const renderedLines =
2821
2955
  mode === "unified" ? [header, frame, ...bodyWithLimit, frame] : [header, ...bodyWithLimit];
2822
2956
  const finalLines = clampDiffLinesToWidth(renderedLines, safeWidth);
2823
- return cache.set(safeWidth, options.expanded, mode, finalLines);
2957
+ return cache.set(safeWidth, options.expanded, mode, configKey, finalLines);
2824
2958
  },
2825
2959
  invalidate: cache.invalidate,
2826
2960
  };
@@ -1,6 +1,10 @@
1
1
  import { createWriteToolDefinition, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { truncateToWidth } from "@earendil-works/pi-tui";
3
- import { renderEditDiffResult, renderWriteDiffResult } from "./diff-renderer.ts";
3
+ import {
4
+ renderEditDiffResult,
5
+ renderWriteDiffResult,
6
+ type DisplayConfigInput,
7
+ } from "./diff-renderer.ts";
4
8
  import { DEFAULT_TOOL_DISPLAY_CONFIG } from "./types.ts";
5
9
  import { executeWriteWithMetadata, WriteExecutionMetadataStore } from "./write-execution.ts";
6
10
 
@@ -34,6 +38,8 @@ export function renderRichToolResult(
34
38
  theme: any,
35
39
  context: any,
36
40
  writeMetadata: WriteExecutionMetadataStore,
41
+ /** Plain snapshot or live getter — getter lets /ccstyle panel changes repaint existing diffs. */
42
+ displayConfig: DisplayConfigInput = DEFAULT_TOOL_DISPLAY_CONFIG,
37
43
  ): any | undefined {
38
44
  if (options?.isPartial || options?.isError || context?.isError) return undefined;
39
45
  const expanded = options?.expanded === true || context?.expanded === true;
@@ -42,7 +48,7 @@ export function renderRichToolResult(
42
48
  return renderEditDiffResult(
43
49
  result?.details,
44
50
  { expanded, filePath },
45
- DEFAULT_TOOL_DISPLAY_CONFIG,
51
+ displayConfig,
46
52
  theme,
47
53
  resultText(result),
48
54
  );
@@ -64,7 +70,7 @@ export function renderRichToolResult(
64
70
  previousContent: metadata.previousContent,
65
71
  fileExistedBeforeWrite: metadata.fileExistedBeforeWrite,
66
72
  },
67
- DEFAULT_TOOL_DISPLAY_CONFIG,
73
+ displayConfig,
68
74
  theme,
69
75
  resultText(result),
70
76
  );
@@ -103,4 +109,11 @@ export function installWriteOverride(
103
109
  return store;
104
110
  }
105
111
 
106
- export { DEFAULT_TOOL_DISPLAY_CONFIG, WriteExecutionMetadataStore };
112
+ export {
113
+ DEFAULT_TOOL_DISPLAY_CONFIG,
114
+ type ToolDisplayConfig,
115
+ type DiffViewMode,
116
+ type DiffIndicatorMode,
117
+ } from "./types.ts";
118
+ export type { DisplayConfigInput } from "./diff-renderer.ts";
119
+ export { WriteExecutionMetadataStore } from "./write-execution.ts";
@@ -14,7 +14,13 @@ export const DEFAULT_TOOL_DISPLAY_CONFIG: ToolDisplayConfig = {
14
14
  diffViewMode: "auto",
15
15
  diffIndicatorMode: "bars",
16
16
  diffSplitMinWidth: 120,
17
+ /** Collapsed tool/diff body: ~half a typical terminal after chrome. */
17
18
  diffCollapsedLines: 24,
18
19
  diffWordWrap: true,
19
- expandedPreviewMaxLines: 4000,
20
+ /**
21
+ * Expanded tool/diff body cap. 40 ≈ one screen of content after title,
22
+ * Input section, editor, and status — keeps the TUI compact.
23
+ * Raise via /ccstyle → Diff → Expanded max lines when reviewing large dumps.
24
+ */
25
+ expandedPreviewMaxLines: 40,
20
26
  };
@@ -6,9 +6,16 @@ const UNSAFE_TERMINAL_ESCAPE = new RegExp(
6
6
  "g",
7
7
  );
8
8
 
9
- /** Prevent captured terminal control responses from being replayed by tool renderers. */
10
- export function sanitizeToolResultText(value: string): string {
11
- return value
9
+ /**
10
+ * Prevent captured terminal control responses from being replayed by tool renderers.
11
+ * Optional `maxChars` trims the input first so preview/one-line paths never scan multi-MB outputs.
12
+ */
13
+ export function sanitizeToolResultText(value: string, maxChars?: number): string {
14
+ const source =
15
+ typeof maxChars === "number" && maxChars >= 0 && value.length > maxChars
16
+ ? value.slice(0, maxChars)
17
+ : value;
18
+ return source
12
19
  .replace(UNSAFE_TERMINAL_ESCAPE, "")
13
20
  .replace(/\x1B/g, "")
14
21
  .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, "")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.13",
3
+ "version": "0.8.15",
4
4
  "description": "A Pi productivity suite with Claude Code-style UI, structured questionnaires, context inspection, and agent/session references",
5
5
  "type": "module",
6
6
  "keywords": [