@astrosheep/keiyaku 0.1.47 → 0.1.49

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.
Files changed (58) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/round-runner.js +1 -1
  3. package/build/agents/selector.js +1 -1
  4. package/build/common/constants.js +6 -1
  5. package/build/common/errors.js +1 -1
  6. package/build/common/response-style.js +4 -1
  7. package/build/config/term-presets/constants.js +24 -0
  8. package/build/config/term-presets/default-preset.js +119 -0
  9. package/build/config/term-presets/index.js +5 -0
  10. package/build/config/term-presets/mischief-preset.js +105 -0
  11. package/build/config/term-presets/pocket-preset.js +105 -0
  12. package/build/config/term-presets/resolver.js +52 -0
  13. package/build/config/term-presets/types.js +1 -0
  14. package/build/handlers/ask.js +10 -120
  15. package/build/handlers/close.js +2 -9
  16. package/build/handlers/drive.js +1 -15
  17. package/build/handlers/shared.js +1 -2
  18. package/build/handlers/start.js +0 -17
  19. package/build/handlers/status.js +2 -6
  20. package/build/index.js +2 -2
  21. package/build/types/git-diff.js +1 -0
  22. package/build/utils/ask-history.js +75 -0
  23. package/build/utils/draft.js +22 -0
  24. package/build/utils/git-diff/constants.js +9 -0
  25. package/build/utils/git-diff/filter.js +70 -0
  26. package/build/utils/git-diff/incremental.js +111 -0
  27. package/build/utils/git-diff/index.js +3 -0
  28. package/build/utils/git-diff/parsers.js +160 -0
  29. package/build/utils/git-diff/preview.js +157 -0
  30. package/build/utils/git-diff/stat.js +27 -0
  31. package/build/utils/git-diff/types.js +1 -0
  32. package/build/utils/git-ops.js +9 -0
  33. package/build/utils/git.js +1 -1
  34. package/build/utils/keiyaku-document/index.js +13 -0
  35. package/build/utils/keiyaku-document/lex.js +178 -0
  36. package/build/utils/keiyaku-document/parser.js +242 -0
  37. package/build/utils/keiyaku-document/render.js +68 -0
  38. package/build/utils/keiyaku-document/sections.js +105 -0
  39. package/build/utils/keiyaku-document/types.js +6 -0
  40. package/build/utils/keiyaku-document.test.js +12 -1
  41. package/build/utils/trace.js +6 -7
  42. package/build/workflow/ask-execution.js +81 -0
  43. package/build/workflow/drive.js +10 -5
  44. package/build/workflow/iterate-plan.js +1 -1
  45. package/build/workflow/keiyaku-draft.js +1 -1
  46. package/build/workflow/{contract.js → keiyaku.js} +2 -2
  47. package/build/workflow/markdown-normalization.js +3 -2
  48. package/build/workflow/present.js +68 -13
  49. package/build/workflow/response-builders.js +75 -44
  50. package/build/workflow/response-meta.js +15 -0
  51. package/build/workflow/response-renderer.js +2 -13
  52. package/build/workflow/round-summary.js +1 -1
  53. package/build/workflow/start.js +8 -3
  54. package/build/workflow/status.js +129 -62
  55. package/package.json +1 -1
  56. package/build/config/term-presets.js +0 -398
  57. package/build/utils/git-diff.js +0 -519
  58. package/build/utils/keiyaku-document.js +0 -539
