@oh-my-pi/pi-coding-agent 8.3.0 → 8.4.0

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.
@@ -7,7 +7,7 @@
7
7
  import * as fs from "node:fs/promises";
8
8
  import * as path from "node:path";
9
9
  import { resolveToCwd } from "../tools/path-utils";
10
- import { DEFAULT_FUZZY_THRESHOLD, findContextLine, findMatch, seekSequence } from "./fuzzy";
10
+ import { DEFAULT_FUZZY_THRESHOLD, findClosestSequenceMatch, findContextLine, findMatch, seekSequence } from "./fuzzy";
11
11
  import {
12
12
  adjustIndentation,
13
13
  convertLeadingTabsToSpaces,
@@ -67,9 +67,12 @@ interface Replacement {
67
67
  newLines: string[];
68
68
  }
69
69
 
70
+ type HunkVariantKind = "trim-common" | "dedupe-shared" | "collapse-repeated" | "single-line";
71
+
70
72
  interface HunkVariant {
71
73
  oldLines: string[];
72
74
  newLines: string[];
75
+ kind: HunkVariantKind;
73
76
  }
74
77
 
75
78
  // ═══════════════════════════════════════════════════════════════════════════
@@ -272,7 +275,7 @@ function trimCommonContext(oldLines: string[], newLines: string[]): HunkVariant
272
275
  if (trimmedOld.length === 0 && trimmedNew.length === 0) {
273
276
  return undefined;
274
277
  }
275
- return { oldLines: trimmedOld, newLines: trimmedNew };
278
+ return { oldLines: trimmedOld, newLines: trimmedNew, kind: "trim-common" };
276
279
  }
277
280
 
278
281
  function collapseConsecutiveSharedLines(oldLines: string[], newLines: string[]): HunkVariant | undefined {
@@ -297,7 +300,7 @@ function collapseConsecutiveSharedLines(oldLines: string[], newLines: string[]):
297
300
  if (collapsedOld.length === oldLines.length && collapsedNew.length === newLines.length) {
298
301
  return undefined;
299
302
  }
300
- return { oldLines: collapsedOld, newLines: collapsedNew };
303
+ return { oldLines: collapsedOld, newLines: collapsedNew, kind: "dedupe-shared" };
301
304
  }
302
305
 
303
306
  function collapseRepeatedBlocks(oldLines: string[], newLines: string[]): HunkVariant | undefined {
@@ -339,7 +342,7 @@ function collapseRepeatedBlocks(oldLines: string[], newLines: string[]): HunkVar
339
342
  if (collapsedOld.length === oldLines.length && collapsedNew.length === newLines.length) {
340
343
  return undefined;
341
344
  }
342
- return { oldLines: collapsedOld, newLines: collapsedNew };
345
+ return { oldLines: collapsedOld, newLines: collapsedNew, kind: "collapse-repeated" };
343
346
  }
344
347
 
345
348
  function reduceToSingleLineChange(oldLines: string[], newLines: string[]): HunkVariant | undefined {
@@ -352,12 +355,12 @@ function reduceToSingleLineChange(oldLines: string[], newLines: string[]): HunkV
352
355
  }
353
356
  }
354
357
  if (changedIndex === undefined) return undefined;
355
- return { oldLines: [oldLines[changedIndex]], newLines: [newLines[changedIndex]] };
358
+ return { oldLines: [oldLines[changedIndex]], newLines: [newLines[changedIndex]], kind: "single-line" };
356
359
  }
357
360
 
358
361
  function buildFallbackVariants(hunk: DiffHunk): HunkVariant[] {
359
362
  const variants: HunkVariant[] = [];
360
- const base: HunkVariant = { oldLines: hunk.oldLines, newLines: hunk.newLines };
363
+ const base: HunkVariant = { oldLines: hunk.oldLines, newLines: hunk.newLines, kind: "trim-common" };
361
364
 
362
365
  const trimmed = trimCommonContext(base.oldLines, base.newLines);
363
366
  if (trimmed) variants.push(trimmed);
@@ -387,6 +390,11 @@ function buildFallbackVariants(hunk: DiffHunk): HunkVariant[] {
387
390
  });
