pi-openai-codex-compat 0.0.6 → 0.0.7

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.
@@ -10,8 +10,24 @@ import {
10
10
  import {
11
11
  type AppliedPatchChange,
12
12
  type ApplyPatchDetails,
13
+ type ApplyPatchFinalPathState,
14
+ type ApplyPatchFailureDetails,
15
+ type ApplyPatchInstructionDetails,
16
+ type ApplyPatchInstructionEffect,
17
+ type ApplyPatchInstructionReason,
18
+ type ApplyPatchInstructionStatus,
19
+ applyPatchHasOtherFilesystemChanges,
20
+ applyPatchNeedsInstructionResults,
21
+ applyPatchSummaryPaths,
13
22
  coalesceAppliedPatchChangesForRendering,
23
+ formatApplyPatchFailureHeading,
24
+ formatApplyPatchInstructionFeedback,
25
+ formatApplyPatchInstructionStatusLabel,
14
26
  } from "./apply-patch-engine.ts";
27
+ import type {
28
+ FormatterMatchCandidateRange,
29
+ FormatterMatchFailureDetails,
30
+ } from "./apply-patch-matcher.ts";
15
31
  import { usesLightToolPalette } from "./codex-tool-surface.ts";
16
32
 
17
33
  type DiffLineKind = "add" | "delete" | "context";
@@ -73,6 +89,10 @@ function displayPath(path: string, cwd: string): string {
73
89
  }
74
90
 
75
91
  function changePath(change: AppliedPatchChange, cwd: string): string {
92
+ if (change.kind === "move") {
93
+ const path = `${displayPath(change.sourcePath, cwd)} → ${displayPath(change.destinationPath, cwd)}`;
94
+ return change.replacedDestination ? `${path} (replaced destination)` : path;
95
+ }
76
96
  const path = displayPath(change.path, cwd);
77
97
  return change.kind === "update" && change.moveTo
78
98
  ? `${path} → ${displayPath(change.moveTo, cwd)}`
@@ -84,12 +104,21 @@ function changeVerb(change: AppliedPatchChange): string {
84
104
  case "add":
85
105
  return "Added";
86
106
  case "delete":
87
- return "Deleted";
107
+ return change.entryType === "symlink" ? "Deleted symlink" : "Deleted";
108
+ case "move":
109
+ return "Moved";
88
110
  case "update":
89
111
  return "Edited";
90
112
  }
91
113
  }
92
114
 
115
+ function changeListPath(change: AppliedPatchChange, cwd: string): string {
116
+ const path = changePath(change, cwd);
117
+ return change.kind === "delete" && change.entryType === "symlink"
118
+ ? `${path} (deleted symlink)`
119
+ : path;
120
+ }
121
+
93
122
  function countSummary(additions: number, deletions: number, theme: Theme): string {
94
123
  return `(${theme.fg("success", `+${additions}`)} ${theme.fg("error", `-${deletions}`)})`;
95
124
  }
@@ -109,6 +138,11 @@ function isAppliedPatchChange(value: unknown): value is AppliedPatchChange {
109
138
  kind?: unknown;
110
139
  path?: unknown;
111
140
  moveTo?: unknown;
141
+ sourcePath?: unknown;
142
+ destinationPath?: unknown;
143
+ replacedDestination?: unknown;
144
+ entryType?: unknown;
145
+ exact?: unknown;
112
146
  content?: unknown;
113
147
  oldContent?: unknown;
114
148
  newContent?: unknown;
@@ -117,15 +151,27 @@ function isAppliedPatchChange(value: unknown): value is AppliedPatchChange {
117
151
  deletions?: unknown;
118
152
  };
119
153
  if (
120
- typeof change.path !== "string" ||
121
154
  typeof change.displayDiff !== "string" ||
122
155
  typeof change.additions !== "number" ||
123
156
  typeof change.deletions !== "number"
124
157
  ) {
125
158
  return false;
126
159
  }
160
+ if (change.kind === "move") {
161
+ return (
162
+ typeof change.sourcePath === "string" &&
163
+ typeof change.destinationPath === "string" &&
164
+ typeof change.replacedDestination === "boolean" &&
165
+ (change.entryType === "regular-file" || change.entryType === "symlink") &&
166
+ typeof change.exact === "boolean"
167
+ );
168
+ }
169
+ if (typeof change.path !== "string") return false;
127
170
  if (change.kind === "add" || change.kind === "delete") {
128
- return typeof change.content === "string";
171
+ return change.kind === "delete"
172
+ ? (change.entryType === "regular-file" || change.entryType === "symlink") &&
173
+ (change.content === undefined || typeof change.content === "string")
174
+ : typeof change.content === "string";
129
175
  }
130
176
  return (
131
177
  change.kind === "update" &&
@@ -135,6 +181,244 @@ function isAppliedPatchChange(value: unknown): value is AppliedPatchChange {
135
181
  );
136
182
  }
137
183
 
184
+ const INSTRUCTION_STATUSES = new Set<ApplyPatchInstructionStatus>([
185
+ "applied",
186
+ "planned",
187
+ "no-op",
188
+ "dead",
189
+ "failed",
190
+ "not-run",
191
+ ]);
192
+
193
+ const INSTRUCTION_REASON_CODES = new Set<ApplyPatchInstructionReason["code"]>([
194
+ "empty-update",
195
+ "identity-update",
196
+ "content-already-present",
197
+ "update-result-unchanged",
198
+ "path-already-absent",
199
+ "same-entry-move",
200
+ "move-already-fulfilled",
201
+ "dead-dominated",
202
+ ]);
203
+
204
+ function isApplyPatchInstructionReason(value: unknown): value is ApplyPatchInstructionReason {
205
+ if (typeof value !== "object" || value === null) return false;
206
+ const reason = value as {
207
+ code?: unknown;
208
+ message?: unknown;
209
+ dominatingInstructions?: unknown;
210
+ relatedInstructions?: unknown;
211
+ };
212
+ return (
213
+ typeof reason.code === "string" &&
214
+ INSTRUCTION_REASON_CODES.has(reason.code as ApplyPatchInstructionReason["code"]) &&
215
+ typeof reason.message === "string" &&
216
+ (reason.dominatingInstructions === undefined ||
217
+ (Array.isArray(reason.dominatingInstructions) &&
218
+ reason.dominatingInstructions.every((index) => typeof index === "number"))) &&
219
+ (reason.relatedInstructions === undefined ||
220
+ (Array.isArray(reason.relatedInstructions) &&
221
+ reason.relatedInstructions.every((index) => typeof index === "number"))) &&
222
+ (reason.code !== "move-already-fulfilled" ||
223
+ (Array.isArray(reason.relatedInstructions) && reason.relatedInstructions.length === 1))
224
+ );
225
+ }
226
+
227
+ const INSTRUCTION_EFFECT_KINDS = new Set<ApplyPatchInstructionEffect["kind"]>([
228
+ "created",
229
+ "replaced",
230
+ "updated",
231
+ "deleted",
232
+ "directory-created",
233
+ "temporary-entry-remains",
234
+ "source-remains",
235
+ "symlink-removed",
236
+ "symlink-moved",
237
+ "symlink-target-modified",
238
+ ]);
239
+
240
+ function isApplyPatchInstructionEffect(value: unknown): value is ApplyPatchInstructionEffect {
241
+ if (typeof value !== "object" || value === null) return false;
242
+ const effect = value as {
243
+ kind?: unknown;
244
+ path?: unknown;
245
+ previousEntry?: unknown;
246
+ replacementEntry?: unknown;
247
+ target?: unknown;
248
+ };
249
+ if (
250
+ typeof effect.kind !== "string" ||
251
+ !INSTRUCTION_EFFECT_KINDS.has(effect.kind as ApplyPatchInstructionEffect["kind"]) ||
252
+ typeof effect.path !== "string"
253
+ ) {
254
+ return false;
255
+ }
256
+ if (effect.kind === "replaced") {
257
+ const isFileEntry = (entry: unknown): boolean => {
258
+ if (typeof entry !== "object" || entry === null) return false;
259
+ const candidate = entry as { entryType?: unknown; target?: unknown };
260
+ return (
261
+ candidate.entryType === "regular-file" ||
262
+ (candidate.entryType === "symlink" && typeof candidate.target === "string")
263
+ );
264
+ };
265
+ return isFileEntry(effect.previousEntry) && isFileEntry(effect.replacementEntry);
266
+ }
267
+ if (
268
+ effect.kind === "symlink-removed" ||
269
+ effect.kind === "symlink-moved" ||
270
+ effect.kind === "symlink-target-modified"
271
+ ) {
272
+ return typeof effect.target === "string";
273
+ }
274
+ return true;
275
+ }
276
+
277
+ const FINAL_PATH_STATES = new Set<ApplyPatchFinalPathState["state"]>([
278
+ "absent",
279
+ "regular-file",
280
+ "symlink",
281
+ "directory",
282
+ "other-entry",
283
+ "unchanged",
284
+ "requested-content",
285
+ "different-from-requested-content",
286
+ "different-from-requested-and-previous-content",
287
+ "different-from-previous-content",
288
+ "different-entry",
289
+ "different-entry-type",
290
+ "not-verified",
291
+ ]);
292
+
293
+ function isApplyPatchFinalPathState(value: unknown): value is ApplyPatchFinalPathState {
294
+ if (typeof value !== "object" || value === null) return false;
295
+ const state = value as { path?: unknown; state?: unknown };
296
+ return (
297
+ typeof state.path === "string" &&
298
+ typeof state.state === "string" &&
299
+ FINAL_PATH_STATES.has(state.state as ApplyPatchFinalPathState["state"])
300
+ );
301
+ }
302
+
303
+ function isApplyPatchInstruction(value: unknown): value is ApplyPatchInstructionDetails {
304
+ if (typeof value !== "object" || value === null) return false;
305
+ const instruction = value as {
306
+ index?: unknown;
307
+ kind?: unknown;
308
+ path?: unknown;
309
+ moveTo?: unknown;
310
+ status?: unknown;
311
+ reason?: unknown;
312
+ effects?: unknown;
313
+ finalStates?: unknown;
314
+ matcher?: unknown;
315
+ changeIndexes?: unknown;
316
+ error?: unknown;
317
+ };
318
+ return (
319
+ typeof instruction.index === "number" &&
320
+ (instruction.kind === "add" ||
321
+ instruction.kind === "delete" ||
322
+ instruction.kind === "update" ||
323
+ instruction.kind === "move") &&
324
+ typeof instruction.path === "string" &&
325
+ (instruction.moveTo === undefined || typeof instruction.moveTo === "string") &&
326
+ typeof instruction.status === "string" &&
327
+ INSTRUCTION_STATUSES.has(instruction.status as ApplyPatchInstructionStatus) &&
328
+ (instruction.reason === undefined || isApplyPatchInstructionReason(instruction.reason)) &&
329
+ (instruction.effects === undefined ||
330
+ (Array.isArray(instruction.effects) &&
331
+ instruction.effects.every(isApplyPatchInstructionEffect))) &&
332
+ (instruction.finalStates === undefined ||
333
+ (Array.isArray(instruction.finalStates) &&
334
+ instruction.finalStates.every(isApplyPatchFinalPathState))) &&
335
+ (instruction.matcher === undefined || isFormatterMatchFailure(instruction.matcher)) &&
336
+ (instruction.changeIndexes === undefined ||
337
+ (Array.isArray(instruction.changeIndexes) &&
338
+ instruction.changeIndexes.every((index) => typeof index === "number"))) &&
339
+ (instruction.error === undefined || typeof instruction.error === "string")
340
+ );
341
+ }
342
+
343
+ function isMatcherRange(value: unknown): value is FormatterMatchCandidateRange {
344
+ if (typeof value !== "object" || value === null) return false;
345
+ const range = value as { startLine?: unknown; endLine?: unknown };
346
+ return typeof range.startLine === "number" && typeof range.endLine === "number";
347
+ }
348
+
349
+ const MATCHER_REASONS = new Set<FormatterMatchFailureDetails["reason"]>([
350
+ "no-candidate",
351
+ "no-ordered-mapping",
352
+ "too-many-candidates",
353
+ "ambiguous-output",
354
+ "mapping-limit",
355
+ "overlapping-edits",
356
+ ]);
357
+
358
+ function isFormatterMatchFailure(value: unknown): value is FormatterMatchFailureDetails {
359
+ if (typeof value !== "object" || value === null) return false;
360
+ const failure = value as {
361
+ reason?: unknown;
362
+ path?: unknown;
363
+ groupCount?: unknown;
364
+ groupIndex?: unknown;
365
+ chunkCount?: unknown;
366
+ chunkIndex?: unknown;
367
+ candidateCount?: unknown;
368
+ candidates?: unknown;
369
+ previousGroupIndex?: unknown;
370
+ previousCandidates?: unknown;
371
+ reverseOrdered?: unknown;
372
+ overlapping?: unknown;
373
+ replacementCandidateCount?: unknown;
374
+ replacementCandidates?: unknown;
375
+ oldExcerpt?: unknown;
376
+ };
377
+ return (
378
+ typeof failure.reason === "string" &&
379
+ MATCHER_REASONS.has(failure.reason as FormatterMatchFailureDetails["reason"]) &&
380
+ typeof failure.path === "string" &&
381
+ typeof failure.groupCount === "number" &&
382
+ (failure.groupIndex === undefined || typeof failure.groupIndex === "number") &&
383
+ (failure.chunkCount === undefined || typeof failure.chunkCount === "number") &&
384
+ (failure.chunkIndex === undefined || typeof failure.chunkIndex === "number") &&
385
+ typeof failure.candidateCount === "number" &&
386
+ Array.isArray(failure.candidates) &&
387
+ failure.candidates.every(isMatcherRange) &&
388
+ (failure.previousGroupIndex === undefined || typeof failure.previousGroupIndex === "number") &&
389
+ (failure.previousCandidates === undefined ||
390
+ (Array.isArray(failure.previousCandidates) &&
391
+ failure.previousCandidates.every(isMatcherRange))) &&
392
+ (failure.reverseOrdered === undefined || typeof failure.reverseOrdered === "boolean") &&
393
+ (failure.overlapping === undefined || typeof failure.overlapping === "boolean") &&
394
+ (failure.replacementCandidateCount === undefined ||
395
+ typeof failure.replacementCandidateCount === "number") &&
396
+ (failure.replacementCandidates === undefined ||
397
+ (Array.isArray(failure.replacementCandidates) &&
398
+ failure.replacementCandidates.every(isMatcherRange))) &&
399
+ (failure.oldExcerpt === undefined || typeof failure.oldExcerpt === "string")
400
+ );
401
+ }
402
+
403
+ function isApplyPatchFailure(value: unknown): value is ApplyPatchFailureDetails {
404
+ if (typeof value !== "object" || value === null) return false;
405
+ const failure = value as {
406
+ phase?: unknown;
407
+ message?: unknown;
408
+ failedInstruction?: unknown;
409
+ matcher?: unknown;
410
+ };
411
+ return (
412
+ (failure.phase === "input" ||
413
+ failure.phase === "parse" ||
414
+ failure.phase === "preflight" ||
415
+ failure.phase === "execution") &&
416
+ typeof failure.message === "string" &&
417
+ (failure.failedInstruction === undefined || typeof failure.failedInstruction === "number") &&
418
+ (failure.matcher === undefined || isFormatterMatchFailure(failure.matcher))
419
+ );
420
+ }
421
+
138
422
  export function isApplyPatchDetails(value: unknown): value is ApplyPatchDetails {
139
423
  if (typeof value !== "object" || value === null) return false;
140
424
  const details = value as {
@@ -144,6 +428,9 @@ export function isApplyPatchDetails(value: unknown): value is ApplyPatchDetails
144
428
  added?: unknown;
145
429
  modified?: unknown;
146
430
  deleted?: unknown;
431
+ instructions?: unknown;
432
+ failure?: unknown;
433
+ error?: unknown;
147
434
  };
148
435
  return (
149
436
  (details.status === "completed" || details.status === "failed") &&
@@ -152,14 +439,22 @@ export function isApplyPatchDetails(value: unknown): value is ApplyPatchDetails
152
439
  details.changes.every(isAppliedPatchChange) &&
153
440
  isStringArray(details.added) &&
154
441
  isStringArray(details.modified) &&
155
- isStringArray(details.deleted)
442
+ isStringArray(details.deleted) &&
443
+ (details.instructions === undefined ||
444
+ (Array.isArray(details.instructions) &&
445
+ details.instructions.every(isApplyPatchInstruction))) &&
446
+ (details.failure === undefined || isApplyPatchFailure(details.failure)) &&
447
+ (details.error === undefined || typeof details.error === "string")
156
448
  );
157
449
  }
158
450
 
159
451
  function sortedChanges(details: ApplyPatchDetails, cwd: string): AppliedPatchChange[] {
160
452
  if (!isApplyPatchDetails(details)) return [];
161
453
  return coalesceAppliedPatchChangesForRendering(details.changes, cwd).toSorted((left, right) =>
162
- comparePaths(resolve(cwd, left.path), resolve(cwd, right.path)),
454
+ comparePaths(
455
+ resolve(cwd, left.kind === "move" ? left.sourcePath : left.path),
456
+ resolve(cwd, right.kind === "move" ? right.sourcePath : right.path),
457
+ ),
163
458
  );
164
459
  }
165
460
 
@@ -231,6 +526,7 @@ function changeDiffLines(change: AppliedPatchChange): DiffLine[] {
231
526
  .map((content, index) => ({ kind: "add", lineNumber: index + 1, content }));
232
527
  }
233
528
  if (change.kind === "delete") {
529
+ if (change.content === undefined) return [];
234
530
  return change.content
235
531
  .replace(/\n$/, "")
236
532
  .split("\n")
@@ -334,6 +630,46 @@ function renderHeader(changes: readonly AppliedPatchChange[], theme: Theme, cwd:
334
630
  return `${theme.fg("dim", "• ")}${theme.bold("Edited")} ${changes.length} ${noun} ${countSummary(additions, deletions, theme)}`;
335
631
  }
336
632
 
633
+ function renderFailedChangeSummary(
634
+ details: ApplyPatchDetails,
635
+ changes: readonly AppliedPatchChange[],
636
+ theme: Theme,
637
+ cwd: string,
638
+ ): string[] {
639
+ const summary = applyPatchSummaryPaths(details);
640
+ const paths = [
641
+ ...summary.added.map((path) => ({ status: "A", path })),
642
+ ...summary.modified.map((path) => ({ status: "M", path })),
643
+ ...summary.deleted.map((path) => ({ status: "D", path })),
644
+ ];
645
+ if (paths.length === 0) return [];
646
+ const additions = changes.reduce((total, change) => total + change.additions, 0);
647
+ const deletions = changes.reduce((total, change) => total + change.deletions, 0);
648
+ const noun = paths.length === 1 ? "file" : "files";
649
+ return [
650
+ `${theme.fg("dim", "• ")}${theme.bold("Changed")} ${paths.length} ${noun} ${countSummary(additions, deletions, theme)}`,
651
+ ...paths.map(
652
+ ({ status, path }) => ` ${theme.fg("dim", "└ ")}${status} ${displayPath(path, cwd)}`,
653
+ ),
654
+ ];
655
+ }
656
+
657
+ function failedResultHasChangedFiles(details: ApplyPatchDetails): boolean {
658
+ const summary = applyPatchSummaryPaths(details);
659
+ return summary.added.length > 0 || summary.modified.length > 0 || summary.deleted.length > 0;
660
+ }
661
+
662
+ function failedResultHasNoChanges(details: ApplyPatchDetails): boolean {
663
+ const hasUnverifiedState = (details.instructions ?? []).some((instruction) =>
664
+ instruction.finalStates?.some((state) => state.state === "not-verified"),
665
+ );
666
+ return (
667
+ !failedResultHasChangedFiles(details) &&
668
+ !applyPatchHasOtherFilesystemChanges(details) &&
669
+ !hasUnverifiedState
670
+ );
671
+ }
672
+
337
673
  function renderChange(
338
674
  change: AppliedPatchChange,
339
675
  width: number,
@@ -341,6 +677,7 @@ function renderChange(
341
677
  palette: DiffPalette,
342
678
  ): string[] {
343
679
  const lines = changeDiffLines(change);
680
+ if (change.kind === "move") return [];
344
681
  const languagePath = change.kind === "update" && change.moveTo ? change.moveTo : change.path;
345
682
  highlightDiffLines(lines, languagePath);
346
683
  const lineNumberWidth = lines.reduce(
@@ -351,6 +688,95 @@ function renderChange(
351
688
  return lines.flatMap((line) => renderDiffLine(line, width, lineNumberWidth, theme, palette));
352
689
  }
353
690
 
691
+ function instructionStatusLabel(status: ApplyPatchInstructionStatus, theme: Theme): string {
692
+ const label = `[${formatApplyPatchInstructionStatusLabel(status)}]`;
693
+ switch (status) {
694
+ case "applied":
695
+ return theme.fg("success", label);
696
+ case "failed":
697
+ return theme.fg("error", label);
698
+ case "dead":
699
+ case "no-op":
700
+ case "not-run":
701
+ case "planned":
702
+ return theme.fg("dim", label);
703
+ }
704
+ }
705
+
706
+ function instructionLabel(instruction: ApplyPatchInstructionDetails, cwd: string): string {
707
+ const verb =
708
+ instruction.kind === "add"
709
+ ? "Add"
710
+ : instruction.kind === "delete"
711
+ ? "Delete"
712
+ : instruction.kind === "move"
713
+ ? "Move"
714
+ : "Update";
715
+ const path = displayPath(instruction.path, cwd);
716
+ if (!instruction.moveTo) return `${verb} ${path}`;
717
+ return instruction.kind === "update"
718
+ ? `Update & Move ${path} → ${displayPath(instruction.moveTo, cwd)}`
719
+ : `${verb} ${path} → ${displayPath(instruction.moveTo, cwd)}`;
720
+ }
721
+
722
+ function instructionChanges(
723
+ details: ApplyPatchDetails,
724
+ instructionIndex: number,
725
+ ): AppliedPatchChange[] {
726
+ const instruction = details.instructions?.find(
727
+ (candidate) => candidate.index === instructionIndex,
728
+ );
729
+ return (instruction?.changeIndexes ?? []).flatMap((index) => {
730
+ const change = details.changes[index];
731
+ return change && !(instruction?.status === "failed" && change.kind === "move" && !change.exact)
732
+ ? [change]
733
+ : [];
734
+ });
735
+ }
736
+
737
+ function renderInstructionResults(
738
+ details: ApplyPatchDetails,
739
+ theme: Theme,
740
+ cwd: string,
741
+ expanded: boolean,
742
+ width?: number,
743
+ ): string[] {
744
+ const lines: string[] = [];
745
+ const instructions = details.instructions ?? [];
746
+ if (!applyPatchNeedsInstructionResults(details, cwd)) return lines;
747
+ lines.push(theme.bold("Patch instruction results:"));
748
+ const palette = diffPalette(theme);
749
+ for (const instruction of instructions) {
750
+ const feedback = formatApplyPatchInstructionFeedback(instruction, details, cwd);
751
+ lines.push(
752
+ ` ${instruction.index}. ${instructionStatusLabel(instruction.status, theme)} ${instructionLabel(instruction, cwd)}${feedback ? ` ${theme.fg("dim", `— ${feedback}`)}` : ""}`,
753
+ );
754
+ if (expanded && width !== undefined) {
755
+ const changes = instructionChanges(details, instruction.index);
756
+ const inset = Math.min(4, Math.max(0, width - 1));
757
+ const contentWidth = Math.max(1, width - inset);
758
+ for (const change of changes) {
759
+ lines.push(
760
+ ...renderChange(change, contentWidth, theme, palette).map(
761
+ (line) => `${" ".repeat(inset)}${line}`,
762
+ ),
763
+ );
764
+ }
765
+ } else if (expanded) {
766
+ for (const change of instructionChanges(details, instruction.index)) {
767
+ lines.push(
768
+ ...changeDiffLines(change).map((line) => {
769
+ if (line.separator) return ` ${theme.fg("dim", "⋮")}`;
770
+ const sign = line.kind === "add" ? "+" : line.kind === "delete" ? "-" : " ";
771
+ return ` ${sign}${line.lineNumber ?? ""} ${line.content}`;
772
+ }),
773
+ );
774
+ }
775
+ }
776
+ }
777
+ return lines;
778
+ }
779
+
354
780
  export function formatApplyPatchRenderText(
355
781
  details: ApplyPatchDetails,
356
782
  theme: Theme,
@@ -358,27 +784,48 @@ export function formatApplyPatchRenderText(
358
784
  ): string {
359
785
  const lines: string[] = [];
360
786
  const changes = sortedChanges(details, cwd);
361
- if (changes.length > 0) {
787
+ const showInstructionResults = applyPatchNeedsInstructionResults(details, cwd);
788
+ if (details.status === "failed") {
789
+ lines.push(...renderFailedChangeSummary(details, changes, theme, cwd));
790
+ } else if (changes.length > 0) {
362
791
  lines.push(renderHeader(changes, theme, cwd));
363
792
  for (const [index, change] of changes.entries()) {
364
793
  if (changes.length > 1) {
365
794
  lines.push(
366
- ` ${theme.fg("dim", "└ ")}${changePath(change, cwd)} ${countSummary(change.additions, change.deletions, theme)}`,
795
+ ` ${theme.fg("dim", "└ ")}${changeListPath(change, cwd)} ${countSummary(change.additions, change.deletions, theme)}`,
796
+ );
797
+ }
798
+ if (!showInstructionResults) {
799
+ lines.push(
800
+ ...changeDiffLines(change).map((line) => {
801
+ if (line.separator) return ` ${theme.fg("dim", "⋮")}`;
802
+ const sign = line.kind === "add" ? "+" : line.kind === "delete" ? "-" : " ";
803
+ return ` ${sign}${line.lineNumber ?? ""} ${line.content}`;
804
+ }),
367
805
  );
368
806
  }
369
- lines.push(
370
- ...changeDiffLines(change).map((line) => {
371
- if (line.separator) return ` ${theme.fg("dim", "⋮")}`;
372
- const sign = line.kind === "add" ? "+" : line.kind === "delete" ? "-" : " ";
373
- return ` ${sign}${line.lineNumber ?? ""} ${line.content}`;
374
- }),
375
- );
376
807
  if (index !== changes.length - 1) lines.push("");
377
808
  }
378
809
  }
379
810
  if (details?.status === "failed") {
380
811
  if (lines.length > 0) lines.push("");
381
812
  lines.push(theme.bold(theme.fg("error", "✘ Failed to apply patch")));
813
+ lines.push(
814
+ ...formatApplyPatchFailureHeading(details).map((line) => ` ${theme.fg("dim", line)}`),
815
+ );
816
+ if (failedResultHasNoChanges(details)) lines.push(" No files were changed.");
817
+ else if (
818
+ !failedResultHasChangedFiles(details) &&
819
+ applyPatchHasOtherFilesystemChanges(details)
820
+ ) {
821
+ lines.push(" Filesystem changed.");
822
+ }
823
+ } else {
824
+ if (changes.length === 0) lines.push(theme.bold("Success. No files were changed."));
825
+ }
826
+ if (showInstructionResults) {
827
+ if (lines.length > 0) lines.push("");
828
+ lines.push(...renderInstructionResults(details, theme, cwd, true));
382
829
  }
383
830
  return lines.join("\n");
384
831
  }
@@ -399,20 +846,25 @@ export class ApplyPatchDiffComponent implements Component {
399
846
  render(width: number): string[] {
400
847
  const effectiveWidth = Math.max(1, width);
401
848
  const changes = sortedChanges(this.details, this.cwd);
402
- const palette = diffPalette(this.theme);
849
+ const showInstructionResults = applyPatchNeedsInstructionResults(this.details, this.cwd);
403
850
  const lines: string[] = [];
404
851
 
405
- if (changes.length > 0) {
852
+ if (this.details.status === "failed") {
853
+ for (const line of renderFailedChangeSummary(this.details, changes, this.theme, this.cwd)) {
854
+ lines.push(...wrapTextWithAnsi(line, effectiveWidth));
855
+ }
856
+ } else if (changes.length > 0) {
406
857
  lines.push(...wrapTextWithAnsi(renderHeader(changes, this.theme, this.cwd), effectiveWidth));
407
858
  for (const [index, change] of changes.entries()) {
408
859
  if (this.expanded && index > 0) lines.push("");
409
860
  if (changes.length > 1) {
410
- const header = ` ${this.theme.fg("dim", "└ ")}${changePath(change, this.cwd)} ${countSummary(change.additions, change.deletions, this.theme)}`;
861
+ const header = ` ${this.theme.fg("dim", "└ ")}${changeListPath(change, this.cwd)} ${countSummary(change.additions, change.deletions, this.theme)}`;
411
862
  lines.push(...wrapTextWithAnsi(header, effectiveWidth));
412
863
  }
413
- if (this.expanded) {
864
+ if (this.expanded && !showInstructionResults) {
414
865
  const inset = Math.min(4, Math.max(0, effectiveWidth - 1));
415
866
  const contentWidth = Math.max(1, effectiveWidth - inset);
867
+ const palette = diffPalette(this.theme);
416
868
  lines.push(
417
869
  ...renderChange(change, contentWidth, this.theme, palette).map(
418
870
  (line) => `${" ".repeat(inset)}${line}`,
@@ -425,12 +877,39 @@ export class ApplyPatchDiffComponent implements Component {
425
877
  if (this.details?.status === "failed") {
426
878
  if (lines.length > 0) lines.push("");
427
879
  lines.push(
428
- truncateToWidth(
880
+ ...wrapTextWithAnsi(
429
881
  this.theme.bold(this.theme.fg("error", "✘ Failed to apply patch")),
430
882
  effectiveWidth,
431
- "",
432
883
  ),
433
884
  );
885
+ for (const line of formatApplyPatchFailureHeading(this.details)) {
886
+ lines.push(...wrapTextWithAnsi(` ${this.theme.fg("dim", line)}`, effectiveWidth));
887
+ }
888
+ if (failedResultHasNoChanges(this.details)) {
889
+ lines.push(...wrapTextWithAnsi(" No files were changed.", effectiveWidth));
890
+ } else if (
891
+ !failedResultHasChangedFiles(this.details) &&
892
+ applyPatchHasOtherFilesystemChanges(this.details)
893
+ ) {
894
+ lines.push(...wrapTextWithAnsi(" Filesystem changed.", effectiveWidth));
895
+ }
896
+ } else if (changes.length === 0) {
897
+ lines.push(
898
+ ...wrapTextWithAnsi(this.theme.bold("Success. No files were changed."), effectiveWidth),
899
+ );
900
+ }
901
+
902
+ if (showInstructionResults) {
903
+ if (lines.length > 0) lines.push("");
904
+ for (const line of renderInstructionResults(
905
+ this.details,
906
+ this.theme,
907
+ this.cwd,
908
+ this.expanded,
909
+ effectiveWidth,
910
+ )) {
911
+ lines.push(...wrapTextWithAnsi(line, effectiveWidth));
912
+ }
434
913
  }
435
914
 
436
915
  return lines;