@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.
- package/CHANGELOG.md +17 -0
- package/package.json +6 -6
- package/src/config/model-registry.ts +8 -0
- package/src/discovery/helpers.ts +12 -2
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/ipy/executor.ts +4 -0
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +30 -0
- package/src/modes/rpc/rpc-mode.ts +4 -0
- package/src/modes/theme/theme.ts +10 -107
- package/src/modes/types.ts +2 -0
- package/src/patch/applicator.ts +202 -38
- package/src/patch/fuzzy.ts +135 -25
- package/src/patch/index.ts +25 -2
- package/src/patch/parser.ts +5 -0
- package/src/patch/types.ts +25 -0
- package/src/sdk.ts +6 -0
- package/src/session/agent-session.ts +1 -0
- package/src/session/session-manager.ts +3 -0
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +49 -17
- package/src/task/index.ts +16 -3
- package/src/task/types.ts +3 -3
- package/src/task/worker-protocol.ts +8 -0
- package/src/task/worker.ts +11 -0
- package/src/tools/index.ts +19 -1
package/src/patch/applicator.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|