388
391
  }
389
392
 
393
+ function filterFallbackVariants(variants: HunkVariant[], allowAggressive: boolean): HunkVariant[] {
394
+ if (allowAggressive) return variants;
395
+ return variants.filter(variant => variant.kind !== "collapse-repeated" && variant.kind !== "single-line");
396
+ }
397
+
390
398
  function findContextRelativeMatch(
391
399
  lines: string[],
392
400
  patternLine: string,
@@ -414,6 +422,47 @@ function findContextRelativeMatch(
414
422
  return undefined;
415
423
  }
416
424
 
425
+ const AMBIGUITY_HINT_WINDOW = 200;
426
+ const MATCH_PREVIEW_CONTEXT = 2;
427
+ const MATCH_PREVIEW_MAX_LEN = 80;
428
+
429
+ function formatSequenceMatchPreview(lines: string[], startIdx: number): string {
430
+ const start = Math.max(0, startIdx - MATCH_PREVIEW_CONTEXT);
431
+ const end = Math.min(lines.length, startIdx + MATCH_PREVIEW_CONTEXT + 1);
432
+ const previewLines = lines.slice(start, end);
433
+ return previewLines
434
+ .map((line, i) => {
435
+ const num = start + i + 1;
436
+ const truncated =
437
+ line.length > MATCH_PREVIEW_MAX_LEN ? `${line.slice(0, MATCH_PREVIEW_MAX_LEN - 3)}...` : line;
438
+ return ` ${num} | ${truncated}`;
439
+ })
440
+ .join("\n");
441
+ }
442
+
443
+ function formatSequenceMatchPreviews(
444
+ lines: string[],
445
+ matchIndices: number[] | undefined,
446
+ matchCount: number | undefined,
447
+ ): string | undefined {
448
+ if (!matchIndices || matchIndices.length === 0) return undefined;
449
+ const previews = matchIndices.map(index => formatSequenceMatchPreview(lines, index));
450
+ const moreMsg =
451
+ matchCount && matchCount > matchIndices.length ? ` (showing first ${matchIndices.length} of ${matchCount})` : "";
452
+ return `${previews.join("\n\n")}${moreMsg}`;
453
+ }
454
+
455
+ function chooseHintedMatch(
456
+ matchIndices: number[] | undefined,
457
+ hintIndex: number | undefined,
458
+ window: number,
459
+ ): number | undefined {
460
+ if (!matchIndices || matchIndices.length === 0 || hintIndex === undefined) return undefined;
461
+ const candidates = matchIndices.filter(index => Math.abs(index - hintIndex) <= window);
462
+ if (candidates.length === 1) return candidates[0];
463
+ return undefined;
464
+ }
465
+
417
466
  /** Get hint index from hunk's line number */
418
467
  function getHunkHintIndex(hunk: DiffHunk, currentIndex: number): number | undefined {
419
468
  if (hunk.oldStartLine === undefined) return undefined;
@@ -458,11 +507,17 @@ function findHierarchicalContext(
458
507
  if (hintStart >= currentStart) {
459
508
  const hintedResult = findContextLine(lines, part, hintStart, { allowFuzzy });
460
509
  if (hintedResult.index !== undefined) {
461
- return { ...hintedResult, matchCount: 1 };
510
+ return { ...hintedResult, matchCount: 1, matchIndices: [hintedResult.index] };
462
511
  }
463
512
  }
464
513
  }
465
- return { index: undefined, confidence: result.confidence, matchCount: result.matchCount };
514
+ return {
515
+ index: undefined,
516
+ confidence: result.confidence,
517
+ matchCount: result.matchCount,
518
+ matchIndices: result.matchIndices,
519
+ strategy: result.strategy,
520
+ };
466
521
  }
467
522
 
468
523
  if (result.index === undefined) {
@@ -471,7 +526,7 @@ function findHierarchicalContext(
471
526
  if (hintStart >= currentStart) {
472
527
  const hintedResult = findContextLine(lines, part, hintStart, { allowFuzzy });
473
528
  if (hintedResult.index !== undefined) {
474
- return { ...hintedResult, matchCount: 1 };
529
+ return { ...hintedResult, matchCount: 1, matchIndices: [hintedResult.index] };
475
530
  }
476
531
  }
477
532
  }
@@ -494,17 +549,27 @@ function findHierarchicalContext(
494
549
  const inner = spaceParts[spaceParts.length - 1];
495
550
  const outerResult = findContextLine(lines, outer, startFrom, { allowFuzzy });
496
551
  if (outerResult.matchCount !== undefined && outerResult.matchCount > 1) {
497
- return { index: undefined, confidence: outerResult.confidence, matchCount: outerResult.matchCount };
552
+ return {
553
+ index: undefined,
554
+ confidence: outerResult.confidence,
555
+ matchCount: outerResult.matchCount,
556
+ matchIndices: outerResult.matchIndices,
557
+ strategy: outerResult.strategy,
558
+ };
498
559
  }
499
560
  if (outerResult.index !== undefined) {
500
561
  const innerResult = findContextLine(lines, inner, outerResult.index + 1, { allowFuzzy });
501
562
  if (innerResult.index !== undefined) {
502
563
  return innerResult.matchCount && innerResult.matchCount > 1
503
- ? { ...innerResult, matchCount: 1 }
564
+ ? { ...innerResult, matchCount: 1, matchIndices: [innerResult.index] }
504
565
  : innerResult;
505
566
  }
506
567
  if (innerResult.matchCount !== undefined && innerResult.matchCount > 1) {
507
- return { ...innerResult, matchCount: 1 };
568
+ return {
569
+ ...innerResult,
570
+ matchCount: 1,
571
+ matchIndices: innerResult.index !== undefined ? [innerResult.index] : innerResult.matchIndices,
572
+ };
508
573
  }
509
574
  }
510
575
  }
@@ -516,7 +581,7 @@ function findHierarchicalContext(
516
581
  const hintStart = Math.max(0, lineHint - 1);
517
582
  const hintedResult = findContextLine(lines, context, hintStart, { allowFuzzy });
518
583
  if (hintedResult.index !== undefined) {
519
- return { ...hintedResult, matchCount: 1 };
584
+ return { ...hintedResult, matchCount: 1, matchIndices: [hintedResult.index] };
520
585
  }
521
586
  }
522
587
 
@@ -547,7 +612,13 @@ function findHierarchicalContext(
547
612
  const outerResult = findContextLine(lines, outer, startFrom, { allowFuzzy });
548
613
 
549
614
  if (outerResult.matchCount !== undefined && outerResult.matchCount > 1) {
550
- return { index: undefined, confidence: outerResult.confidence, matchCount: outerResult.matchCount };
615
+ return {
616
+ index: undefined,
617
+ confidence: outerResult.confidence,
618
+ matchCount: outerResult.matchCount,
619
+ matchIndices: outerResult.matchIndices,
620
+ strategy: outerResult.strategy,
621
+ };
551
622
  }
552
623
 
553
624
  if (outerResult.index === undefined) {
@@ -556,10 +627,16 @@ function findHierarchicalContext(
556
627
 
557
628
  const innerResult = findContextLine(lines, inner, outerResult.index + 1, { allowFuzzy });
558
629
  if (innerResult.index !== undefined) {
559
- return innerResult.matchCount && innerResult.matchCount > 1 ? { ...innerResult, matchCount: 1 } : innerResult;
630
+ return innerResult.matchCount && innerResult.matchCount > 1
631
+ ? { ...innerResult, matchCount: 1, matchIndices: [innerResult.index] }
632
+ : innerResult;
560
633
  }
561
634
  if (innerResult.matchCount !== undefined && innerResult.matchCount > 1) {
562
- return { ...innerResult, matchCount: 1 };
635
+ return {
636
+ ...innerResult,
637
+ matchCount: 1,
638
+ matchIndices: innerResult.index !== undefined ? [innerResult.index] : innerResult.matchIndices,
639
+ };
563
640
  }
564
641
  }
565
642
 
@@ -620,6 +697,7 @@ function attemptSequenceFallback(
620
697
  currentIndex: number,
621
698
  lineHint: number | undefined,
622
699
  allowFuzzy: boolean,
700
+ allowAggressiveFallbacks: boolean,
623
701
  ): number | undefined {
624
702
  if (hunk.oldLines.length === 0) return undefined;
625
703
  const matchHint = getHunkHintIndex(hunk, currentIndex);
@@ -642,7 +720,7 @@ function attemptSequenceFallback(
642
720
  return fallbackResult.index;
643
721
  }
644
722
 
645
- for (const variant of buildFallbackVariants(hunk)) {
723
+ for (const variant of filterFallbackVariants(buildFallbackVariants(hunk), allowAggressiveFallbacks)) {
646
724
  if (variant.oldLines.length === 0) continue;
647
725
  const variantResult = findSequenceWithHint(
648
726
  lines,
@@ -669,7 +747,7 @@ function applyCharacterMatch(
669
747
  hunk: DiffHunk,
670
748
  fuzzyThreshold: number,
671
749
  allowFuzzy: boolean,
672
- ): string {
750
+ ): { content: string; warnings: string[] } {
673
751
  const oldText = hunk.oldLines.join("\n");
674
752
  const newText = hunk.newLines.join("\n");
675
753
 
@@ -725,10 +803,18 @@ function applyCharacterMatch(
725
803
  // Adjust indentation to match what was actually found
726
804
  const adjustedNewText = adjustIndentation(normalizedOldText, matchOutcome.match.actualText, newText);
727
805
 
806
+ const warnings: string[] = [];
807
+ if (matchOutcome.dominantFuzzy && matchOutcome.match) {
808
+ const similarity = Math.round(matchOutcome.match.confidence * 100);
809
+ warnings.push(
810
+ `Dominant fuzzy match selected in ${path} near line ${matchOutcome.match.startLine} (${similarity}% similar).`,
811
+ );
812
+ }
813
+
728
814
  // Apply the replacement
729
815
  const before = normalizedContent.substring(0, matchOutcome.match.startIndex);
730
816
  const after = normalizedContent.substring(matchOutcome.match.startIndex + matchOutcome.match.actualText.length);
731
- return before + adjustedNewText + after;
817
+ return { content: before + adjustedNewText + after, warnings };
732
818
  }
733
819
 
734
820
  function applyTrailingNewlinePolicy(content: string, hadFinalNewline: boolean): string {
@@ -746,8 +832,9 @@ function computeReplacements(
746
832
  path: string,
747
833
  hunks: DiffHunk[],
748
834
  allowFuzzy: boolean,
749
- ): Replacement[] {
835
+ ): { replacements: Replacement[]; warnings: string[] } {
750
836
  const replacements: Replacement[] = [];
837
+ const warnings: string[] = [];
751
838
  let lineIndex = 0;
752
839
 
753
840
  for (const hunk of hunks) {
@@ -763,6 +850,7 @@ function computeReplacements(
763
850
  );
764
851
  }
765
852
  const lineHint = hunk.oldStartLine;
853
+ const allowAggressiveFallbacks = hunk.changeContext !== undefined || lineHint !== undefined || hunk.isEndOfFile;
766
854
  if (lineHint !== undefined && hunk.changeContext === undefined && !hunk.hasContextLines) {
767
855
  lineIndex = Math.max(0, Math.min(lineHint - 1, originalLines.length - 1));
768
856
  }
@@ -775,16 +863,26 @@ function computeReplacements(
775
863
  contextIndex = idx;
776
864
 
777
865
  if (idx === undefined || (result.matchCount !== undefined && result.matchCount > 1)) {
778
- const fallback = attemptSequenceFallback(originalLines, hunk, lineIndex, lineHint, allowFuzzy);
866
+ const fallback = attemptSequenceFallback(
867
+ originalLines,
868
+ hunk,
869
+ lineIndex,
870
+ lineHint,
871
+ allowFuzzy,
872
+ allowAggressiveFallbacks,
873
+ );
779
874
  if (fallback !== undefined) {
780
875
  lineIndex = fallback;
781
876
  } else if (result.matchCount !== undefined && result.matchCount > 1) {
782
877
  const displayContext = hunk.changeContext.includes("\n")
783
878
  ? hunk.changeContext.split("\n").pop()
784
879
  : hunk.changeContext;
880
+ const previews = formatSequenceMatchPreviews(originalLines, result.matchIndices, result.matchCount);
881
+ const strategyHint = result.strategy ? ` Matching strategy: ${result.strategy}.` : "";
882
+ const previewText = previews ? `\n\n${previews}` : "";
785
883
  throw new ApplyPatchError(
786
- `Found ${result.matchCount} matches for context '${displayContext}' in ${path}. ` +
787
- `Add more surrounding context or additional @@ anchors to make it unique.`,
884
+ `Found ${result.matchCount} matches for context '${displayContext}' in ${path}.${strategyHint}` +
885
+ `${previewText}\n\nAdd more surrounding context or additional @@ anchors to make it unique.`,
788
886
  );
789
887
  } else {
790
888
  const displayContext = hunk.changeContext.includes("\n")
@@ -875,7 +973,7 @@ function computeReplacements(
875
973
  }
876
974
 
877
975
  if (searchResult.index === undefined || (searchResult.matchCount ?? 0) > 1) {
878
- for (const variant of buildFallbackVariants(hunk)) {
976
+ for (const variant of filterFallbackVariants(buildFallbackVariants(hunk), allowAggressiveFallbacks)) {
879
977
  if (variant.oldLines.length === 0) continue;
880
978
  const variantResult = findSequenceWithHint(
881
979
  originalLines,
@@ -895,7 +993,7 @@ function computeReplacements(
895
993
  }
896
994
 
897
995
  if (searchResult.index === undefined && contextIndex !== undefined) {
898
- for (const variant of buildFallbackVariants(hunk)) {
996
+ for (const variant of filterFallbackVariants(buildFallbackVariants(hunk), allowAggressiveFallbacks)) {
899
997
  if (variant.oldLines.length !== 1 || variant.newLines.length !== 1) continue;
900
998
  const removedLine = variant.oldLines[0];
901
999
  const hasSharedDuplicate = hunk.newLines.some(line => line.trim() === removedLine.trim());
@@ -929,11 +1027,38 @@ function computeReplacements(
929
1027
  }
930
1028
  }
931
1029
 
1030
+ if ((searchResult.matchCount ?? 0) > 1) {
1031
+ const hintIndex = matchHint ?? (lineHint ? lineHint - 1 : undefined);
1032
+ const hinted = chooseHintedMatch(searchResult.matchIndices, hintIndex, AMBIGUITY_HINT_WINDOW);
1033
+ if (hinted !== undefined) {
1034
+ searchResult = { ...searchResult, index: hinted, matchCount: 1 };
1035
+ }
1036
+ }
1037
+
932
1038
  if (searchResult.index === undefined) {
933
1039
  if (searchResult.matchCount !== undefined && searchResult.matchCount > 1) {
1040
+ const previews = formatSequenceMatchPreviews(
1041
+ originalLines,
1042
+ searchResult.matchIndices,
1043
+ searchResult.matchCount,
1044
+ );
1045
+ const strategyHint = searchResult.strategy ? ` Matching strategy: ${searchResult.strategy}.` : "";
1046
+ const previewText = previews ? `\n\n${previews}` : "";
934
1047
  throw new ApplyPatchError(
935
- `Found ${searchResult.matchCount} matches for the text in ${path}. ` +
936
- `Add more surrounding context or additional @@ anchors to make it unique.`,
1048
+ `Found ${searchResult.matchCount} matches for the text in ${path}.${strategyHint}` +
1049
+ `${previewText}\n\nAdd more surrounding context or additional @@ anchors to make it unique.`,
1050
+ );
1051
+ }
1052
+ const closest = findClosestSequenceMatch(originalLines, pattern, {
1053
+ start: lineIndex,
1054
+ eof: hunk.isEndOfFile,
1055
+ });
1056
+ if (closest.index !== undefined && closest.confidence > 0) {
1057
+ const similarity = Math.round(closest.confidence * 100);
1058
+ const preview = formatSequenceMatchPreview(originalLines, closest.index);
1059
+ throw new ApplyPatchError(
1060
+ `Failed to find expected lines in ${path}:\n${hunk.oldLines.join("\n")}\n\n` +
1061
+ `Closest match (${similarity}% similar) near line ${closest.index + 1}:\n${preview}`,
937
1062
  );
938
1063
  }
939
1064
  throw new ApplyPatchError(`Failed to find expected lines in ${path}:\n${hunk.oldLines.join("\n")}`);
@@ -941,11 +1066,23 @@ function computeReplacements(
941
1066
 
942
1067
  const found = searchResult.index;
943
1068
 
1069
+ if (searchResult.strategy === "fuzzy-dominant") {
1070
+ const similarity = Math.round(searchResult.confidence * 100);
1071
+ warnings.push(`Dominant fuzzy match selected in ${path} near line ${found + 1} (${similarity}% similar).`);
1072
+ }
1073
+
944
1074
  // Reject if match is ambiguous (prefix/substring matching found multiple matches)
945
1075
  if (searchResult.matchCount !== undefined && searchResult.matchCount > 1) {
1076
+ const previews = formatSequenceMatchPreviews(
1077
+ originalLines,
1078
+ searchResult.matchIndices,
1079
+ searchResult.matchCount,
1080
+ );
1081
+ const strategyHint = searchResult.strategy ? ` Matching strategy: ${searchResult.strategy}.` : "";
1082
+ const previewText = previews ? `\n\n${previews}` : "";
946
1083
  throw new ApplyPatchError(
947
- `Found ${searchResult.matchCount} matches for the text in ${path}. ` +
948
- `Add more surrounding context or additional @@ anchors to make it unique.`,
1084
+ `Found ${searchResult.matchCount} matches for the text in ${path}.${strategyHint}` +
1085
+ `${previewText}\n\nAdd more surrounding context or additional @@ anchors to make it unique.`,
949
1086
  );
950
1087
  }
951
1088
 
@@ -990,7 +1127,27 @@ function computeReplacements(
990
1127
  // Sort by start index
991
1128
  replacements.sort((a, b) => a.startIndex - b.startIndex);
992
1129
 
993
- return replacements;
1130
+ for (let i = 1; i < replacements.length; i++) {
1131
+ const prev = replacements[i - 1];
1132
+ const next = replacements[i];
1133
+ const prevEnd = prev.startIndex + prev.oldLen;
1134
+ if (next.startIndex < prevEnd) {
1135
+ const formatRange = (replacement: Replacement): string => {
1136
+ if (replacement.oldLen === 0) {
1137
+ return `${replacement.startIndex + 1} (insertion)`;
1138
+ }
1139
+ return `${replacement.startIndex + 1}-${replacement.startIndex + replacement.oldLen}`;
1140
+ };
1141
+ const prevRange = formatRange(prev);
1142
+ const nextRange = formatRange(next);
1143
+ throw new ApplyPatchError(
1144
+ `Overlapping hunks detected in ${path} at lines ${prevRange} and ${nextRange}. ` +
1145
+ `Split hunks or add more context to avoid overlap.`,
1146
+ );
1147
+ }
1148
+ }
1149
+
1150
+ return { replacements, warnings };
994
1151
  }
995
1152
 
996
1153
  /**
@@ -1018,7 +1175,7 @@ function applyHunksToContent(
1018
1175
  hunks: DiffHunk[],
1019
1176
  fuzzyThreshold: number,
1020
1177
  allowFuzzy: boolean,
1021
- ): string {
1178
+ ): { content: string; warnings: string[] } {
1022
1179
  const hadFinalNewline = originalContent.endsWith("\n");
1023
1180
 
1024
1181
  // Detect simple replace pattern: single hunk, no @@ context, no context lines, has old lines to match
@@ -1032,8 +1189,8 @@ function applyHunksToContent(
1032
1189
  hunk.oldStartLine === undefined && // No line hint to use for positioning
1033
1190
  !hunk.isEndOfFile // No EOF targeting (prefer end of file)
1034
1191
  ) {
1035
- const content = applyCharacterMatch(originalContent, path, hunk, fuzzyThreshold, allowFuzzy);
1036
- return applyTrailingNewlinePolicy(content, hadFinalNewline);
1192
+ const { content, warnings } = applyCharacterMatch(originalContent, path, hunk, fuzzyThreshold, allowFuzzy);
1193
+ return { content: applyTrailingNewlinePolicy(content, hadFinalNewline), warnings };
1037
1194
  }
1038
1195
  }
1039
1196
 
@@ -1048,7 +1205,7 @@ function applyHunksToContent(
1048
1205
  strippedTrailingEmpty = true;
1049
1206
  }
1050
1207
 
1051
- const replacements = computeReplacements(originalLines, path, hunks, allowFuzzy);
1208
+ const { replacements, warnings } = computeReplacements(originalLines, path, hunks, allowFuzzy);
1052
1209
  const newLines = applyReplacements(originalLines, replacements);
1053
1210
 
1054
1211
  // Restore the trailing empty element if we stripped it
@@ -1060,12 +1217,12 @@ function applyHunksToContent(
1060
1217
 
1061
1218
  // Preserve original trailing newline behavior
1062
1219
  if (hadFinalNewline && !content.endsWith("\n")) {
1063
- return `${content}\n`;
1220
+ return { content: `${content}\n`, warnings };
1064
1221
  }
1065
1222
  if (!hadFinalNewline && content.endsWith("\n")) {
1066
- return content.slice(0, -1);
1223
+ return { content: content.slice(0, -1), warnings };
1067
1224
  }
1068
- return content;
1225
+ return { content, warnings };
1069
1226
  }
1070
1227
 
1071
1228
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1178,7 +1335,13 @@ async function applyNormalizedPatch(
1178
1335
  throw new ApplyPatchError("Diff contains no hunks");
1179
1336
  }
1180
1337
 
1181
- const newContent = applyHunksToContent(normalizedContent, input.path, hunks, fuzzyThreshold, allowFuzzy);
1338
+ const { content: newContent, warnings } = applyHunksToContent(
1339
+ normalizedContent,
1340
+ input.path,
1341
+ hunks,
1342
+ fuzzyThreshold,
1343
+ allowFuzzy,
1344
+ );
1182
1345
  const finalContent = bom + restoreLineEndings(newContent, lineEnding);
1183
1346
  const destPath = input.rename ? resolvePath(input.rename) : absolutePath;
1184
1347
  const isMove = Boolean(input.rename) && destPath !== absolutePath;
@@ -1204,6 +1367,7 @@ async function applyNormalizedPatch(
1204
1367
  oldContent: originalContent,
1205
1368
  newContent: finalContent,
1206
1369
  },
1370
+ warnings: warnings.length > 0 ? warnings : undefined,
1207
1371
  };
1208
1372
  }
1209
1373