@@ -1,519 +0,0 @@
1
- import { DEFAULT_INCREMENTAL_DIFF_MODE, MAX_TARGETED_DIFF_CHARS, } from "../common/constants.js";
2
- import { createGit, wrapGitError } from "./git-ops.js";
3
- const DIFF_EXCLUDES = [":(exclude)KEIYAKU.md", ":(exclude)KEIYAKU_TRACE.md"];
4
- // Diff preview limits (no env/config knobs on purpose).
5
- const DIFF_PREVIEW_MAX_FILES = 12;
6
- const DIFF_PREVIEW_MAX_HUNKS_PER_FILE = 4;
7
- const DIFF_PREVIEW_MAX_LINES_PER_HUNK = 80;
8
- const DIFF_PREVIEW_MAX_PRELUDE_LINES = 50;
9
- const DIFF_PREVIEW_TEXT_MAX_CHARS = 10000;
10
- const INCREMENTAL_DIFF_RANGE = "HEAD~1...HEAD";
11
- const TARGETED_DIFF_WARNING = "Warning: no valid diff coordinates found in KEIYAKU_TRACE.md; showing stat-only diff.";
12
- const NOT_SHOWN_IN_DETAIL_PREFIX = "--- not shown in detail: ";
13
- function buildDiffPathspec(baseBranch) {
14
- return [`${baseBranch}...HEAD`, "--", ".", ...DIFF_EXCLUDES];
15
- }
16
- function parseNumStat(content) {
17
- const rows = [];
18
- for (const line of content.split(/\r?\n/)) {
19
- if (!line.trim())
20
- continue;
21
- const [addRaw, delRaw, ...pathParts] = line.split("\t");
22
- const filePath = pathParts.join("\t").trim();
23
- if (!filePath)
24
- continue;
25
- const binary = addRaw === "-" || delRaw === "-";
26
- rows.push({
27
- path: filePath,
28
- additions: binary ? 0 : Number.parseInt(addRaw, 10) || 0,
29
- deletions: binary ? 0 : Number.parseInt(delRaw, 10) || 0,
30
- binary,
31
- });
32
- }
33
- return rows;
34
- }
35
- function parseNameStatus(content) {
36
- const map = new Map();
37
- for (const line of content.split(/\r?\n/)) {
38
- if (!line.trim())
39
- continue;
40
- const parts = line.split("\t");
41
- const statusRaw = (parts[0] ?? "").trim();
42
- if (!statusRaw)
43
- continue;
44
- if ((statusRaw.startsWith("R") || statusRaw.startsWith("C")) && parts.length >= 3) {
45
- const scoreRaw = statusRaw.slice(1);
46
- const score = Number.parseInt(scoreRaw, 10);
47
- const oldPath = (parts[1] ?? "").trim();
48
- const path = (parts[2] ?? "").trim();
49
- if (!path)
50
- continue;
51
- const status = statusRaw[0] === "R" ? "R" : "C";
52
- map.set(path, { status, score: Number.isFinite(score) ? score : 0, oldPath, path });
53
- continue;
54
- }
55
- const path = (parts[1] ?? "").trim();
56
- if (!path)
57
- continue;
58
- // Git can emit single-letter statuses, possibly in combinations; we keep just the first.
59
- const status = statusRaw[0];
60
- map.set(path, { status, path });
61
- }
62
- return map;
63
- }
64
- function splitDiffByFile(content) {
65
- const sections = [];
66
- let current = [];
67
- for (const line of content.split(/\r?\n/)) {
68
- if (line.startsWith("diff --git ")) {
69
- if (current.length > 0)
70
- sections.push(current);
71
- current = [line];
72
- continue;
73
- }
74
- if (current.length > 0)
75
- current.push(line);
76
- }
77
- if (current.length > 0)
78
- sections.push(current);
79
- return sections;
80
- }
81
- function parseUnifiedHunks(section) {
82
- const firstHunkIndex = section.findIndex((line) => line.startsWith("@@ "));
83
- if (firstHunkIndex === -1) {
84
- return { prelude: [...section], hunks: [] };
85
- }
86
- const prelude = section.slice(0, firstHunkIndex);
87
- const hunks = [];
88
- let current = [];
89
- for (const line of section.slice(firstHunkIndex)) {
90
- if (line.startsWith("@@ ")) {
91
- if (current.length > 0) {
92
- const parsed = parseHunkRange(current[0] ?? "");
93
- if (parsed) {
94
- hunks.push({
95
- header: current[0] ?? "",
96
- lines: current.slice(1),
97
- newStart: parsed.start,
98
- newEnd: parsed.end,
99
- });
100
- }
101
- }
102
- current = [line];
103
- continue;
104
- }
105
- if (current.length > 0)
106
- current.push(line);
107
- }
108
- if (current.length > 0) {
109
- const parsed = parseHunkRange(current[0] ?? "");
110
- if (parsed) {
111
- hunks.push({
112
- header: current[0] ?? "",
113
- lines: current.slice(1),
114
- newStart: parsed.start,
115
- newEnd: parsed.end,
116
- });
117
- }
118
- }
119
- return { prelude, hunks };
120
- }
121
- function parseHunkRange(hunkHeader) {
122
- const match = hunkHeader.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
123
- if (!match)
124
- return null;
125
- const start = Number.parseInt(match[1] ?? "", 10);
126
- const countRaw = match[2];
127
- const count = countRaw ? Number.parseInt(countRaw, 10) : 1;
128
- if (!Number.isFinite(start) || !Number.isFinite(count))
129
- return null;
130
- if (count <= 0)
131
- return { start, end: start - 1 };
132
- return { start, end: start + count - 1 };
133
- }
134
- function rangesOverlap(aStart, aEnd, bStart, bEnd) {
135
- return aStart <= bEnd && bStart <= aEnd;
136
- }
137
- function isDeletedDiffSection(section) {
138
- return section.some((line) => line.startsWith("deleted file mode ") || line.startsWith("+++ /dev/null"));
139
- }
140
- function buildCoordinateIndex(coordinates) {
141
- const index = new Map();
142
- for (const coordinate of coordinates) {
143
- const existing = index.get(coordinate.path) ?? [];
144
- existing.push(coordinate);
145
- index.set(coordinate.path, existing);
146
- }
147
- return index;
148
- }
149
- function capTargetedDiffText(diffText) {
150
- if (diffText.length <= MAX_TARGETED_DIFF_CHARS)
151
- return diffText;
152
- const cut = diffText.slice(0, MAX_TARGETED_DIFF_CHARS);
153
- return `${cut}\n...[truncated ${diffText.length - MAX_TARGETED_DIFF_CHARS} chars]...`;
154
- }
155
- export function filterDiffByCoordinates(rawPatch, coordinates) {
156
- const sections = splitDiffByFile(rawPatch);
157
- if (sections.length === 0)
158
- return "";
159
- const byPath = buildCoordinateIndex(coordinates);
160
- const selectedSectionsByPath = new Map();
161
- const deletedSections = [];
162
- const nonDeletedPathsInPatch = [];
163
- const sectionByPath = new Map();
164
- for (const section of sections) {
165
- const path = parseDiffPathFromHeader(section[0] ?? "");
166
- if (!path)
167
- continue;
168
- if (isDeletedDiffSection(section)) {
169
- deletedSections.push(section.join("\n"));
170
- continue;
171
- }
172
- if (!sectionByPath.has(path)) {
173
- nonDeletedPathsInPatch.push(path);
174
- sectionByPath.set(path, section);
175
- }
176
- }
177
- for (const coordinate of coordinates) {
178
- const section = sectionByPath.get(coordinate.path);
179
- if (!section || selectedSectionsByPath.has(coordinate.path))
180
- continue;
181
- const fileCoordinates = byPath.get(coordinate.path);
182
- if (!fileCoordinates || fileCoordinates.length === 0)
183
- continue;
184
- const { prelude, hunks } = parseUnifiedHunks(section);
185
- if (hunks.length === 0)
186
- continue;
187
- const matchingHunks = hunks.filter((hunk) => fileCoordinates.some((coord) => rangesOverlap(hunk.newStart, hunk.newEnd, coord.start, coord.end)));
188
- if (matchingHunks.length === 0)
189
- continue;
190
- const rendered = [
191
- ...prelude,
192
- ...matchingHunks.flatMap((hunk) => [hunk.header, ...hunk.lines]),
193
- ].join("\n");
194
- selectedSectionsByPath.set(coordinate.path, rendered);
195
- }
196
- const selectedSections = [...selectedSectionsByPath.values(), ...deletedSections];
197
- if (selectedSections.length === 0)
198
- return "";
199
- const filteredOutPaths = nonDeletedPathsInPatch.filter((path) => !selectedSectionsByPath.has(path));
200
- if (filteredOutPaths.length > 0) {
201
- selectedSections.push(`${NOT_SHOWN_IN_DETAIL_PREFIX}${filteredOutPaths.join(", ")}`);
202
- }
203
- return selectedSections.join("\n");
204
- }
205
- function totalJoinedLength(blocks) {
206
- if (blocks.length === 0)
207
- return 0;
208
- return blocks.reduce((sum, block) => sum + block.length, 0) + (blocks.length - 1);
209
- }
210
- function truncateBlockToLineBudget(block, maxChars) {
211
- if (maxChars <= 0)
212
- return "";
213
- if (block.length <= maxChars)
214
- return block;
215
- const lines = block.split("\n");
216
- const kept = [];
217
- let used = 0;
218
- for (const line of lines) {
219
- const delta = (kept.length > 0 ? 1 : 0) + line.length;
220
- if (used + delta > maxChars)
221
- break;
222
- kept.push(line);
223
- used += delta;
224
- }
225
- return kept.join("\n");
226
- }
227
- function isMissingBaseRevisionError(err) {
228
- const source = (err ?? {});
229
- const text = [source.message, source.stderr, source.stdErr, source.stdout, source.stdOut]
230
- .filter((value) => typeof value === "string" && value.length > 0)
231
- .join("\n");
232
- return (text.includes("bad revision") ||
233
- text.includes("unknown revision or path not in the working tree") ||
234
- text.includes("ambiguous argument"));
235
- }
236
- function renderStatDiffPreviewText(output) {
237
- const trimmed = output.trim();
238
- if (!trimmed)
239
- return "No diff.";
240
- if (trimmed.length <= DIFF_PREVIEW_TEXT_MAX_CHARS)
241
- return trimmed;
242
- const cut = trimmed.slice(0, DIFF_PREVIEW_TEXT_MAX_CHARS);
243
- return `${cut}\n...[truncated ${trimmed.length - DIFF_PREVIEW_TEXT_MAX_CHARS} chars]...`;
244
- }
245
- async function readStatDiff(git, cwd, range, pathspec) {
246
- let output = "";
247
- try {
248
- output = await git.raw(["diff", "--stat=120,80", "--compact-summary", range, ...pathspec]);
249
- }
250
- catch (err) {
251
- throw wrapGitError(`diff --stat=120,80 --compact-summary ${range}`, err, cwd);
252
- }
253
- return renderStatDiffPreviewText(output);
254
- }
255
- async function readIncrementalPatch(git, cwd) {
256
- try {
257
- return await git.raw([
258
- "diff",
259
- "--no-color",
260
- "--no-ext-diff",
261
- "--unified=3",
262
- INCREMENTAL_DIFF_RANGE,
263
- "--",
264
- ".",
265
- ...DIFF_EXCLUDES,
266
- ]);
267
- }
268
- catch (err) {
269
- if (isMissingBaseRevisionError(err)) {
270
- return null;
271
- }
272
- throw wrapGitError(`diff --no-color --no-ext-diff --unified=3 ${INCREMENTAL_DIFF_RANGE}`, err, cwd);
273
- }
274
- }
275
- function renderIncrementalUnifiedDiff(rawPatch) {
276
- const sections = splitDiffByFile(rawPatch);
277
- if (sections.length === 0)
278
- return "No changes in last round.";
279
- const MAX_TOTAL_CHARS = 15000;
280
- const MAX_LINES_PER_FILE = 90;
281
- const filePreviews = [];
282
- let omittedFiles = 0;
283
- for (let i = 0; i < sections.length; i += 1) {
284
- const section = sections[i];
285
- const fileName = parseDiffPathFromHeader(section[0] ?? "") ?? "unknown";
286
- const header = `--- ${fileName} ---`;
287
- const content = section.slice(0, MAX_LINES_PER_FILE);
288
- const isTruncated = section.length > MAX_LINES_PER_FILE;
289
- if (isTruncated) {
290
- content.push(`... [truncated ${section.length - MAX_LINES_PER_FILE} lines for this file]`);
291
- }
292
- const fileBlock = `${header}\n${content.join("\n")}\n`;
293
- const projected = totalJoinedLength([...filePreviews, fileBlock]);
294
- if (projected <= MAX_TOTAL_CHARS) {
295
- filePreviews.push(fileBlock);
296
- continue;
297
- }
298
- omittedFiles = sections.length - i;
299
- if (filePreviews.length === 0) {
300
- const notice = `... [omitted ${omittedFiles} file(s) to stay under ${MAX_TOTAL_CHARS} chars] ...`;
301
- const partialBudget = Math.max(0, MAX_TOTAL_CHARS - notice.length - 1);
302
- const partial = truncateBlockToLineBudget(fileBlock, partialBudget);
303
- if (partial.trim().length > 0) {
304
- filePreviews.push(partial);
305
- }
306
- }
307
- break;
308
- }
309
- if (omittedFiles > 0) {
310
- const notice = `... [omitted ${omittedFiles} file(s) to stay under ${MAX_TOTAL_CHARS} chars] ...`;
311
- while (filePreviews.length > 0 && totalJoinedLength([...filePreviews, notice]) > MAX_TOTAL_CHARS) {
312
- filePreviews.pop();
313
- }
314
- if (totalJoinedLength([...filePreviews, notice]) <= MAX_TOTAL_CHARS) {
315
- filePreviews.push(notice);
316
- }
317
- }
318
- return filePreviews.join("\n");
319
- }
320
- async function getIncrementalUnifiedDiff(git, cwd) {
321
- const rawPatch = await readIncrementalPatch(git, cwd);
322
- if (rawPatch === null)
323
- return "No incremental diff available.";
324
- return renderIncrementalUnifiedDiff(rawPatch);
325
- }
326
- async function getIncrementalStatDiff(git, cwd) {
327
- let output = "";
328
- try {
329
- output = await git.raw(["diff", "--numstat", INCREMENTAL_DIFF_RANGE, "--", ".", ...DIFF_EXCLUDES]);
330
- }
331
- catch (err) {
332
- if (isMissingBaseRevisionError(err))
333
- return "No incremental diff available.";
334
- throw wrapGitError(`diff --numstat ${INCREMENTAL_DIFF_RANGE}`, err, cwd);
335
- }
336
- return renderStatDiffPreviewText(output);
337
- }
338
- export async function getIncrementalDiff(cwd, mode = DEFAULT_INCREMENTAL_DIFF_MODE, coordinates) {
339
- const git = createGit(cwd);
340
- const renderByMode = {
341
- unified: () => getIncrementalUnifiedDiff(git, cwd),
342
- stat: () => getIncrementalStatDiff(git, cwd),
343
- targeted: async () => {
344
- const stat = await getIncrementalStatDiff(git, cwd);
345
- if (!coordinates || coordinates.length === 0) {
346
- return `${stat}\n\n${TARGETED_DIFF_WARNING}`;
347
- }
348
- const rawPatch = await readIncrementalPatch(git, cwd);
349
- if (rawPatch === null)
350
- return stat;
351
- const targetedPatch = filterDiffByCoordinates(rawPatch, coordinates).trim();
352
- if (!targetedPatch) {
353
- return `${stat}\n\n${TARGETED_DIFF_WARNING}`;
354
- }
355
- return `${stat}\n\n${capTargetedDiffText(targetedPatch)}`;
356
- },
357
- };
358
- return renderByMode[mode]();
359
- }
360
- export async function getDiffStats(cwd, baseBranch) {
361
- const git = createGit(cwd);
362
- const pathspec = buildDiffPathspec(baseBranch);
363
- let rawNumStat;
364
- try {
365
- rawNumStat = await git.raw(["diff", "--numstat", ...pathspec]);
366
- }
367
- catch (err) {
368
- throw wrapGitError(`diff --numstat ${baseBranch}...HEAD`, err, cwd);
369
- }
370
- const rows = parseNumStat(rawNumStat);
371
- return {
372
- filesChanged: rows.length,
373
- insertions: rows.reduce((sum, row) => sum + row.additions, 0),
374
- deletions: rows.reduce((sum, row) => sum + row.deletions, 0),
375
- };
376
- }
377
- export async function getDiffPreviewText(cwd, baseBranch) {
378
- const git = createGit(cwd);
379
- const range = `${baseBranch}...HEAD`;
380
- return readStatDiff(git, cwd, range, ["--", ".", ...DIFF_EXCLUDES]);
381
- }
382
- function parseDiffPathFromHeader(diffGitLine) {
383
- // Format: diff --git a/<path> b/<path>
384
- const parts = diffGitLine.split(" ");
385
- const bPart = parts[3] ?? "";
386
- if (!bPart.startsWith("b/"))
387
- return null;
388
- return bPart.slice(2);
389
- }
390
- function buildPatchPreview(section) {
391
- // Keep a small, representative prelude (diff header, index, ---/+++), then first N hunks.
392
- const firstHunkIdx = section.findIndex((l) => l.startsWith("@@ "));
393
- const prelude = firstHunkIdx === -1 ? section : section.slice(0, firstHunkIdx).slice(0, DIFF_PREVIEW_MAX_PRELUDE_LINES);
394
- if (firstHunkIdx === -1) {
395
- const truncated = section.length > DIFF_PREVIEW_MAX_PRELUDE_LINES;
396
- const lines = truncated
397
- ? [...prelude, `... [truncated ${section.length - prelude.length} prelude line(s)]`]
398
- : prelude;
399
- return { patch: lines.join("\n"), truncated };
400
- }
401
- const hunks = [];
402
- let current = [];
403
- for (const line of section.slice(firstHunkIdx)) {
404
- if (line.startsWith("@@ ")) {
405
- if (current.length > 0)
406
- hunks.push(current);
407
- current = [line];
408
- continue;
409
- }
410
- if (current.length > 0)
411
- current.push(line);
412
- }
413
- if (current.length > 0)
414
- hunks.push(current);
415
- let truncated = false;
416
- const shownHunks = hunks.slice(0, DIFF_PREVIEW_MAX_HUNKS_PER_FILE).map((hunk) => {
417
- if (hunk.length <= DIFF_PREVIEW_MAX_LINES_PER_HUNK)
418
- return hunk;
419
- truncated = true;
420
- return [
421
- ...hunk.slice(0, DIFF_PREVIEW_MAX_LINES_PER_HUNK),
422
- `... [truncated ${hunk.length - DIFF_PREVIEW_MAX_LINES_PER_HUNK} line(s) in this hunk]`,
423
- ];
424
- });
425
- if (hunks.length > DIFF_PREVIEW_MAX_HUNKS_PER_FILE) {
426
- truncated = true;
427
- }
428
- const lines = [
429
- ...prelude,
430
- ...shownHunks.flat(),
431
- ...(hunks.length > DIFF_PREVIEW_MAX_HUNKS_PER_FILE
432
- ? [`... [omitted ${hunks.length - DIFF_PREVIEW_MAX_HUNKS_PER_FILE} hunk(s)]`]
433
- : []),
434
- ];
435
- return { patch: lines.join("\n"), truncated };
436
- }
437
- /**
438
- * Gets a compact, structured diff preview for quick review.
439
- * - Always includes full stats for the whole range.
440
- * - Includes patch previews for the top churn files only (hunk-based truncation).
441
- */
442
- export async function getDiff(cwd, baseBranch) {
443
- const git = createGit(cwd);
444
- const pathspec = buildDiffPathspec(baseBranch);
445
- const range = `${baseBranch}...HEAD`;
446
- let rawNumStat;
447
- try {
448
- rawNumStat = await git.raw(["diff", "--numstat", ...pathspec]);
449
- }
450
- catch (err) {
451
- throw wrapGitError(`diff --numstat ${range}`, err);
452
- }
453
- const rows = parseNumStat(rawNumStat);
454
- const stats = {
455
- filesChanged: rows.length,
456
- insertions: rows.reduce((sum, row) => sum + row.additions, 0),
457
- deletions: rows.reduce((sum, row) => sum + row.deletions, 0),
458
- };
459
- let rawNameStatus = "";
460
- try {
461
- rawNameStatus = await git.raw(["diff", "--name-status", ...pathspec]);
462
- }
463
- catch (err) {
464
- throw wrapGitError(`diff --name-status ${range}`, err);
465
- }
466
- const statusByPath = parseNameStatus(rawNameStatus);
467
- const sorted = [...rows].sort((a, b) => b.additions + b.deletions - (a.additions + a.deletions));
468
- const selected = sorted.slice(0, DIFF_PREVIEW_MAX_FILES);
469
- const omittedFileCount = Math.max(0, rows.length - selected.length);
470
- const selectedPaths = selected.map((r) => r.path);
471
- let rawPatch = "";
472
- if (selectedPaths.length > 0) {
473
- try {
474
- rawPatch = await git.raw([
475
- "diff",
476
- "--no-color",
477
- "--no-ext-diff",
478
- "--unified=3",
479
- range,
480
- "--",
481
- ...selectedPaths,
482
- ...DIFF_EXCLUDES,
483
- ]);
484
- }
485
- catch (err) {
486
- throw wrapGitError(`diff --no-color --no-ext-diff --unified=3 ${range} -- <top files>`, err);
487
- }
488
- }
489
- const sections = rawPatch ? splitDiffByFile(rawPatch) : [];
490
- const patchByPath = new Map();
491
- for (const section of sections) {
492
- const path = parseDiffPathFromHeader(section[0] ?? "");
493
- if (!path)
494
- continue;
495
- patchByPath.set(path, buildPatchPreview(section));
496
- }
497
- const files = selected.map((row) => {
498
- const status = statusByPath.get(row.path);
499
- const patch = patchByPath.get(row.path)?.patch ?? "";
500
- const truncated = patchByPath.get(row.path)?.truncated ?? false;
501
- return {
502
- path: row.path,
503
- status: status?.status ?? "M",
504
- oldPath: status && (status.status === "R" || status.status === "C") ? status.oldPath : undefined,
505
- additions: row.additions,
506
- deletions: row.deletions,
507
- binary: row.binary,
508
- patch,
509
- truncated,
510
- };
511
- });
512
- return {
513
- range,
514
- excludes: DIFF_EXCLUDES,
515
- stats,
516
- files,
517
- omittedFileCount,
518
- };
519
- }