hunkdiff 0.12.0-beta.0 → 0.12.0-beta.1

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.
@@ -0,0 +1,2114 @@
1
+ // src/opentui/index.ts
2
+ import { parseDiffFromFile, parsePatchFiles as parsePatchFiles2 } from "@pierre/diffs";
3
+
4
+ // src/opentui/themes.ts
5
+ var HUNK_DIFF_THEME_NAMES = ["graphite", "midnight", "paper", "ember"];
6
+ // src/opentui/HunkDiffBody.tsx
7
+ import { useMemo } from "react";
8
+
9
+ // src/ui/diff/codeColumns.ts
10
+ var DIFF_CODE_TAB_WIDTH = 2;
11
+ var DIFF_RAIL_PREFIX_WIDTH = 1;
12
+ var DIFF_SPLIT_SEPARATOR_WIDTH = 1;
13
+ function expandDiffTabs(text) {
14
+ return text.replaceAll("\t", " ".repeat(DIFF_CODE_TAB_WIDTH));
15
+ }
16
+ function findMaxLineNumber(file) {
17
+ let highest = 0;
18
+ for (const hunk of file.metadata.hunks) {
19
+ highest = Math.max(highest, hunk.deletionStart + hunk.deletionCount, hunk.additionStart + hunk.additionCount);
20
+ }
21
+ return Math.max(highest, 1);
22
+ }
23
+ function resolveSplitPaneWidths(width) {
24
+ const usableWidth = Math.max(0, width - DIFF_RAIL_PREFIX_WIDTH - DIFF_SPLIT_SEPARATOR_WIDTH);
25
+ const leftWidth = Math.max(0, DIFF_RAIL_PREFIX_WIDTH + Math.floor(usableWidth / 2));
26
+ const rightWidth = Math.max(0, DIFF_SPLIT_SEPARATOR_WIDTH + usableWidth - Math.floor(usableWidth / 2));
27
+ return { leftWidth, rightWidth };
28
+ }
29
+ function resolveSplitCellGeometry(width, lineNumberDigits, showLineNumbers, prefixWidth = DIFF_RAIL_PREFIX_WIDTH) {
30
+ const availableWidth = Math.max(0, width - prefixWidth);
31
+ const gutterWidth = Math.min(availableWidth, showLineNumbers ? lineNumberDigits + 3 : 2);
32
+ return {
33
+ gutterWidth,
34
+ contentWidth: Math.max(0, availableWidth - gutterWidth)
35
+ };
36
+ }
37
+ function resolveStackCellGeometry(width, lineNumberDigits, showLineNumbers, prefixWidth = DIFF_RAIL_PREFIX_WIDTH) {
38
+ const availableWidth = Math.max(0, width - prefixWidth);
39
+ const gutterWidth = Math.min(availableWidth, showLineNumbers ? lineNumberDigits * 2 + 5 : 2);
40
+ return {
41
+ gutterWidth,
42
+ contentWidth: Math.max(0, availableWidth - gutterWidth)
43
+ };
44
+ }
45
+
46
+ // src/ui/diff/pierre.ts
47
+ import {
48
+ cleanLastNewline,
49
+ getHighlighterOptions,
50
+ getSharedHighlighter,
51
+ renderDiffWithHighlighter
52
+ } from "@pierre/diffs";
53
+
54
+ // src/core/hunkHeader.ts
55
+ function formatHunkHeader(hunk) {
56
+ const specs = hunk.hunkSpecs ?? `@@ -${hunk.deletionStart},${hunk.deletionLines} +${hunk.additionStart},${hunk.additionLines} @@`;
57
+ return hunk.hunkContext ? `${specs} ${hunk.hunkContext}` : specs;
58
+ }
59
+
60
+ // src/ui/lib/color.ts
61
+ function hexToRgb(hex) {
62
+ const normalized = /^#?[0-9a-f]{6}$/i.test(hex) ? hex.replace(/^#/, "") : "000000";
63
+ const value = parseInt(normalized, 16);
64
+ return {
65
+ r: value >> 16 & 255,
66
+ g: value >> 8 & 255,
67
+ b: value & 255
68
+ };
69
+ }
70
+ function blendHex(fg, bg, ratio) {
71
+ const foreground = hexToRgb(fg);
72
+ const background = hexToRgb(bg);
73
+ const mix = (front, back) => Math.max(0, Math.min(255, Math.round(back + (front - back) * ratio)));
74
+ return `#${(mix(foreground.r, background.r) << 16 | mix(foreground.g, background.g) << 8 | mix(foreground.b, background.b)).toString(16).padStart(6, "0")}`;
75
+ }
76
+ function hexColorDistance(left, right) {
77
+ const a = hexToRgb(left);
78
+ const b = hexToRgb(right);
79
+ return Math.abs(a.r - b.r) + Math.abs(a.g - b.g) + Math.abs(a.b - b.b);
80
+ }
81
+
82
+ // src/ui/diff/pierre.ts
83
+ var PIERRE_THEME = {
84
+ light: "pierre-light",
85
+ dark: "pierre-dark"
86
+ };
87
+ function pierreThemeName(appearance) {
88
+ return PIERRE_THEME[appearance];
89
+ }
90
+ var PIERRE_RENDER_OPTIONS_BY_APPEARANCE = {
91
+ light: {
92
+ theme: pierreThemeName("light"),
93
+ useTokenTransformer: false,
94
+ tokenizeMaxLineLength: 1000,
95
+ lineDiffType: "word-alt",
96
+ maxLineDiffLength: 1e4
97
+ },
98
+ dark: {
99
+ theme: pierreThemeName("dark"),
100
+ useTokenTransformer: false,
101
+ tokenizeMaxLineLength: 1000,
102
+ lineDiffType: "word-alt",
103
+ maxLineDiffLength: 1e4
104
+ }
105
+ };
106
+ function pierreRenderOptions(appearance) {
107
+ return PIERRE_RENDER_OPTIONS_BY_APPEARANCE[appearance];
108
+ }
109
+ var highlighterOptionsByKey = new Map;
110
+ var queuedHighlightWork = Promise.resolve();
111
+ function tabify(text) {
112
+ return expandDiffTabs(text);
113
+ }
114
+ var EMPTY_STYLE_VALUES = new Map;
115
+ var parsedStyleValueCache = new Map;
116
+ function parseStyleValue(styleValue) {
117
+ if (typeof styleValue !== "string") {
118
+ return EMPTY_STYLE_VALUES;
119
+ }
120
+ const cached = parsedStyleValueCache.get(styleValue);
121
+ if (cached) {
122
+ return cached;
123
+ }
124
+ const styles = new Map;
125
+ for (const segment of styleValue.split(";")) {
126
+ const separator = segment.indexOf(":");
127
+ if (separator <= 0) {
128
+ continue;
129
+ }
130
+ const key = segment.slice(0, separator).trim();
131
+ const value = segment.slice(separator + 1).trim();
132
+ if (key && value) {
133
+ styles.set(key, value);
134
+ }
135
+ }
136
+ parsedStyleValueCache.set(styleValue, styles);
137
+ return styles;
138
+ }
139
+ var RESERVED_PIERRE_TOKEN_COLORS = {
140
+ dark: {
141
+ "#ff6762": "keyword",
142
+ "#5ecc71": "string"
143
+ },
144
+ light: {
145
+ "#d52c36": "keyword",
146
+ "#199f43": "string"
147
+ }
148
+ };
149
+ var normalizedColorCache = new Map;
150
+ var flattenedHighlightedLineCache = new WeakMap;
151
+ var MIN_WORD_DIFF_BG_DISTANCE = 28;
152
+ var WORD_DIFF_BLEND_STEP = 0.005;
153
+ var WORD_DIFF_MAX_BLEND = 0.2;
154
+ var wordDiffBackgroundCache = new Map;
155
+ function strengthenWordDiffBg(lineBg, signColor) {
156
+ let strongestCandidate = lineBg;
157
+ const maxSteps = Math.floor(WORD_DIFF_MAX_BLEND / WORD_DIFF_BLEND_STEP);
158
+ for (let step = 1;step <= maxSteps; step += 1) {
159
+ const blendRatio = step * WORD_DIFF_BLEND_STEP;
160
+ const candidate = blendHex(signColor, lineBg, blendRatio);
161
+ strongestCandidate = candidate;
162
+ if (hexColorDistance(candidate, lineBg) >= MIN_WORD_DIFF_BG_DISTANCE) {
163
+ return candidate;
164
+ }
165
+ }
166
+ return strongestCandidate;
167
+ }
168
+ function wordDiffHighlightBg(kind, theme) {
169
+ let cached = wordDiffBackgroundCache.get(theme.id);
170
+ if (!cached) {
171
+ const addition = hexColorDistance(theme.addedContentBg, theme.addedBg) >= MIN_WORD_DIFF_BG_DISTANCE ? theme.addedContentBg : strengthenWordDiffBg(theme.addedBg, theme.addedSignColor);
172
+ const deletion = hexColorDistance(theme.removedContentBg, theme.removedBg) >= MIN_WORD_DIFF_BG_DISTANCE ? theme.removedContentBg : strengthenWordDiffBg(theme.removedBg, theme.removedSignColor);
173
+ cached = {
174
+ addition,
175
+ context: theme.contextContentBg,
176
+ deletion,
177
+ empty: theme.panelAlt
178
+ };
179
+ wordDiffBackgroundCache.set(theme.id, cached);
180
+ }
181
+ return cached[kind];
182
+ }
183
+ function normalizeHighlightedColor(color, theme) {
184
+ if (!color) {
185
+ return color;
186
+ }
187
+ let cacheForTheme = normalizedColorCache.get(theme.id);
188
+ if (!cacheForTheme) {
189
+ cacheForTheme = new Map;
190
+ normalizedColorCache.set(theme.id, cacheForTheme);
191
+ }
192
+ const cached = cacheForTheme.get(color);
193
+ if (cached) {
194
+ return cached;
195
+ }
196
+ const normalized = color.trim().toLowerCase();
197
+ const reserved = RESERVED_PIERRE_TOKEN_COLORS[theme.appearance][normalized];
198
+ const resolvedColor = reserved ? theme.syntaxColors[reserved] : color;
199
+ cacheForTheme.set(color, resolvedColor);
200
+ return resolvedColor;
201
+ }
202
+ function mergeSpan(target, next) {
203
+ if (next.text.length === 0) {
204
+ return;
205
+ }
206
+ const previous = target[target.length - 1];
207
+ if (previous && previous.fg === next.fg && previous.bg === next.bg) {
208
+ previous.text += next.text;
209
+ return;
210
+ }
211
+ target.push(next);
212
+ }
213
+ function flattenHighlightedLine(node, theme, emphasisBg) {
214
+ if (!node) {
215
+ return [];
216
+ }
217
+ const cacheKey = `${theme.id}:${emphasisBg}`;
218
+ const cachedByTheme = flattenedHighlightedLineCache.get(node);
219
+ const cached = cachedByTheme?.get(cacheKey);
220
+ if (cached) {
221
+ return cached;
222
+ }
223
+ const spans = [];
224
+ const colorVariable = theme.appearance === "light" ? "--diffs-token-light" : "--diffs-token-dark";
225
+ const visit = (current, inherited) => {
226
+ if (!current) {
227
+ return;
228
+ }
229
+ if (current.type === "text") {
230
+ mergeSpan(spans, {
231
+ text: tabify(cleanLastNewline(current.value)),
232
+ fg: inherited.fg,
233
+ bg: inherited.bg
234
+ });
235
+ return;
236
+ }
237
+ const properties = current.properties ?? {};
238
+ const styles = parseStyleValue(properties.style);
239
+ const nextStyle = {
240
+ fg: normalizeHighlightedColor(styles.get(colorVariable) ?? styles.get("color") ?? inherited.fg, theme),
241
+ bg: Object.hasOwn(properties, "data-diff-span") ? emphasisBg : inherited.bg
242
+ };
243
+ for (const child of current.children ?? []) {
244
+ visit(child, nextStyle);
245
+ }
246
+ };
247
+ visit(node, {});
248
+ const nextCachedByTheme = cachedByTheme ?? new Map;
249
+ nextCachedByTheme.set(cacheKey, spans);
250
+ if (!cachedByTheme) {
251
+ flattenedHighlightedLineCache.set(node, nextCachedByTheme);
252
+ }
253
+ return spans;
254
+ }
255
+ function cleanDiffLine(line) {
256
+ return tabify(cleanLastNewline(line ?? ""));
257
+ }
258
+ function makeSplitCell(kind, lineNumber, rawLine, highlightedLine, theme) {
259
+ if (kind === "empty") {
260
+ return {
261
+ kind,
262
+ sign: " ",
263
+ spans: []
264
+ };
265
+ }
266
+ let spans;
267
+ if (highlightedLine === undefined) {
268
+ const fallbackText = cleanDiffLine(rawLine);
269
+ spans = fallbackText.length > 0 ? [{ text: fallbackText }] : [];
270
+ } else {
271
+ spans = flattenHighlightedLine(highlightedLine, theme, wordDiffHighlightBg(kind, theme));
272
+ if (spans.length === 0) {
273
+ const fallbackText = cleanDiffLine(rawLine);
274
+ spans = fallbackText.length > 0 ? [{ text: fallbackText }] : [];
275
+ }
276
+ }
277
+ return {
278
+ kind,
279
+ sign: kind === "addition" ? "+" : kind === "deletion" ? "-" : " ",
280
+ lineNumber,
281
+ spans
282
+ };
283
+ }
284
+ function makeStackCell(kind, oldLineNumber, newLineNumber, rawLine, highlightedLine, theme) {
285
+ let spans;
286
+ if (highlightedLine === undefined) {
287
+ const fallbackText = cleanDiffLine(rawLine);
288
+ spans = fallbackText.length > 0 ? [{ text: fallbackText }] : [];
289
+ } else {
290
+ spans = flattenHighlightedLine(highlightedLine, theme, wordDiffHighlightBg(kind, theme));
291
+ if (spans.length === 0) {
292
+ const fallbackText = cleanDiffLine(rawLine);
293
+ spans = fallbackText.length > 0 ? [{ text: fallbackText }] : [];
294
+ }
295
+ }
296
+ return {
297
+ kind,
298
+ sign: kind === "addition" ? "+" : kind === "deletion" ? "-" : " ",
299
+ oldLineNumber,
300
+ newLineNumber,
301
+ spans
302
+ };
303
+ }
304
+ function collapsedRowText(lines) {
305
+ return `${lines} unchanged ${lines === 1 ? "line" : "lines"}`;
306
+ }
307
+ function trailingCollapsedLines(metadata) {
308
+ const lastHunk = metadata.hunks.at(-1);
309
+ if (!lastHunk || metadata.isPartial) {
310
+ return 0;
311
+ }
312
+ const additionRemaining = metadata.additionLines.length - (lastHunk.additionLineIndex + lastHunk.additionCount);
313
+ const deletionRemaining = metadata.deletionLines.length - (lastHunk.deletionLineIndex + lastHunk.deletionCount);
314
+ if (additionRemaining !== deletionRemaining) {
315
+ return 0;
316
+ }
317
+ return Math.max(additionRemaining, 0);
318
+ }
319
+ async function prepareHighlighter(language, appearance) {
320
+ const resolvedLanguage = language ?? "text";
321
+ const cacheKey = `${appearance}:${resolvedLanguage}`;
322
+ const options = highlighterOptionsByKey.get(cacheKey) ?? getHighlighterOptions(resolvedLanguage, {
323
+ theme: pierreThemeName(appearance)
324
+ });
325
+ if (!highlighterOptionsByKey.has(cacheKey)) {
326
+ highlighterOptionsByKey.set(cacheKey, options);
327
+ }
328
+ return getSharedHighlighter({
329
+ ...options,
330
+ preferredHighlighter: "shiki-wasm"
331
+ });
332
+ }
333
+ function queueHighlightedDiff(run) {
334
+ const queued = queuedHighlightWork.then(() => new Promise((resolve, reject) => {
335
+ queueMicrotask(() => {
336
+ try {
337
+ resolve(run());
338
+ } catch (error) {
339
+ reject(error);
340
+ }
341
+ });
342
+ }));
343
+ queuedHighlightWork = queued.then(() => {
344
+ return;
345
+ }, () => {
346
+ return;
347
+ });
348
+ return queued;
349
+ }
350
+ function aliasHighlightedContextLines(file, highlighted) {
351
+ for (const hunk of file.metadata.hunks) {
352
+ let deletionLineIndex = hunk.deletionLineIndex;
353
+ let additionLineIndex = hunk.additionLineIndex;
354
+ for (const content of hunk.hunkContent) {
355
+ if (content.type === "context") {
356
+ for (let offset = 0;offset < content.lines; offset += 1) {
357
+ const sharedLine = highlighted.additionLines[additionLineIndex + offset] ?? highlighted.deletionLines[deletionLineIndex + offset];
358
+ if (!sharedLine) {
359
+ continue;
360
+ }
361
+ highlighted.deletionLines[deletionLineIndex + offset] = sharedLine;
362
+ highlighted.additionLines[additionLineIndex + offset] = sharedLine;
363
+ }
364
+ deletionLineIndex += content.lines;
365
+ additionLineIndex += content.lines;
366
+ continue;
367
+ }
368
+ deletionLineIndex += content.deletions;
369
+ additionLineIndex += content.additions;
370
+ }
371
+ }
372
+ return highlighted;
373
+ }
374
+ async function loadHighlightedDiff(file, appearance = "dark") {
375
+ try {
376
+ const highlighter = await prepareHighlighter(file.language, appearance);
377
+ return queueHighlightedDiff(() => {
378
+ const highlighted = renderDiffWithHighlighter(file.metadata, highlighter, pierreRenderOptions(appearance));
379
+ return aliasHighlightedContextLines(file, {
380
+ deletionLines: highlighted.code.deletionLines,
381
+ additionLines: highlighted.code.additionLines
382
+ });
383
+ });
384
+ } catch {
385
+ const highlighter = await prepareHighlighter("text", appearance);
386
+ return queueHighlightedDiff(() => {
387
+ const highlighted = renderDiffWithHighlighter({ ...file.metadata, lang: "text" }, highlighter, pierreRenderOptions(appearance));
388
+ return aliasHighlightedContextLines(file, {
389
+ deletionLines: highlighted.code.deletionLines,
390
+ additionLines: highlighted.code.additionLines
391
+ });
392
+ });
393
+ }
394
+ }
395
+ function buildSplitRows(file, highlighted, theme) {
396
+ const rows = [];
397
+ const deletionLines = highlighted?.deletionLines ?? [];
398
+ const additionLines = highlighted?.additionLines ?? [];
399
+ for (const [hunkIndex, hunk] of file.metadata.hunks.entries()) {
400
+ if (hunk.collapsedBefore > 0) {
401
+ rows.push({
402
+ type: "collapsed",
403
+ key: `${file.id}:collapsed:${hunkIndex}`,
404
+ fileId: file.id,
405
+ hunkIndex,
406
+ text: collapsedRowText(hunk.collapsedBefore)
407
+ });
408
+ }
409
+ rows.push({
410
+ type: "hunk-header",
411
+ key: `${file.id}:header:${hunkIndex}`,
412
+ fileId: file.id,
413
+ hunkIndex,
414
+ text: formatHunkHeader(hunk)
415
+ });
416
+ let deletionLineIndex = hunk.deletionLineIndex;
417
+ let additionLineIndex = hunk.additionLineIndex;
418
+ let deletionLineNumber = hunk.deletionStart;
419
+ let additionLineNumber = hunk.additionStart;
420
+ for (const content of hunk.hunkContent) {
421
+ if (content.type === "context") {
422
+ for (let offset = 0;offset < content.lines; offset += 1) {
423
+ rows.push({
424
+ type: "split-line",
425
+ key: `${file.id}:split:${hunkIndex}:context:${deletionLineIndex + offset}:${additionLineIndex + offset}`,
426
+ fileId: file.id,
427
+ hunkIndex,
428
+ left: makeSplitCell("context", deletionLineNumber + offset, file.metadata.deletionLines[deletionLineIndex + offset], deletionLines[deletionLineIndex + offset], theme),
429
+ right: makeSplitCell("context", additionLineNumber + offset, file.metadata.additionLines[additionLineIndex + offset], additionLines[additionLineIndex + offset], theme)
430
+ });
431
+ }
432
+ deletionLineIndex += content.lines;
433
+ additionLineIndex += content.lines;
434
+ deletionLineNumber += content.lines;
435
+ additionLineNumber += content.lines;
436
+ continue;
437
+ }
438
+ const pairedLines = Math.max(content.deletions, content.additions);
439
+ for (let offset = 0;offset < pairedLines; offset += 1) {
440
+ const hasDeletion = offset < content.deletions;
441
+ const hasAddition = offset < content.additions;
442
+ rows.push({
443
+ type: "split-line",
444
+ key: `${file.id}:split:${hunkIndex}:change:${deletionLineIndex + offset}:${additionLineIndex + offset}`,
445
+ fileId: file.id,
446
+ hunkIndex,
447
+ left: hasDeletion ? makeSplitCell("deletion", deletionLineNumber + offset, file.metadata.deletionLines[deletionLineIndex + offset], deletionLines[deletionLineIndex + offset], theme) : makeSplitCell("empty", undefined, undefined, undefined, theme),
448
+ right: hasAddition ? makeSplitCell("addition", additionLineNumber + offset, file.metadata.additionLines[additionLineIndex + offset], additionLines[additionLineIndex + offset], theme) : makeSplitCell("empty", undefined, undefined, undefined, theme)
449
+ });
450
+ }
451
+ deletionLineIndex += content.deletions;
452
+ additionLineIndex += content.additions;
453
+ deletionLineNumber += content.deletions;
454
+ additionLineNumber += content.additions;
455
+ }
456
+ }
457
+ const trailingLines = trailingCollapsedLines(file.metadata);
458
+ if (trailingLines > 0) {
459
+ rows.push({
460
+ type: "collapsed",
461
+ key: `${file.id}:collapsed:trailing`,
462
+ fileId: file.id,
463
+ hunkIndex: Math.max(file.metadata.hunks.length - 1, 0),
464
+ text: collapsedRowText(trailingLines)
465
+ });
466
+ }
467
+ return rows;
468
+ }
469
+ function buildStackRows(file, highlighted, theme) {
470
+ const rows = [];
471
+ const deletionLines = highlighted?.deletionLines ?? [];
472
+ const additionLines = highlighted?.additionLines ?? [];
473
+ for (const [hunkIndex, hunk] of file.metadata.hunks.entries()) {
474
+ if (hunk.collapsedBefore > 0) {
475
+ rows.push({
476
+ type: "collapsed",
477
+ key: `${file.id}:stack:collapsed:${hunkIndex}`,
478
+ fileId: file.id,
479
+ hunkIndex,
480
+ text: collapsedRowText(hunk.collapsedBefore)
481
+ });
482
+ }
483
+ rows.push({
484
+ type: "hunk-header",
485
+ key: `${file.id}:stack:header:${hunkIndex}`,
486
+ fileId: file.id,
487
+ hunkIndex,
488
+ text: formatHunkHeader(hunk)
489
+ });
490
+ let deletionLineIndex = hunk.deletionLineIndex;
491
+ let additionLineIndex = hunk.additionLineIndex;
492
+ let deletionLineNumber = hunk.deletionStart;
493
+ let additionLineNumber = hunk.additionStart;
494
+ for (const content of hunk.hunkContent) {
495
+ if (content.type === "context") {
496
+ for (let offset = 0;offset < content.lines; offset += 1) {
497
+ rows.push({
498
+ type: "stack-line",
499
+ key: `${file.id}:stack:${hunkIndex}:context:${deletionLineIndex + offset}:${additionLineIndex + offset}`,
500
+ fileId: file.id,
501
+ hunkIndex,
502
+ cell: makeStackCell("context", deletionLineNumber + offset, additionLineNumber + offset, file.metadata.additionLines[additionLineIndex + offset], additionLines[additionLineIndex + offset], theme)
503
+ });
504
+ }
505
+ deletionLineIndex += content.lines;
506
+ additionLineIndex += content.lines;
507
+ deletionLineNumber += content.lines;
508
+ additionLineNumber += content.lines;
509
+ continue;
510
+ }
511
+ for (let offset = 0;offset < content.deletions; offset += 1) {
512
+ rows.push({
513
+ type: "stack-line",
514
+ key: `${file.id}:stack:${hunkIndex}:deletion:${deletionLineIndex + offset}`,
515
+ fileId: file.id,
516
+ hunkIndex,
517
+ cell: makeStackCell("deletion", deletionLineNumber + offset, undefined, file.metadata.deletionLines[deletionLineIndex + offset], deletionLines[deletionLineIndex + offset], theme)
518
+ });
519
+ }
520
+ for (let offset = 0;offset < content.additions; offset += 1) {
521
+ rows.push({
522
+ type: "stack-line",
523
+ key: `${file.id}:stack:${hunkIndex}:addition:${additionLineIndex + offset}`,
524
+ fileId: file.id,
525
+ hunkIndex,
526
+ cell: makeStackCell("addition", undefined, additionLineNumber + offset, file.metadata.additionLines[additionLineIndex + offset], additionLines[additionLineIndex + offset], theme)
527
+ });
528
+ }
529
+ deletionLineIndex += content.deletions;
530
+ additionLineIndex += content.additions;
531
+ deletionLineNumber += content.deletions;
532
+ additionLineNumber += content.additions;
533
+ }
534
+ }
535
+ const trailingLines = trailingCollapsedLines(file.metadata);
536
+ if (trailingLines > 0) {
537
+ rows.push({
538
+ type: "collapsed",
539
+ key: `${file.id}:stack:collapsed:trailing`,
540
+ fileId: file.id,
541
+ hunkIndex: Math.max(file.metadata.hunks.length - 1, 0),
542
+ text: collapsedRowText(trailingLines)
543
+ });
544
+ }
545
+ return rows;
546
+ }
547
+
548
+ // src/ui/diff/renderRows.tsx
549
+ import { memo } from "react";
550
+
551
+ // src/ui/diff/rowStyle.ts
552
+ var INACTIVE_RAIL_BLEND = 0.35;
553
+ function diffRailMarker() {
554
+ return "▌";
555
+ }
556
+ function neutralRailColor(theme) {
557
+ return theme.lineNumberFg;
558
+ }
559
+ function dimRailColor(color, theme) {
560
+ return blendHex(color, theme.panel, INACTIVE_RAIL_BLEND);
561
+ }
562
+ function stackRailColor(kind, theme, selected) {
563
+ let color;
564
+ if (kind === "addition") {
565
+ color = theme.addedSignColor;
566
+ } else if (kind === "deletion") {
567
+ color = theme.removedSignColor;
568
+ } else {
569
+ color = neutralRailColor(theme);
570
+ }
571
+ return selected ? color : dimRailColor(color, theme);
572
+ }
573
+ function splitLeftRailColor(kind, theme, selected) {
574
+ const color = kind === "deletion" ? theme.removedSignColor : neutralRailColor(theme);
575
+ return selected ? color : dimRailColor(color, theme);
576
+ }
577
+ function splitRightRailColor(kind, theme, selected) {
578
+ const color = kind === "addition" ? theme.addedSignColor : neutralRailColor(theme);
579
+ return selected ? color : dimRailColor(color, theme);
580
+ }
581
+ function splitCellPalette(kind, theme) {
582
+ if (kind === "addition") {
583
+ return {
584
+ gutterBg: theme.addedBg,
585
+ contentBg: theme.addedBg,
586
+ signColor: theme.addedSignColor,
587
+ numberColor: theme.addedSignColor
588
+ };
589
+ }
590
+ if (kind === "deletion") {
591
+ return {
592
+ gutterBg: theme.removedBg,
593
+ contentBg: theme.removedBg,
594
+ signColor: theme.removedSignColor,
595
+ numberColor: theme.removedSignColor
596
+ };
597
+ }
598
+ if (kind === "empty") {
599
+ return {
600
+ gutterBg: theme.lineNumberBg,
601
+ contentBg: theme.panelAlt,
602
+ signColor: theme.muted,
603
+ numberColor: theme.lineNumberFg
604
+ };
605
+ }
606
+ return {
607
+ gutterBg: theme.lineNumberBg,
608
+ contentBg: theme.contextBg,
609
+ signColor: theme.muted,
610
+ numberColor: theme.lineNumberFg
611
+ };
612
+ }
613
+ function stackCellPalette(kind, theme) {
614
+ if (kind === "addition") {
615
+ return {
616
+ gutterBg: theme.addedBg,
617
+ contentBg: theme.addedBg,
618
+ signColor: theme.addedSignColor,
619
+ numberColor: theme.addedSignColor
620
+ };
621
+ }
622
+ if (kind === "deletion") {
623
+ return {
624
+ gutterBg: theme.removedBg,
625
+ contentBg: theme.removedBg,
626
+ signColor: theme.removedSignColor,
627
+ numberColor: theme.removedSignColor
628
+ };
629
+ }
630
+ return {
631
+ gutterBg: theme.lineNumberBg,
632
+ contentBg: theme.contextBg,
633
+ signColor: theme.muted,
634
+ numberColor: theme.lineNumberFg
635
+ };
636
+ }
637
+ function diffLineNumberText(value, width) {
638
+ return value === undefined ? " ".repeat(width) : String(value).padStart(width, " ");
639
+ }
640
+ function stackGutterText(cell, lineNumberDigits, showLineNumbers) {
641
+ if (!showLineNumbers) {
642
+ return `${cell.sign} `;
643
+ }
644
+ const oldNumber = diffLineNumberText(cell.oldLineNumber, lineNumberDigits);
645
+ const newNumber = diffLineNumberText(cell.newLineNumber, lineNumberDigits);
646
+ return `${oldNumber} ${newNumber} ${cell.sign}`;
647
+ }
648
+
649
+ // src/ui/diff/renderRows.tsx
650
+ import { jsxDEV, Fragment } from "@opentui/react/jsx-dev-runtime";
651
+ function fitText(text, width) {
652
+ if (width <= 0) {
653
+ return "";
654
+ }
655
+ if (text.length <= width) {
656
+ return text;
657
+ }
658
+ if (width === 1) {
659
+ return "…";
660
+ }
661
+ return `${text.slice(0, width - 1)}…`;
662
+ }
663
+ function sliceSpansWindow(spans, offset, width) {
664
+ if (width <= 0) {
665
+ return {
666
+ spans: [],
667
+ usedWidth: 0
668
+ };
669
+ }
670
+ const sliced = [];
671
+ let remainingOffset = Math.max(0, offset);
672
+ let remaining = width;
673
+ let usedWidth = 0;
674
+ for (const span of spans) {
675
+ if (remaining <= 0) {
676
+ break;
677
+ }
678
+ if (remainingOffset >= span.text.length) {
679
+ remainingOffset -= span.text.length;
680
+ continue;
681
+ }
682
+ const start = remainingOffset;
683
+ const text = span.text.slice(start, start + remaining);
684
+ remainingOffset = 0;
685
+ if (text.length === 0) {
686
+ continue;
687
+ }
688
+ const nextSpan = {
689
+ ...span,
690
+ text
691
+ };
692
+ const previous = sliced.at(-1);
693
+ if (previous && previous.fg === nextSpan.fg && previous.bg === nextSpan.bg) {
694
+ previous.text += nextSpan.text;
695
+ } else {
696
+ sliced.push(nextSpan);
697
+ }
698
+ remaining -= text.length;
699
+ usedWidth += text.length;
700
+ }
701
+ return {
702
+ spans: sliced,
703
+ usedWidth
704
+ };
705
+ }
706
+ var marker = diffRailMarker;
707
+ function renderInlineSpans(spans, width, fallbackColor, fallbackBg, keyPrefix, horizontalOffset = 0) {
708
+ const { spans: trimmed, usedWidth } = sliceSpansWindow(spans, horizontalOffset, width);
709
+ let padding = Math.max(0, width - usedWidth);
710
+ if (padding > 0) {
711
+ const lastSpan = trimmed.at(-1);
712
+ if (lastSpan && (lastSpan.fg ?? fallbackColor) === fallbackColor && (lastSpan.bg ?? fallbackBg) === fallbackBg) {
713
+ lastSpan.text += " ".repeat(padding);
714
+ padding = 0;
715
+ }
716
+ }
717
+ return /* @__PURE__ */ jsxDEV(Fragment, {
718
+ children: [
719
+ trimmed.map((span, index) => /* @__PURE__ */ jsxDEV("span", {
720
+ fg: span.fg ?? fallbackColor,
721
+ bg: span.bg ?? fallbackBg,
722
+ children: span.text
723
+ }, `${keyPrefix}:${index}`, false, undefined, this)),
724
+ padding > 0 ? /* @__PURE__ */ jsxDEV("span", {
725
+ fg: fallbackColor,
726
+ bg: fallbackBg,
727
+ children: `${" ".repeat(padding)}`
728
+ }, `${keyPrefix}:padding`, false, undefined, this) : null
729
+ ]
730
+ }, undefined, true, undefined, this);
731
+ }
732
+ function wrapSpans(spans, width) {
733
+ if (width <= 0) {
734
+ return [[]];
735
+ }
736
+ const lines = [[]];
737
+ let current = lines[0];
738
+ let remaining = width;
739
+ for (const span of spans) {
740
+ let offset = 0;
741
+ while (offset < span.text.length) {
742
+ if (remaining <= 0) {
743
+ current = [];
744
+ lines.push(current);
745
+ remaining = width;
746
+ }
747
+ const text = span.text.slice(offset, offset + remaining);
748
+ if (text.length === 0) {
749
+ break;
750
+ }
751
+ const nextSpan = {
752
+ ...span,
753
+ text
754
+ };
755
+ const previous = current.at(-1);
756
+ if (previous && previous.fg === nextSpan.fg && previous.bg === nextSpan.bg) {
757
+ previous.text += nextSpan.text;
758
+ } else {
759
+ current.push(nextSpan);
760
+ }
761
+ offset += text.length;
762
+ remaining -= text.length;
763
+ }
764
+ }
765
+ return lines;
766
+ }
767
+ function buildWrappedSplitCell(cell, width, lineNumberDigits, showLineNumbers, prefixWidth, theme) {
768
+ const palette = splitCellPalette(cell.kind, theme);
769
+ const { gutterWidth, contentWidth } = resolveSplitCellGeometry(width, lineNumberDigits, showLineNumbers, prefixWidth);
770
+ const firstGutterText = showLineNumbers ? `${cell.lineNumber ? String(cell.lineNumber).padStart(lineNumberDigits, " ") : " ".repeat(lineNumberDigits)} ${cell.sign}`.padEnd(gutterWidth) : `${cell.sign} `.padEnd(gutterWidth);
771
+ const wrappedSpans = wrapSpans(cell.spans, contentWidth);
772
+ return {
773
+ gutterWidth,
774
+ palette,
775
+ lines: wrappedSpans.map((spans, index) => ({
776
+ gutterText: index === 0 ? firstGutterText : " ".repeat(gutterWidth),
777
+ spans
778
+ }))
779
+ };
780
+ }
781
+ function buildWrappedStackCell(cell, width, lineNumberDigits, showLineNumbers, prefixWidth, theme) {
782
+ const palette = stackCellPalette(cell.kind, theme);
783
+ const { gutterWidth, contentWidth } = resolveStackCellGeometry(width, lineNumberDigits, showLineNumbers, prefixWidth);
784
+ const firstGutterText = stackGutterText(cell, lineNumberDigits, showLineNumbers).padEnd(gutterWidth);
785
+ const wrappedSpans = wrapSpans(cell.spans, contentWidth);
786
+ return {
787
+ gutterWidth,
788
+ palette,
789
+ lines: wrappedSpans.map((spans, index) => ({
790
+ gutterText: index === 0 ? firstGutterText : " ".repeat(gutterWidth),
791
+ spans
792
+ }))
793
+ };
794
+ }
795
+ function renderSplitCell(cell, width, lineNumberDigits, showLineNumbers, theme, keyPrefix, contentOffset = 0, prefix) {
796
+ const palette = splitCellPalette(cell.kind, theme);
797
+ const prefixWidth = prefix?.text.length ?? 0;
798
+ const { gutterWidth, contentWidth } = resolveSplitCellGeometry(width, lineNumberDigits, showLineNumbers, prefixWidth);
799
+ const gutterText = showLineNumbers ? `${cell.lineNumber ? String(cell.lineNumber).padStart(lineNumberDigits, " ") : " ".repeat(lineNumberDigits)} ${cell.sign}`.padEnd(gutterWidth) : `${cell.sign} `.padEnd(gutterWidth);
800
+ return /* @__PURE__ */ jsxDEV(Fragment, {
801
+ children: [
802
+ prefix ? /* @__PURE__ */ jsxDEV("span", {
803
+ fg: prefix.fg,
804
+ bg: prefix.bg,
805
+ children: prefix.text
806
+ }, `${keyPrefix}:prefix`, false, undefined, this) : null,
807
+ /* @__PURE__ */ jsxDEV("span", {
808
+ fg: palette.numberColor,
809
+ bg: palette.gutterBg,
810
+ children: gutterText
811
+ }, `${keyPrefix}:gutter`, false, undefined, this),
812
+ renderInlineSpans(cell.spans, contentWidth, theme.text, palette.contentBg, `${keyPrefix}:content`, contentOffset)
813
+ ]
814
+ }, undefined, true, undefined, this);
815
+ }
816
+ function renderStackCell(cell, width, lineNumberDigits, showLineNumbers, theme, keyPrefix, contentOffset = 0, prefix) {
817
+ const palette = stackCellPalette(cell.kind, theme);
818
+ const prefixWidth = prefix?.text.length ?? 0;
819
+ const { gutterWidth, contentWidth } = resolveStackCellGeometry(width, lineNumberDigits, showLineNumbers, prefixWidth);
820
+ return /* @__PURE__ */ jsxDEV(Fragment, {
821
+ children: [
822
+ prefix ? /* @__PURE__ */ jsxDEV("span", {
823
+ fg: prefix.fg,
824
+ bg: prefix.bg,
825
+ children: prefix.text
826
+ }, `${keyPrefix}:prefix`, false, undefined, this) : null,
827
+ /* @__PURE__ */ jsxDEV("span", {
828
+ fg: palette.numberColor,
829
+ bg: palette.gutterBg,
830
+ children: stackGutterText(cell, lineNumberDigits, showLineNumbers).padEnd(gutterWidth)
831
+ }, `${keyPrefix}:gutter`, false, undefined, this),
832
+ renderInlineSpans(cell.spans, contentWidth, theme.text, palette.contentBg, `${keyPrefix}:content`, contentOffset)
833
+ ]
834
+ }, undefined, true, undefined, this);
835
+ }
836
+ function renderWrappedSplitCellLine(line, palette, contentWidth, theme, keyPrefix, prefix) {
837
+ return /* @__PURE__ */ jsxDEV(Fragment, {
838
+ children: [
839
+ /* @__PURE__ */ jsxDEV("span", {
840
+ fg: prefix.fg,
841
+ bg: prefix.bg,
842
+ children: prefix.text
843
+ }, `${keyPrefix}:prefix`, false, undefined, this),
844
+ /* @__PURE__ */ jsxDEV("span", {
845
+ fg: palette.numberColor,
846
+ bg: palette.gutterBg,
847
+ children: line.gutterText
848
+ }, `${keyPrefix}:gutter`, false, undefined, this),
849
+ renderInlineSpans(line.spans, contentWidth, theme.text, palette.contentBg, `${keyPrefix}:content`)
850
+ ]
851
+ }, undefined, true, undefined, this);
852
+ }
853
+ function renderWrappedStackCellLine(line, palette, contentWidth, theme, keyPrefix, prefix) {
854
+ return /* @__PURE__ */ jsxDEV(Fragment, {
855
+ children: [
856
+ /* @__PURE__ */ jsxDEV("span", {
857
+ fg: prefix.fg,
858
+ bg: prefix.bg,
859
+ children: prefix.text
860
+ }, `${keyPrefix}:prefix`, false, undefined, this),
861
+ /* @__PURE__ */ jsxDEV("span", {
862
+ fg: palette.numberColor,
863
+ bg: palette.gutterBg,
864
+ children: line.gutterText
865
+ }, `${keyPrefix}:gutter`, false, undefined, this),
866
+ renderInlineSpans(line.spans, contentWidth, theme.text, palette.contentBg, `${keyPrefix}:content`)
867
+ ]
868
+ }, undefined, true, undefined, this);
869
+ }
870
+ function diffMessage(file) {
871
+ if (file.metadata.type === "rename-pure") {
872
+ return "No textual hunks. This change only renames the file.";
873
+ }
874
+ if (file.isBinary) {
875
+ return "Binary file skipped";
876
+ }
877
+ if (file.isTooLarge) {
878
+ return "File too large to render automatically.";
879
+ }
880
+ if (file.metadata.type === "new") {
881
+ return "No textual hunks. The file is marked as new.";
882
+ }
883
+ if (file.metadata.type === "deleted") {
884
+ return "No textual hunks. The file is marked as deleted.";
885
+ }
886
+ return "No textual hunks to render for this file.";
887
+ }
888
+ function renderHeaderRow(row, width, theme, selected, annotated, anchorId, onOpenAgentNotesAtHunk) {
889
+ const badgeText = annotated ? "[AI]" : "";
890
+ const badgeWidth = annotated ? badgeText.length + 1 : 0;
891
+ const label = row.type === "collapsed" ? fitText(`··· ${row.text} ···`, Math.max(0, width - 1 - badgeWidth)) : fitText(row.text, Math.max(0, width - 1 - badgeWidth));
892
+ if (!annotated) {
893
+ return /* @__PURE__ */ jsxDEV("box", {
894
+ id: anchorId,
895
+ style: {
896
+ width: "100%",
897
+ height: 1,
898
+ backgroundColor: theme.panelAlt
899
+ },
900
+ children: /* @__PURE__ */ jsxDEV("text", {
901
+ children: [
902
+ /* @__PURE__ */ jsxDEV("span", {
903
+ fg: selected ? neutralRailColor(theme) : dimRailColor(neutralRailColor(theme), theme),
904
+ bg: theme.panelAlt,
905
+ children: marker()
906
+ }, undefined, false, undefined, this),
907
+ /* @__PURE__ */ jsxDEV("span", {
908
+ fg: row.type === "collapsed" ? theme.muted : theme.badgeNeutral,
909
+ bg: theme.panelAlt,
910
+ children: label
911
+ }, undefined, false, undefined, this)
912
+ ]
913
+ }, undefined, true, undefined, this)
914
+ }, row.key, false, undefined, this);
915
+ }
916
+ return /* @__PURE__ */ jsxDEV("box", {
917
+ id: anchorId,
918
+ style: {
919
+ width: "100%",
920
+ height: 1,
921
+ flexDirection: "row",
922
+ backgroundColor: theme.panelAlt
923
+ },
924
+ children: [
925
+ /* @__PURE__ */ jsxDEV("box", {
926
+ style: { width: Math.max(0, width - badgeWidth), height: 1 },
927
+ children: /* @__PURE__ */ jsxDEV("text", {
928
+ children: [
929
+ /* @__PURE__ */ jsxDEV("span", {
930
+ fg: selected ? neutralRailColor(theme) : dimRailColor(neutralRailColor(theme), theme),
931
+ bg: theme.panelAlt,
932
+ children: marker()
933
+ }, undefined, false, undefined, this),
934
+ /* @__PURE__ */ jsxDEV("span", {
935
+ fg: row.type === "collapsed" ? theme.muted : theme.badgeNeutral,
936
+ bg: theme.panelAlt,
937
+ children: label
938
+ }, undefined, false, undefined, this)
939
+ ]
940
+ }, undefined, true, undefined, this)
941
+ }, undefined, false, undefined, this),
942
+ /* @__PURE__ */ jsxDEV("box", {
943
+ style: { width: badgeWidth, height: 1 },
944
+ onMouseUp: () => onOpenAgentNotesAtHunk?.(row.hunkIndex),
945
+ children: /* @__PURE__ */ jsxDEV("text", {
946
+ fg: theme.noteTitleText,
947
+ bg: theme.noteTitleBackground,
948
+ children: ` ${badgeText}`
949
+ }, undefined, false, undefined, this)
950
+ }, undefined, false, undefined, this)
951
+ ]
952
+ }, row.key, true, undefined, this);
953
+ }
954
+ function renderRow(row, width, lineNumberDigits, showLineNumbers, showHunkHeaders, wrapLines, codeHorizontalOffset, theme, selected, annotated, anchorId, noteGuideSide, onOpenAgentNotesAtHunk) {
955
+ let baseRow;
956
+ if (row.type === "collapsed") {
957
+ baseRow = renderHeaderRow(row, width, theme, selected, annotated, anchorId, onOpenAgentNotesAtHunk);
958
+ } else if (row.type === "hunk-header") {
959
+ baseRow = showHunkHeaders ? renderHeaderRow(row, width, theme, selected, annotated, anchorId, onOpenAgentNotesAtHunk) : null;
960
+ } else if (row.type === "split-line") {
961
+ const guideOnOldSide = noteGuideSide === "old";
962
+ const guideOnNewSide = noteGuideSide === "new";
963
+ const { leftWidth, rightWidth } = resolveSplitPaneWidths(width);
964
+ const rightRenderWidth = Math.max(0, rightWidth - (guideOnNewSide ? 1 : 0));
965
+ const leftPrefix = {
966
+ text: guideOnOldSide ? "│" : marker(),
967
+ fg: guideOnOldSide ? theme.noteBorder : splitLeftRailColor(row.left.kind, theme, selected),
968
+ bg: theme.panel
969
+ };
970
+ const rightPrefix = {
971
+ text: "▌",
972
+ fg: splitRightRailColor(row.right.kind, theme, selected),
973
+ bg: theme.panel
974
+ };
975
+ if (!wrapLines) {
976
+ baseRow = /* @__PURE__ */ jsxDEV("box", {
977
+ id: anchorId,
978
+ style: { width: "100%", height: 1 },
979
+ children: /* @__PURE__ */ jsxDEV("text", {
980
+ children: [
981
+ renderSplitCell(row.left, leftWidth, lineNumberDigits, showLineNumbers, theme, `${row.key}:left`, codeHorizontalOffset, leftPrefix),
982
+ renderSplitCell(row.right, rightRenderWidth, lineNumberDigits, showLineNumbers, theme, `${row.key}:right`, codeHorizontalOffset, rightPrefix),
983
+ guideOnNewSide ? /* @__PURE__ */ jsxDEV("span", {
984
+ fg: theme.noteBorder,
985
+ children: "│"
986
+ }, `${row.key}:note-guide`, false, undefined, this) : null
987
+ ]
988
+ }, undefined, true, undefined, this)
989
+ }, undefined, false, undefined, this);
990
+ } else {
991
+ const leftLayout = buildWrappedSplitCell(row.left, leftWidth, lineNumberDigits, showLineNumbers, leftPrefix.text.length, theme);
992
+ const rightLayout = buildWrappedSplitCell(row.right, rightRenderWidth, lineNumberDigits, showLineNumbers, rightPrefix.text.length, theme);
993
+ const leftContentWidth = Math.max(0, leftWidth - leftPrefix.text.length - leftLayout.gutterWidth);
994
+ const rightContentWidth = Math.max(0, rightRenderWidth - rightPrefix.text.length - rightLayout.gutterWidth);
995
+ const visualLineCount = Math.max(leftLayout.lines.length, rightLayout.lines.length);
996
+ baseRow = /* @__PURE__ */ jsxDEV("box", {
997
+ id: anchorId,
998
+ style: { width: "100%", flexDirection: "column" },
999
+ children: Array.from({ length: visualLineCount }, (_, index) => {
1000
+ const leftLine = leftLayout.lines[index] ?? {
1001
+ gutterText: " ".repeat(leftLayout.gutterWidth),
1002
+ spans: []
1003
+ };
1004
+ const rightLine = rightLayout.lines[index] ?? {
1005
+ gutterText: " ".repeat(rightLayout.gutterWidth),
1006
+ spans: []
1007
+ };
1008
+ return /* @__PURE__ */ jsxDEV("box", {
1009
+ style: { width: "100%", height: 1 },
1010
+ children: /* @__PURE__ */ jsxDEV("text", {
1011
+ children: [
1012
+ renderWrappedSplitCellLine(leftLine, leftLayout.palette, leftContentWidth, theme, `${row.key}:left:${index}`, leftPrefix),
1013
+ renderWrappedSplitCellLine(rightLine, rightLayout.palette, rightContentWidth, theme, `${row.key}:right:${index}`, rightPrefix),
1014
+ guideOnNewSide ? /* @__PURE__ */ jsxDEV("span", {
1015
+ fg: theme.noteBorder,
1016
+ children: "│"
1017
+ }, `${row.key}:note-guide:${index}`, false, undefined, this) : null
1018
+ ]
1019
+ }, undefined, true, undefined, this)
1020
+ }, `${row.key}:wrap:${index}`, false, undefined, this);
1021
+ })
1022
+ }, undefined, false, undefined, this);
1023
+ }
1024
+ } else if (row.type === "stack-line") {
1025
+ const guideOnOldSide = noteGuideSide === "old";
1026
+ const guideOnNewSide = noteGuideSide === "new";
1027
+ const contentWidth = Math.max(0, width - (guideOnNewSide ? 1 : 0));
1028
+ const prefix = {
1029
+ text: guideOnOldSide ? "│" : marker(),
1030
+ fg: guideOnOldSide ? theme.noteBorder : stackRailColor(row.cell.kind, theme, selected),
1031
+ bg: theme.panel
1032
+ };
1033
+ if (!wrapLines) {
1034
+ baseRow = /* @__PURE__ */ jsxDEV("box", {
1035
+ id: anchorId,
1036
+ style: { width: "100%", height: 1 },
1037
+ children: /* @__PURE__ */ jsxDEV("text", {
1038
+ children: [
1039
+ renderStackCell(row.cell, contentWidth, lineNumberDigits, showLineNumbers, theme, `${row.key}:stack`, codeHorizontalOffset, prefix),
1040
+ guideOnNewSide ? /* @__PURE__ */ jsxDEV("span", {
1041
+ fg: theme.noteBorder,
1042
+ children: "│"
1043
+ }, `${row.key}:note-guide`, false, undefined, this) : null
1044
+ ]
1045
+ }, undefined, true, undefined, this)
1046
+ }, undefined, false, undefined, this);
1047
+ } else {
1048
+ const layout = buildWrappedStackCell(row.cell, contentWidth, lineNumberDigits, showLineNumbers, prefix.text.length, theme);
1049
+ const wrappedContentWidth = Math.max(0, contentWidth - prefix.text.length - layout.gutterWidth);
1050
+ baseRow = /* @__PURE__ */ jsxDEV("box", {
1051
+ id: anchorId,
1052
+ style: { width: "100%", flexDirection: "column" },
1053
+ children: layout.lines.map((line, index) => /* @__PURE__ */ jsxDEV("box", {
1054
+ style: { width: "100%", height: 1 },
1055
+ children: /* @__PURE__ */ jsxDEV("text", {
1056
+ children: [
1057
+ renderWrappedStackCellLine(line, layout.palette, wrappedContentWidth, theme, `${row.key}:stack:${index}`, prefix),
1058
+ guideOnNewSide ? /* @__PURE__ */ jsxDEV("span", {
1059
+ fg: theme.noteBorder,
1060
+ children: "│"
1061
+ }, `${row.key}:note-guide:${index}`, false, undefined, this) : null
1062
+ ]
1063
+ }, undefined, true, undefined, this)
1064
+ }, `${row.key}:wrap:${index}`, false, undefined, this))
1065
+ }, undefined, false, undefined, this);
1066
+ }
1067
+ } else {
1068
+ baseRow = /* @__PURE__ */ jsxDEV("box", {
1069
+ style: { width: "100%", height: 1 },
1070
+ children: /* @__PURE__ */ jsxDEV("text", {
1071
+ fg: theme.muted,
1072
+ children: "Unsupported row."
1073
+ }, undefined, false, undefined, this)
1074
+ }, undefined, false, undefined, this);
1075
+ }
1076
+ return baseRow;
1077
+ }
1078
+ var DiffRowView = memo(function DiffRowViewComponent({
1079
+ row,
1080
+ width,
1081
+ lineNumberDigits,
1082
+ showLineNumbers,
1083
+ showHunkHeaders,
1084
+ wrapLines,
1085
+ codeHorizontalOffset,
1086
+ theme,
1087
+ selected,
1088
+ annotated,
1089
+ anchorId,
1090
+ noteGuideSide,
1091
+ onOpenAgentNotesAtHunk
1092
+ }) {
1093
+ return renderRow(row, width, lineNumberDigits, showLineNumbers, showHunkHeaders, wrapLines, codeHorizontalOffset, theme, selected, annotated, anchorId, noteGuideSide, onOpenAgentNotesAtHunk);
1094
+ }, (previous, next) => {
1095
+ return previous.row === next.row && previous.width === next.width && previous.lineNumberDigits === next.lineNumberDigits && previous.showLineNumbers === next.showLineNumbers && previous.showHunkHeaders === next.showHunkHeaders && previous.wrapLines === next.wrapLines && previous.codeHorizontalOffset === next.codeHorizontalOffset && previous.theme === next.theme && previous.selected === next.selected && previous.annotated === next.annotated && previous.anchorId === next.anchorId && previous.noteGuideSide === next.noteGuideSide;
1096
+ });
1097
+
1098
+ // src/ui/diff/useHighlightedDiff.ts
1099
+ import { useLayoutEffect, useState } from "react";
1100
+ var MAX_CACHE_ENTRIES = 150;
1101
+ var SHARED_HIGHLIGHTED_DIFF_CACHE = new Map;
1102
+ var SHARED_HIGHLIGHT_PROMISES = new Map;
1103
+ function enforceCacheLimit() {
1104
+ while (SHARED_HIGHLIGHTED_DIFF_CACHE.size > MAX_CACHE_ENTRIES) {
1105
+ const oldest = SHARED_HIGHLIGHTED_DIFF_CACHE.keys().next().value;
1106
+ if (oldest !== undefined) {
1107
+ SHARED_HIGHLIGHTED_DIFF_CACHE.delete(oldest);
1108
+ }
1109
+ }
1110
+ }
1111
+ function lineSetFingerprint(lines) {
1112
+ let totalChars = 0;
1113
+ let hash = 2166136261;
1114
+ for (const line of lines ?? []) {
1115
+ totalChars += line.length;
1116
+ for (let index = 0;index < line.length; index += 1) {
1117
+ hash ^= line.charCodeAt(index);
1118
+ hash = Math.imul(hash, 16777619);
1119
+ }
1120
+ hash ^= 10;
1121
+ hash = Math.imul(hash, 16777619);
1122
+ }
1123
+ return `${lines?.length ?? 0}:${totalChars}:${(hash >>> 0).toString(36)}`;
1124
+ }
1125
+ function metadataFingerprint(file) {
1126
+ const hunkSummary = file.metadata.hunks.map((hunk) => `${hunk.hunkSpecs ?? ""}:${hunk.deletionStart}:${hunk.deletionCount}:${hunk.additionStart}:${hunk.additionCount}:${hunk.hunkContent.length}`).join("|");
1127
+ return [
1128
+ file.metadata.name,
1129
+ file.metadata.prevName ?? "",
1130
+ file.metadata.type,
1131
+ lineSetFingerprint(file.metadata.deletionLines),
1132
+ lineSetFingerprint(file.metadata.additionLines),
1133
+ hunkSummary
1134
+ ].join(":");
1135
+ }
1136
+ function patchFingerprint(file) {
1137
+ const { patch } = file;
1138
+ if (patch.length === 0) {
1139
+ return metadataFingerprint(file);
1140
+ }
1141
+ const mid = Math.floor(patch.length / 2);
1142
+ return `${patch.length}:${patch.slice(0, 64)}:${patch.slice(mid, mid + 64)}:${patch.slice(-64)}`;
1143
+ }
1144
+ function buildCacheKey(appearance, file) {
1145
+ return `${appearance}:${file.id}:${patchFingerprint(file)}`;
1146
+ }
1147
+ function commitHighlightResult(cacheKey, promise, result) {
1148
+ if (SHARED_HIGHLIGHT_PROMISES.get(cacheKey) !== promise) {
1149
+ return false;
1150
+ }
1151
+ SHARED_HIGHLIGHT_PROMISES.delete(cacheKey);
1152
+ SHARED_HIGHLIGHTED_DIFF_CACHE.set(cacheKey, result);
1153
+ enforceCacheLimit();
1154
+ return true;
1155
+ }
1156
+ function ensureHighlightedDiffLoaded(file, appearance, cacheKey = buildCacheKey(appearance, file)) {
1157
+ const cached = SHARED_HIGHLIGHTED_DIFF_CACHE.get(cacheKey);
1158
+ if (cached) {
1159
+ return Promise.resolve(cached);
1160
+ }
1161
+ const existing = SHARED_HIGHLIGHT_PROMISES.get(cacheKey);
1162
+ if (existing) {
1163
+ return existing;
1164
+ }
1165
+ let pending;
1166
+ pending = loadHighlightedDiff(file, appearance).then((nextHighlighted) => {
1167
+ commitHighlightResult(cacheKey, pending, nextHighlighted);
1168
+ return nextHighlighted;
1169
+ }).catch(() => {
1170
+ const fallback = {
1171
+ deletionLines: [],
1172
+ additionLines: []
1173
+ };
1174
+ commitHighlightResult(cacheKey, pending, fallback);
1175
+ return fallback;
1176
+ });
1177
+ SHARED_HIGHLIGHT_PROMISES.set(cacheKey, pending);
1178
+ return pending;
1179
+ }
1180
+ function resolveHighlightedSnapshot({
1181
+ appearanceCacheKey,
1182
+ highlighted,
1183
+ highlightedCacheKey
1184
+ }) {
1185
+ if (!appearanceCacheKey) {
1186
+ return null;
1187
+ }
1188
+ if (highlightedCacheKey === appearanceCacheKey) {
1189
+ return highlighted;
1190
+ }
1191
+ return SHARED_HIGHLIGHTED_DIFF_CACHE.get(appearanceCacheKey) ?? null;
1192
+ }
1193
+ function useHighlightedDiff({
1194
+ file,
1195
+ appearance,
1196
+ shouldLoadHighlight
1197
+ }) {
1198
+ const [highlighted, setHighlighted] = useState(null);
1199
+ const [highlightedCacheKey, setHighlightedCacheKey] = useState(null);
1200
+ const appearanceCacheKey = file ? buildCacheKey(appearance, file) : null;
1201
+ useLayoutEffect(() => {
1202
+ if (!file || !appearanceCacheKey) {
1203
+ setHighlighted(null);
1204
+ setHighlightedCacheKey(null);
1205
+ return;
1206
+ }
1207
+ if (highlightedCacheKey === appearanceCacheKey) {
1208
+ return;
1209
+ }
1210
+ const cached = SHARED_HIGHLIGHTED_DIFF_CACHE.get(appearanceCacheKey);
1211
+ if (cached) {
1212
+ setHighlighted(cached);
1213
+ setHighlightedCacheKey(appearanceCacheKey);
1214
+ return;
1215
+ }
1216
+ if (!shouldLoadHighlight) {
1217
+ return;
1218
+ }
1219
+ let cancelled = false;
1220
+ setHighlighted(null);
1221
+ ensureHighlightedDiffLoaded(file, appearance, appearanceCacheKey).then((nextHighlighted) => {
1222
+ if (cancelled) {
1223
+ return;
1224
+ }
1225
+ setHighlighted(nextHighlighted);
1226
+ setHighlightedCacheKey(appearanceCacheKey);
1227
+ });
1228
+ return () => {
1229
+ cancelled = true;
1230
+ };
1231
+ }, [appearance, appearanceCacheKey, file, highlightedCacheKey, shouldLoadHighlight]);
1232
+ return resolveHighlightedSnapshot({
1233
+ appearanceCacheKey,
1234
+ highlighted,
1235
+ highlightedCacheKey
1236
+ });
1237
+ }
1238
+
1239
+ // src/ui/themes.ts
1240
+ import { RGBA, SyntaxStyle } from "@opentui/core";
1241
+ function createSyntaxStyle(colors) {
1242
+ return SyntaxStyle.fromStyles({
1243
+ default: { fg: RGBA.fromHex(colors.default) },
1244
+ keyword: { fg: RGBA.fromHex(colors.keyword), bold: true },
1245
+ string: { fg: RGBA.fromHex(colors.string) },
1246
+ comment: { fg: RGBA.fromHex(colors.comment), italic: true },
1247
+ number: { fg: RGBA.fromHex(colors.number) },
1248
+ function: { fg: RGBA.fromHex(colors.function) },
1249
+ method: { fg: RGBA.fromHex(colors.function) },
1250
+ property: { fg: RGBA.fromHex(colors.property) },
1251
+ variable: { fg: RGBA.fromHex(colors.default) },
1252
+ constant: { fg: RGBA.fromHex(colors.number), bold: true },
1253
+ type: { fg: RGBA.fromHex(colors.type) },
1254
+ class: { fg: RGBA.fromHex(colors.type) },
1255
+ punctuation: { fg: RGBA.fromHex(colors.punctuation) }
1256
+ });
1257
+ }
1258
+ function withLazySyntaxStyle(theme, syntaxColors) {
1259
+ let syntaxStyle = null;
1260
+ return {
1261
+ ...theme,
1262
+ syntaxColors,
1263
+ get syntaxStyle() {
1264
+ syntaxStyle ??= createSyntaxStyle(syntaxColors);
1265
+ return syntaxStyle;
1266
+ }
1267
+ };
1268
+ }
1269
+ var THEMES = [
1270
+ withLazySyntaxStyle({
1271
+ id: "graphite",
1272
+ label: "Graphite",
1273
+ appearance: "dark",
1274
+ background: "#111315",
1275
+ panel: "#171a1d",
1276
+ panelAlt: "#1d2126",
1277
+ border: "#343c45",
1278
+ accent: "#d5e0ea",
1279
+ accentMuted: "#414a54",
1280
+ text: "#f2f4f6",
1281
+ muted: "#9aa4af",
1282
+ addedBg: "#1f3025",
1283
+ removedBg: "#372526",
1284
+ contextBg: "#181c20",
1285
+ addedContentBg: "#24362a",
1286
+ removedContentBg: "#432b2d",
1287
+ contextContentBg: "#1e2328",
1288
+ addedSignColor: "#88d39b",
1289
+ removedSignColor: "#f0a0a0",
1290
+ lineNumberBg: "#14181b",
1291
+ lineNumberFg: "#798592",
1292
+ selectedHunk: "#3b434b",
1293
+ badgeAdded: "#88d39b",
1294
+ badgeRemoved: "#f0a0a0",
1295
+ badgeNeutral: "#a9b4bf",
1296
+ fileNew: "#88d39b",
1297
+ fileDeleted: "#f0a0a0",
1298
+ fileRenamed: "#e6cf98",
1299
+ fileModified: "#c49bff",
1300
+ fileUntracked: "#7fd1ff",
1301
+ noteBorder: "#c6a0ff",
1302
+ noteBackground: "#241c31",
1303
+ noteTitleBackground: "#322446",
1304
+ noteTitleText: "#f5edff"
1305
+ }, {
1306
+ default: "#f2f4f6",
1307
+ keyword: "#c4d0da",
1308
+ string: "#d8c6ef",
1309
+ comment: "#7f8b97",
1310
+ number: "#e6cf98",
1311
+ function: "#dfe6ed",
1312
+ property: "#bac8d4",
1313
+ type: "#d3d9e2",
1314
+ punctuation: "#7f8b97"
1315
+ }),
1316
+ withLazySyntaxStyle({
1317
+ id: "midnight",
1318
+ label: "Midnight",
1319
+ appearance: "dark",
1320
+ background: "#08111f",
1321
+ panel: "#0e1b2e",
1322
+ panelAlt: "#13243a",
1323
+ border: "#284264",
1324
+ accent: "#7fd1ff",
1325
+ accentMuted: "#355578",
1326
+ text: "#eef4ff",
1327
+ muted: "#8da5c7",
1328
+ addedBg: "#153526",
1329
+ removedBg: "#47262a",
1330
+ contextBg: "#0f1b2d",
1331
+ addedContentBg: "#102a1f",
1332
+ removedContentBg: "#371b1e",
1333
+ contextContentBg: "#132238",
1334
+ addedSignColor: "#69d69a",
1335
+ removedSignColor: "#ff8e8e",
1336
+ lineNumberBg: "#0b1627",
1337
+ lineNumberFg: "#56739a",
1338
+ selectedHunk: "#20466a",
1339
+ badgeAdded: "#5ad188",
1340
+ badgeRemoved: "#ff8b8b",
1341
+ badgeNeutral: "#89a5d3",
1342
+ fileNew: "#5ad188",
1343
+ fileDeleted: "#ff8b8b",
1344
+ fileRenamed: "#ffd883",
1345
+ fileModified: "#b794f6",
1346
+ fileUntracked: "#7fd1ff",
1347
+ noteBorder: "#c49bff",
1348
+ noteBackground: "#211a36",
1349
+ noteTitleBackground: "#30234f",
1350
+ noteTitleText: "#f5eeff"
1351
+ }, {
1352
+ default: "#e8f1ff",
1353
+ keyword: "#8ed4ff",
1354
+ string: "#c7b4ff",
1355
+ comment: "#6e85a7",
1356
+ number: "#ffd883",
1357
+ function: "#b6c9ff",
1358
+ property: "#a8d6ff",
1359
+ type: "#a4b7ff",
1360
+ punctuation: "#6e85a7"
1361
+ }),
1362
+ withLazySyntaxStyle({
1363
+ id: "paper",
1364
+ label: "Paper",
1365
+ appearance: "light",
1366
+ background: "#f4efe6",
1367
+ panel: "#fffaf3",
1368
+ panelAlt: "#f8f1e7",
1369
+ border: "#d8c8b3",
1370
+ accent: "#77593a",
1371
+ accentMuted: "#d7ccbe",
1372
+ text: "#2f2417",
1373
+ muted: "#786753",
1374
+ addedBg: "#dff0e1",
1375
+ removedBg: "#f6ddde",
1376
+ contextBg: "#faf6ee",
1377
+ addedContentBg: "#eaf8ec",
1378
+ removedContentBg: "#fbebeb",
1379
+ contextContentBg: "#fffaf3",
1380
+ addedSignColor: "#3f8d58",
1381
+ removedSignColor: "#b4545b",
1382
+ lineNumberBg: "#f2e9dc",
1383
+ lineNumberFg: "#9b8367",
1384
+ selectedHunk: "#eadcc5",
1385
+ badgeAdded: "#3f8d58",
1386
+ badgeRemoved: "#b4545b",
1387
+ badgeNeutral: "#8e7355",
1388
+ fileNew: "#3f8d58",
1389
+ fileDeleted: "#b4545b",
1390
+ fileRenamed: "#9f6c1f",
1391
+ fileModified: "#7d5bc4",
1392
+ fileUntracked: "#4a6890",
1393
+ noteBorder: "#7d5bc4",
1394
+ noteBackground: "#efe6ff",
1395
+ noteTitleBackground: "#e3d7ff",
1396
+ noteTitleText: "#462b74"
1397
+ }, {
1398
+ default: "#2f2417",
1399
+ keyword: "#7b5a35",
1400
+ string: "#4a6890",
1401
+ comment: "#8f7a65",
1402
+ number: "#9f6c1f",
1403
+ function: "#5a4a8e",
1404
+ property: "#356b7f",
1405
+ type: "#5f5f9a",
1406
+ punctuation: "#8f7a65"
1407
+ }),
1408
+ withLazySyntaxStyle({
1409
+ id: "ember",
1410
+ label: "Ember",
1411
+ appearance: "dark",
1412
+ background: "#140b08",
1413
+ panel: "#22120d",
1414
+ panelAlt: "#2c1710",
1415
+ border: "#643627",
1416
+ accent: "#ffb07a",
1417
+ accentMuted: "#5d3428",
1418
+ text: "#fff0e6",
1419
+ muted: "#c7a18d",
1420
+ addedBg: "#183424",
1421
+ removedBg: "#4a1f1f",
1422
+ contextBg: "#24140e",
1423
+ addedContentBg: "#21432c",
1424
+ removedContentBg: "#5a2727",
1425
+ contextContentBg: "#2b1711",
1426
+ addedSignColor: "#83d99d",
1427
+ removedSignColor: "#ff9d8f",
1428
+ lineNumberBg: "#1c100c",
1429
+ lineNumberFg: "#9a735f",
1430
+ selectedHunk: "#6a3829",
1431
+ badgeAdded: "#83d99d",
1432
+ badgeRemoved: "#ff9d8f",
1433
+ badgeNeutral: "#f1be9d",
1434
+ fileNew: "#83d99d",
1435
+ fileDeleted: "#ff9d8f",
1436
+ fileRenamed: "#ffd08f",
1437
+ fileModified: "#d8b4fe",
1438
+ fileUntracked: "#ffb07a",
1439
+ noteBorder: "#e1a3ff",
1440
+ noteBackground: "#311d36",
1441
+ noteTitleBackground: "#452650",
1442
+ noteTitleText: "#fff0ff"
1443
+ }, {
1444
+ default: "#fff0e6",
1445
+ keyword: "#ffb47f",
1446
+ string: "#ffd3a8",
1447
+ comment: "#a17d69",
1448
+ number: "#ffd08f",
1449
+ function: "#ffd9b3",
1450
+ property: "#ffc89f",
1451
+ type: "#f7c5b0",
1452
+ punctuation: "#a17d69"
1453
+ })
1454
+ ];
1455
+ function resolveTheme(requested, _themeMode) {
1456
+ const exact = THEMES.find((theme) => theme.id === requested);
1457
+ if (exact) {
1458
+ return exact;
1459
+ }
1460
+ return THEMES.find((theme) => theme.id === "graphite") ?? THEMES[0];
1461
+ }
1462
+
1463
+ // src/opentui/model.ts
1464
+ import { parsePatchFiles } from "@pierre/diffs";
1465
+
1466
+ // src/core/binary.ts
1467
+ function patchLooksBinary(patch) {
1468
+ return /(^|\n)Binary files .* differ(?:\n|$)/.test(patch) || patch.includes(`
1469
+ GIT binary patch
1470
+ `);
1471
+ }
1472
+
1473
+ // src/core/diffPaths.ts
1474
+ function normalizeDiffPath(path) {
1475
+ return path?.replace(/[\r\n]+$/u, "");
1476
+ }
1477
+ function normalizeDiffMetadataPaths(metadata) {
1478
+ const name = normalizeDiffPath(metadata.name) ?? metadata.name;
1479
+ const prevName = normalizeDiffPath(metadata.prevName);
1480
+ if (name === metadata.name && prevName === metadata.prevName) {
1481
+ return metadata;
1482
+ }
1483
+ return {
1484
+ ...metadata,
1485
+ name,
1486
+ prevName
1487
+ };
1488
+ }
1489
+
1490
+ // src/opentui/model.ts
1491
+ var NORMALIZED_HUNK_DIFF_FILES = new WeakSet;
1492
+ function splitPatchIntoFileChunks(rawPatch) {
1493
+ const patch = rawPatch.replaceAll(`\r
1494
+ `, `
1495
+ `);
1496
+ const lines = patch.split(`
1497
+ `);
1498
+ const chunks = [];
1499
+ let current = [];
1500
+ const hasGitHeaders = lines.some((line) => line.startsWith("diff --git "));
1501
+ const flush = () => {
1502
+ if (current.length > 0) {
1503
+ chunks.push(`${current.join(`
1504
+ `).trimEnd()}
1505
+ `);
1506
+ current = [];
1507
+ }
1508
+ };
1509
+ for (let index = 0;index < lines.length; index += 1) {
1510
+ const line = lines[index];
1511
+ if (hasGitHeaders && line.startsWith("diff --git ")) {
1512
+ flush();
1513
+ current.push(line);
1514
+ continue;
1515
+ }
1516
+ if (!hasGitHeaders && line.startsWith("--- ") && lines[index + 1]?.startsWith("+++ ")) {
1517
+ flush();
1518
+ current.push(line);
1519
+ current.push(lines[index + 1]);
1520
+ index += 1;
1521
+ continue;
1522
+ }
1523
+ if (current.length > 0) {
1524
+ current.push(line);
1525
+ }
1526
+ }
1527
+ flush();
1528
+ return chunks;
1529
+ }
1530
+ function findPatchChunk(metadata, chunks, index) {
1531
+ const byIndex = chunks[index];
1532
+ if (byIndex) {
1533
+ return byIndex;
1534
+ }
1535
+ const paths = [metadata.name, metadata.prevName].map(normalizeDiffPath).filter((value) => Boolean(value));
1536
+ return chunks.find((chunk) => paths.some((path) => chunk.includes(path))) ?? "";
1537
+ }
1538
+ function countHunkDiffStats(metadata) {
1539
+ let additions = 0;
1540
+ let deletions = 0;
1541
+ for (const hunk of metadata.hunks) {
1542
+ for (const content of hunk.hunkContent) {
1543
+ if (content.type === "change") {
1544
+ additions += content.additions;
1545
+ deletions += content.deletions;
1546
+ }
1547
+ }
1548
+ }
1549
+ return { additions, deletions };
1550
+ }
1551
+ function createHunkDiffFile(input) {
1552
+ const metadata = normalizeDiffMetadataPaths(input.metadata);
1553
+ const path = normalizeDiffPath(input.path) ?? metadata.name;
1554
+ const previousPath = normalizeDiffPath(input.previousPath) ?? metadata.prevName;
1555
+ const normalized = {
1556
+ ...input,
1557
+ id: input.id,
1558
+ metadata,
1559
+ path,
1560
+ previousPath,
1561
+ stats: input.stats ?? countHunkDiffStats(metadata)
1562
+ };
1563
+ NORMALIZED_HUNK_DIFF_FILES.add(normalized);
1564
+ return normalized;
1565
+ }
1566
+ function resolveHunkDiffFile(input) {
1567
+ if (NORMALIZED_HUNK_DIFF_FILES.has(input)) {
1568
+ return input;
1569
+ }
1570
+ return createHunkDiffFile(input);
1571
+ }
1572
+ function toInternalDiffFile(diff) {
1573
+ const normalized = resolveHunkDiffFile(diff);
1574
+ const patch = normalized.patch ?? "";
1575
+ return {
1576
+ agent: null,
1577
+ id: normalized.id,
1578
+ isBinary: normalized.isBinary ?? patchLooksBinary(patch),
1579
+ isTooLarge: normalized.isTooLarge,
1580
+ isUntracked: normalized.isUntracked,
1581
+ language: normalized.language,
1582
+ metadata: normalized.metadata,
1583
+ patch,
1584
+ path: normalized.path ?? normalized.metadata.name,
1585
+ previousPath: normalized.previousPath,
1586
+ stats: normalized.stats,
1587
+ statsTruncated: normalized.statsTruncated
1588
+ };
1589
+ }
1590
+ function createHunkDiffFilesFromPatch(patchText, sourceId = "patch") {
1591
+ const chunks = splitPatchIntoFileChunks(patchText);
1592
+ return parsePatchFiles(patchText, sourceId, true).flatMap((entry) => entry.files).map((metadata, index) => createHunkDiffFile({
1593
+ id: `${sourceId}:${index}:${normalizeDiffPath(metadata.name) ?? metadata.name}`,
1594
+ metadata,
1595
+ patch: findPatchChunk(metadata, chunks, index)
1596
+ }));
1597
+ }
1598
+ function toInternalDiffFiles(files) {
1599
+ return files.map(toInternalDiffFile);
1600
+ }
1601
+
1602
+ // src/opentui/HunkDiffBody.tsx
1603
+ import { jsxDEV as jsxDEV2 } from "@opentui/react/jsx-dev-runtime";
1604
+ var EMPTY_ANNOTATED_HUNK_INDICES = new Set;
1605
+ function HunkDiffBody({
1606
+ file,
1607
+ layout = "split",
1608
+ width,
1609
+ theme = "graphite",
1610
+ showLineNumbers = true,
1611
+ showHunkHeaders = true,
1612
+ wrapLines = false,
1613
+ horizontalOffset = 0,
1614
+ highlight = true,
1615
+ selectedHunkIndex = 0
1616
+ }) {
1617
+ const resolvedTheme = resolveTheme(theme, null);
1618
+ const internalFile = useMemo(() => file ? toInternalDiffFile(file) : undefined, [file]);
1619
+ const resolvedHighlighted = useHighlightedDiff({
1620
+ file: internalFile,
1621
+ appearance: resolvedTheme.appearance,
1622
+ shouldLoadHighlight: highlight
1623
+ });
1624
+ const rows = useMemo(() => internalFile ? layout === "split" ? buildSplitRows(internalFile, resolvedHighlighted, resolvedTheme) : buildStackRows(internalFile, resolvedHighlighted, resolvedTheme) : [], [internalFile, layout, resolvedHighlighted, resolvedTheme]);
1625
+ const lineNumberDigits = useMemo(() => String(internalFile ? findMaxLineNumber(internalFile) : 1).length, [internalFile]);
1626
+ if (!internalFile) {
1627
+ return /* @__PURE__ */ jsxDEV2("box", {
1628
+ style: { width: "100%", paddingLeft: 1, paddingRight: 1 },
1629
+ children: /* @__PURE__ */ jsxDEV2("text", {
1630
+ fg: resolvedTheme.muted,
1631
+ children: fitText("No file selected.", Math.max(1, width - 2))
1632
+ }, undefined, false, undefined, this)
1633
+ }, undefined, false, undefined, this);
1634
+ }
1635
+ if (internalFile.metadata.hunks.length === 0) {
1636
+ return /* @__PURE__ */ jsxDEV2("box", {
1637
+ style: { width: "100%", paddingLeft: 1, paddingRight: 1, paddingBottom: 1 },
1638
+ children: /* @__PURE__ */ jsxDEV2("text", {
1639
+ fg: resolvedTheme.muted,
1640
+ children: fitText(diffMessage(internalFile), Math.max(1, width - 2))
1641
+ }, undefined, false, undefined, this)
1642
+ }, undefined, false, undefined, this);
1643
+ }
1644
+ return /* @__PURE__ */ jsxDEV2("box", {
1645
+ style: { width: "100%", flexDirection: "column" },
1646
+ children: rows.map((row) => /* @__PURE__ */ jsxDEV2("box", {
1647
+ style: { width: "100%", flexDirection: "column" },
1648
+ children: /* @__PURE__ */ jsxDEV2(DiffRowView, {
1649
+ row,
1650
+ width,
1651
+ lineNumberDigits,
1652
+ showLineNumbers,
1653
+ showHunkHeaders,
1654
+ wrapLines,
1655
+ codeHorizontalOffset: horizontalOffset,
1656
+ theme: resolvedTheme,
1657
+ selected: row.hunkIndex === selectedHunkIndex,
1658
+ annotated: EMPTY_ANNOTATED_HUNK_INDICES.has(row.hunkIndex)
1659
+ }, undefined, false, undefined, this)
1660
+ }, row.key, false, undefined, this))
1661
+ }, undefined, false, undefined, this);
1662
+ }
1663
+ // src/opentui/HunkDiffFileHeader.tsx
1664
+ import { useMemo as useMemo2 } from "react";
1665
+
1666
+ // src/ui/lib/files.ts
1667
+ import { basename, dirname } from "node:path/posix";
1668
+ function sidebarFileName(file) {
1669
+ const path = normalizeDiffPath(file.path) ?? file.path;
1670
+ const previousPath = normalizeDiffPath(file.previousPath);
1671
+ if (!previousPath || previousPath === path) {
1672
+ return basename(path);
1673
+ }
1674
+ const previousName = basename(previousPath);
1675
+ const nextName = basename(path);
1676
+ return previousName === nextName ? nextName : `${previousName} -> ${nextName}`;
1677
+ }
1678
+ function formatSidebarStat(prefix, value, truncated = false) {
1679
+ return value > 0 ? `${prefix}${value}${truncated ? "+" : ""}` : null;
1680
+ }
1681
+ function sidebarEntryStats(entry) {
1682
+ const stats = [];
1683
+ if (entry.agentCommentsText) {
1684
+ stats.push({ kind: "agent-comment", text: entry.agentCommentsText });
1685
+ }
1686
+ if (entry.additionsText) {
1687
+ stats.push({ kind: "addition", text: entry.additionsText });
1688
+ }
1689
+ if (entry.deletionsText) {
1690
+ stats.push({ kind: "deletion", text: entry.deletionsText });
1691
+ }
1692
+ return stats;
1693
+ }
1694
+ function sidebarEntryStatsWidth(entry) {
1695
+ return sidebarEntryStats(entry).reduce((width, stat, index) => width + stat.text.length + (index > 0 ? 1 : 0), 0);
1696
+ }
1697
+ function buildSidebarEntries(files) {
1698
+ const entries = [];
1699
+ let activeGroup = null;
1700
+ files.forEach((file, index) => {
1701
+ const path = normalizeDiffPath(file.path) ?? file.path;
1702
+ const group = dirname(path);
1703
+ const nextGroup = group === "." ? null : group;
1704
+ if (nextGroup !== activeGroup) {
1705
+ activeGroup = nextGroup;
1706
+ if (activeGroup) {
1707
+ entries.push({
1708
+ kind: "group",
1709
+ id: `group:${activeGroup}:${index}`,
1710
+ label: `${activeGroup}/`
1711
+ });
1712
+ }
1713
+ }
1714
+ const agentCommentCount = file.agent?.annotations.length ?? 0;
1715
+ entries.push({
1716
+ kind: "file",
1717
+ id: file.id,
1718
+ name: sidebarFileName(file),
1719
+ agentCommentsText: agentCommentCount > 0 ? `*${agentCommentCount}` : null,
1720
+ additionsText: formatSidebarStat("+", file.stats.additions, file.statsTruncated),
1721
+ deletionsText: formatSidebarStat("-", file.stats.deletions),
1722
+ changeType: file.metadata.type,
1723
+ isUntracked: file.isUntracked ?? false
1724
+ });
1725
+ });
1726
+ return entries;
1727
+ }
1728
+ function fileLabelParts(file) {
1729
+ if (!file) {
1730
+ return { filename: "No file selected", stateLabel: null };
1731
+ }
1732
+ const path = normalizeDiffPath(file.path) ?? file.path;
1733
+ const previousPath = normalizeDiffPath(file.previousPath);
1734
+ const baseLabel = previousPath && previousPath !== path ? `${previousPath} -> ${path}` : path;
1735
+ let stateLabel = null;
1736
+ if (file.isUntracked) {
1737
+ stateLabel = " (untracked)";
1738
+ } else if (file.metadata.type === "new") {
1739
+ stateLabel = " (new)";
1740
+ } else if (file.metadata.type === "deleted") {
1741
+ stateLabel = " (deleted)";
1742
+ }
1743
+ return { filename: baseLabel, stateLabel };
1744
+ }
1745
+
1746
+ // src/ui/lib/text.ts
1747
+ function fitText2(text, width) {
1748
+ if (width <= 0) {
1749
+ return "";
1750
+ }
1751
+ if (text.length <= width) {
1752
+ return text;
1753
+ }
1754
+ if (width === 1) {
1755
+ return ".";
1756
+ }
1757
+ return `${text.slice(0, width - 1)}.`;
1758
+ }
1759
+ function padText(text, width) {
1760
+ const trimmed = fitText2(text, width);
1761
+ return trimmed.padEnd(width, " ");
1762
+ }
1763
+
1764
+ // src/ui/components/panes/DiffFileHeaderRow.tsx
1765
+ import { jsxDEV as jsxDEV3 } from "@opentui/react/jsx-dev-runtime";
1766
+ function DiffFileHeaderRow({
1767
+ file,
1768
+ headerLabelWidth,
1769
+ headerStatsWidth,
1770
+ theme,
1771
+ onSelect
1772
+ }) {
1773
+ const additionsText = `+${file.stats.additions}${file.statsTruncated ? "+" : ""}`;
1774
+ const deletionsText = `-${file.stats.deletions}`;
1775
+ const { filename, stateLabel } = fileLabelParts(file);
1776
+ return /* @__PURE__ */ jsxDEV3("box", {
1777
+ style: {
1778
+ width: "100%",
1779
+ height: 1,
1780
+ flexShrink: 0,
1781
+ flexDirection: "row",
1782
+ justifyContent: "space-between",
1783
+ paddingLeft: 1,
1784
+ paddingRight: 1,
1785
+ backgroundColor: theme.panel
1786
+ },
1787
+ onMouseUp: onSelect,
1788
+ children: [
1789
+ /* @__PURE__ */ jsxDEV3("box", {
1790
+ style: { flexDirection: "row" },
1791
+ children: [
1792
+ /* @__PURE__ */ jsxDEV3("text", {
1793
+ fg: theme.text,
1794
+ children: fitText2(filename, Math.max(1, headerLabelWidth - (stateLabel?.length ?? 0)))
1795
+ }, undefined, false, undefined, this),
1796
+ stateLabel && /* @__PURE__ */ jsxDEV3("text", {
1797
+ fg: theme.muted,
1798
+ children: stateLabel
1799
+ }, undefined, false, undefined, this)
1800
+ ]
1801
+ }, undefined, true, undefined, this),
1802
+ /* @__PURE__ */ jsxDEV3("box", {
1803
+ style: {
1804
+ width: headerStatsWidth,
1805
+ height: 1,
1806
+ flexDirection: "row",
1807
+ justifyContent: "flex-end"
1808
+ },
1809
+ children: [
1810
+ /* @__PURE__ */ jsxDEV3("text", {
1811
+ fg: theme.badgeAdded,
1812
+ children: additionsText
1813
+ }, undefined, false, undefined, this),
1814
+ /* @__PURE__ */ jsxDEV3("text", {
1815
+ fg: theme.muted,
1816
+ children: " "
1817
+ }, undefined, false, undefined, this),
1818
+ /* @__PURE__ */ jsxDEV3("text", {
1819
+ fg: theme.badgeRemoved,
1820
+ children: deletionsText
1821
+ }, undefined, false, undefined, this)
1822
+ ]
1823
+ }, undefined, true, undefined, this)
1824
+ ]
1825
+ }, undefined, true, undefined, this);
1826
+ }
1827
+
1828
+ // src/opentui/HunkDiffFileHeader.tsx
1829
+ import { jsxDEV as jsxDEV4 } from "@opentui/react/jsx-dev-runtime";
1830
+ function HunkDiffFileHeader({
1831
+ file,
1832
+ width,
1833
+ theme = "graphite",
1834
+ onSelect
1835
+ }) {
1836
+ const resolvedTheme = resolveTheme(theme, null);
1837
+ const internalFile = useMemo2(() => toInternalDiffFile(file), [file]);
1838
+ const headerStatsWidth = Math.max(7, `+${internalFile.stats.additions}${internalFile.statsTruncated ? "+" : ""} -${internalFile.stats.deletions}`.length);
1839
+ return /* @__PURE__ */ jsxDEV4(DiffFileHeaderRow, {
1840
+ file: internalFile,
1841
+ headerLabelWidth: Math.max(1, width - headerStatsWidth - 2),
1842
+ headerStatsWidth,
1843
+ theme: resolvedTheme,
1844
+ onSelect
1845
+ }, undefined, false, undefined, this);
1846
+ }
1847
+ // src/opentui/HunkDiffView.tsx
1848
+ import { jsxDEV as jsxDEV5 } from "@opentui/react/jsx-dev-runtime";
1849
+ function HunkDiffView({ diff, scrollable = true, ...props }) {
1850
+ const content = /* @__PURE__ */ jsxDEV5(HunkDiffBody, {
1851
+ file: diff,
1852
+ ...props
1853
+ }, undefined, false, undefined, this);
1854
+ if (!scrollable) {
1855
+ return content;
1856
+ }
1857
+ return /* @__PURE__ */ jsxDEV5("scrollbox", {
1858
+ width: "100%",
1859
+ height: "100%",
1860
+ scrollY: true,
1861
+ viewportCulling: true,
1862
+ focused: false,
1863
+ children: content
1864
+ }, undefined, false, undefined, this);
1865
+ }
1866
+ // src/opentui/HunkFileNav.tsx
1867
+ import { useMemo as useMemo3 } from "react";
1868
+
1869
+ // src/ui/lib/ids.ts
1870
+ function fileRowId(fileId) {
1871
+ return `file-row:${fileId}`;
1872
+ }
1873
+
1874
+ // src/ui/components/panes/FileListItem.tsx
1875
+ import { jsxDEV as jsxDEV6 } from "@opentui/react/jsx-dev-runtime";
1876
+ function getFileStateIcon(entry, theme) {
1877
+ if (entry.isUntracked) {
1878
+ return { icon: "?", color: theme.fileUntracked };
1879
+ }
1880
+ switch (entry.changeType) {
1881
+ case "new":
1882
+ return { icon: "A", color: theme.fileNew };
1883
+ case "deleted":
1884
+ return { icon: "D", color: theme.fileDeleted };
1885
+ case "rename-pure":
1886
+ case "rename-changed":
1887
+ return { icon: "R", color: theme.fileRenamed };
1888
+ case "change":
1889
+ return { icon: "M", color: theme.fileModified };
1890
+ default:
1891
+ return { icon: "", color: theme.text };
1892
+ }
1893
+ }
1894
+ function FileGroupHeader({
1895
+ entry,
1896
+ paddingLeft = 1,
1897
+ textWidth,
1898
+ theme
1899
+ }) {
1900
+ return /* @__PURE__ */ jsxDEV6("box", {
1901
+ style: {
1902
+ width: "100%",
1903
+ height: 1,
1904
+ paddingLeft,
1905
+ backgroundColor: theme.panel
1906
+ },
1907
+ children: /* @__PURE__ */ jsxDEV6("text", {
1908
+ fg: theme.muted,
1909
+ children: fitText2(entry.label, Math.max(1, textWidth))
1910
+ }, undefined, false, undefined, this)
1911
+ }, undefined, false, undefined, this);
1912
+ }
1913
+ function FileListItem({
1914
+ entry,
1915
+ paddingLeft = 1,
1916
+ selected,
1917
+ statsWidth,
1918
+ textWidth,
1919
+ theme,
1920
+ onSelect
1921
+ }) {
1922
+ const rowBackground = selected ? theme.panelAlt : theme.panel;
1923
+ const stats = sidebarEntryStats(entry);
1924
+ const { icon, color } = getFileStateIcon(entry, theme);
1925
+ const iconWidth = icon ? 2 : 0;
1926
+ const statsSectionWidth = statsWidth > 0 ? statsWidth + 1 : 0;
1927
+ const nameWidth = Math.max(1, textWidth - 1 - iconWidth - statsSectionWidth);
1928
+ return /* @__PURE__ */ jsxDEV6("box", {
1929
+ id: fileRowId(entry.id),
1930
+ style: {
1931
+ width: "100%",
1932
+ height: 1,
1933
+ backgroundColor: rowBackground,
1934
+ flexDirection: "row"
1935
+ },
1936
+ onMouseUp: onSelect,
1937
+ children: [
1938
+ /* @__PURE__ */ jsxDEV6("box", {
1939
+ style: {
1940
+ width: 1,
1941
+ height: 1,
1942
+ backgroundColor: selected ? theme.accent : rowBackground
1943
+ }
1944
+ }, undefined, false, undefined, this),
1945
+ /* @__PURE__ */ jsxDEV6("box", {
1946
+ style: {
1947
+ flexGrow: 1,
1948
+ height: 1,
1949
+ paddingLeft,
1950
+ flexDirection: "row",
1951
+ backgroundColor: rowBackground
1952
+ },
1953
+ children: [
1954
+ icon && /* @__PURE__ */ jsxDEV6("text", {
1955
+ fg: color,
1956
+ children: [
1957
+ icon,
1958
+ " "
1959
+ ]
1960
+ }, undefined, true, undefined, this),
1961
+ /* @__PURE__ */ jsxDEV6("text", {
1962
+ fg: theme.text,
1963
+ children: padText(fitText2(entry.name, nameWidth), nameWidth)
1964
+ }, undefined, false, undefined, this),
1965
+ statsSectionWidth > 0 && /* @__PURE__ */ jsxDEV6("box", {
1966
+ style: {
1967
+ width: statsSectionWidth,
1968
+ height: 1,
1969
+ flexDirection: "row",
1970
+ justifyContent: "flex-end",
1971
+ backgroundColor: rowBackground
1972
+ },
1973
+ children: stats.map((stat, index) => /* @__PURE__ */ jsxDEV6("box", {
1974
+ style: { height: 1, flexDirection: "row", backgroundColor: rowBackground },
1975
+ children: [
1976
+ index > 0 && /* @__PURE__ */ jsxDEV6("text", {
1977
+ fg: selected ? theme.text : theme.muted,
1978
+ children: " "
1979
+ }, undefined, false, undefined, this),
1980
+ /* @__PURE__ */ jsxDEV6("text", {
1981
+ fg: stat.kind === "agent-comment" ? theme.noteBorder : stat.kind === "addition" ? theme.badgeAdded : theme.badgeRemoved,
1982
+ children: stat.text
1983
+ }, undefined, false, undefined, this)
1984
+ ]
1985
+ }, `${entry.id}:${stat.kind}`, true, undefined, this))
1986
+ }, undefined, false, undefined, this)
1987
+ ]
1988
+ }, undefined, true, undefined, this)
1989
+ ]
1990
+ }, undefined, true, undefined, this);
1991
+ }
1992
+
1993
+ // src/opentui/HunkFileNav.tsx
1994
+ import { jsxDEV as jsxDEV7 } from "@opentui/react/jsx-dev-runtime";
1995
+ function HunkFileNav({
1996
+ files,
1997
+ selectedFileId,
1998
+ width,
1999
+ theme = "graphite",
2000
+ onSelectFile = () => {}
2001
+ }) {
2002
+ const resolvedTheme = resolveTheme(theme, null);
2003
+ const internalFiles = useMemo3(() => toInternalDiffFiles(files), [files]);
2004
+ const entries = useMemo3(() => buildSidebarEntries(internalFiles), [internalFiles]);
2005
+ const fileEntries = entries.filter((entry) => entry.kind === "file");
2006
+ const statsWidth = Math.max(0, ...fileEntries.map((entry) => sidebarEntryStatsWidth(entry)));
2007
+ const textWidth = Math.max(1, width - 1);
2008
+ return /* @__PURE__ */ jsxDEV7("box", {
2009
+ style: { width: "100%", flexDirection: "column", backgroundColor: resolvedTheme.panel },
2010
+ children: entries.map((entry) => entry.kind === "group" ? /* @__PURE__ */ jsxDEV7(FileGroupHeader, {
2011
+ entry,
2012
+ paddingLeft: 0,
2013
+ textWidth: Math.max(1, width),
2014
+ theme: resolvedTheme
2015
+ }, entry.id, false, undefined, this) : /* @__PURE__ */ jsxDEV7(FileListItem, {
2016
+ entry,
2017
+ paddingLeft: 0,
2018
+ selected: entry.id === selectedFileId,
2019
+ statsWidth,
2020
+ textWidth,
2021
+ theme: resolvedTheme,
2022
+ onSelect: () => onSelectFile(entry.id)
2023
+ }, entry.id, false, undefined, this))
2024
+ }, undefined, false, undefined, this);
2025
+ }
2026
+ // src/opentui/HunkReviewStream.tsx
2027
+ import { jsxDEV as jsxDEV8 } from "@opentui/react/jsx-dev-runtime";
2028
+ function resolveSelection(files, selection) {
2029
+ if (selection && files.some((file) => file.id === selection.fileId)) {
2030
+ return selection;
2031
+ }
2032
+ const first = files[0];
2033
+ return first ? { fileId: first.id, hunkIndex: 0 } : undefined;
2034
+ }
2035
+ function HunkReviewStream({
2036
+ files,
2037
+ layout = "split",
2038
+ width,
2039
+ theme = "graphite",
2040
+ selection,
2041
+ showFileHeaders = true,
2042
+ showFileSeparators = true,
2043
+ showLineNumbers = true,
2044
+ showHunkHeaders = true,
2045
+ wrapLines = false,
2046
+ horizontalOffset = 0,
2047
+ highlight = true,
2048
+ onSelectionChange
2049
+ }) {
2050
+ const resolvedTheme = resolveTheme(theme, null);
2051
+ const activeSelection = resolveSelection(files, selection);
2052
+ if (files.length === 0) {
2053
+ return /* @__PURE__ */ jsxDEV8("box", {
2054
+ style: { width: "100%", paddingLeft: 1, paddingRight: 1 },
2055
+ children: /* @__PURE__ */ jsxDEV8("text", {
2056
+ fg: resolvedTheme.muted,
2057
+ children: "No files to render."
2058
+ }, undefined, false, undefined, this)
2059
+ }, undefined, false, undefined, this);
2060
+ }
2061
+ return /* @__PURE__ */ jsxDEV8("box", {
2062
+ style: { width: "100%", flexDirection: "column", backgroundColor: resolvedTheme.panel },
2063
+ children: files.map((file, index) => {
2064
+ const selectedHunkIndex = activeSelection?.fileId === file.id ? activeSelection.hunkIndex : -1;
2065
+ return /* @__PURE__ */ jsxDEV8("box", {
2066
+ style: {
2067
+ width: "100%",
2068
+ flexDirection: "column",
2069
+ backgroundColor: resolvedTheme.panel
2070
+ },
2071
+ children: [
2072
+ showFileSeparators && index > 0 ? /* @__PURE__ */ jsxDEV8("box", {
2073
+ style: { width: "100%", height: 1, paddingLeft: 1, paddingRight: 1 },
2074
+ children: /* @__PURE__ */ jsxDEV8("text", {
2075
+ fg: resolvedTheme.border,
2076
+ children: "─".repeat(Math.max(1, width - 2))
2077
+ }, undefined, false, undefined, this)
2078
+ }, undefined, false, undefined, this) : null,
2079
+ showFileHeaders ? /* @__PURE__ */ jsxDEV8(HunkDiffFileHeader, {
2080
+ file,
2081
+ width,
2082
+ theme,
2083
+ onSelect: () => onSelectionChange?.({ fileId: file.id, hunkIndex: 0 })
2084
+ }, undefined, false, undefined, this) : null,
2085
+ /* @__PURE__ */ jsxDEV8(HunkDiffBody, {
2086
+ file,
2087
+ layout,
2088
+ width,
2089
+ theme,
2090
+ showLineNumbers,
2091
+ showHunkHeaders,
2092
+ wrapLines,
2093
+ horizontalOffset,
2094
+ highlight,
2095
+ selectedHunkIndex
2096
+ }, undefined, false, undefined, this)
2097
+ ]
2098
+ }, file.id, true, undefined, this);
2099
+ })
2100
+ }, undefined, false, undefined, this);
2101
+ }
2102
+ export {
2103
+ parsePatchFiles2 as parsePatchFiles,
2104
+ parseDiffFromFile,
2105
+ createHunkDiffFilesFromPatch,
2106
+ createHunkDiffFile,
2107
+ countHunkDiffStats,
2108
+ HunkReviewStream,
2109
+ HunkFileNav,
2110
+ HunkDiffView,
2111
+ HunkDiffFileHeader,
2112
+ HunkDiffBody,
2113
+ HUNK_DIFF_THEME_NAMES
2114
+ };