@oh-my-pi/pi-coding-agent 8.2.2 → 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 +31 -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/extensibility/extensions/wrapper.ts +4 -0
- package/src/extensibility/hooks/tool-wrapper.ts +4 -0
- package/src/ipy/executor.ts +4 -0
- package/src/lsp/index.ts +6 -4
- package/src/lsp/render.ts +115 -19
- package/src/lsp/types.ts +1 -0
- package/src/modes/components/tool-execution.ts +22 -6
- 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 +209 -41
- package/src/patch/fuzzy.ts +148 -28
- 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/bash.ts +1 -0
- package/src/tools/fetch.ts +1 -0
- package/src/tools/index.ts +19 -1
- package/src/tools/write.ts +82 -83
- package/src/tui/output-block.ts +26 -13
package/src/patch/fuzzy.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* fallback strategies for finding text in files.
|
|
6
6
|
*/
|
|
7
7
|
import { countLeadingWhitespace, normalizeForFuzzy, normalizeUnicode } from "./normalize";
|
|
8
|
-
import type { ContextLineResult, FuzzyMatch, MatchOutcome, SequenceSearchResult } from "./types";
|
|
8
|
+
import type { ContextLineResult, FuzzyMatch, MatchOutcome, SequenceMatchStrategy, SequenceSearchResult } from "./types";
|
|
9
9
|
|
|
10
10
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
11
|
// Constants
|
|
@@ -29,6 +29,12 @@ const PARTIAL_MATCH_MIN_LENGTH = 6;
|
|
|
29
29
|
/** Minimum ratio of pattern to line length for substring match */
|
|
30
30
|
const PARTIAL_MATCH_MIN_RATIO = 0.3;
|
|
31
31
|
|
|
32
|
+
/** Context lines to show before/after an ambiguous match preview */
|
|
33
|
+
const OCCURRENCE_PREVIEW_CONTEXT = 5;
|
|
34
|
+
|
|
35
|
+
/** Maximum line length for ambiguous match previews */
|
|
36
|
+
const OCCURRENCE_PREVIEW_MAX_LEN = 80;
|
|
37
|
+
|
|
32
38
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
33
39
|
// Core Algorithms
|
|
34
40
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -129,6 +135,7 @@ function computeLineOffsets(lines: string[]): number[] {
|
|
|
129
135
|
interface BestFuzzyMatchResult {
|
|
130
136
|
best?: FuzzyMatch;
|
|
131
137
|
aboveThresholdCount: number;
|
|
138
|
+
secondBestScore: number;
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
function findBestFuzzyMatchCore(
|
|
@@ -142,6 +149,7 @@ function findBestFuzzyMatchCore(
|
|
|
142
149
|
|
|
143
150
|
let best: FuzzyMatch | undefined;
|
|
144
151
|
let bestScore = -1;
|
|
152
|
+
let secondBestScore = -1;
|
|
145
153
|
let aboveThresholdCount = 0;
|
|
146
154
|
|
|
147
155
|
for (let start = 0; start <= contentLines.length - targetLines.length; start++) {
|
|
@@ -158,6 +166,7 @@ function findBestFuzzyMatchCore(
|
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
if (score > bestScore) {
|
|
169
|
+
secondBestScore = bestScore;
|
|
161
170
|
bestScore = score;
|
|
162
171
|
best = {
|
|
163
172
|
actualText: windowLines.join("\n"),
|
|
@@ -165,10 +174,12 @@ function findBestFuzzyMatchCore(
|
|
|
165
174
|
startLine: start + 1,
|
|
166
175
|
confidence: score,
|
|
167
176
|
};
|
|
177
|
+
} else if (score > secondBestScore) {
|
|
178
|
+
secondBestScore = score;
|
|
168
179
|
}
|
|
169
180
|
}
|
|
170
181
|
|
|
171
|
-
return { best, aboveThresholdCount };
|
|
182
|
+
return { best, aboveThresholdCount, secondBestScore };
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
function findBestFuzzyMatch(content: string, target: string, threshold: number): BestFuzzyMatchResult {
|
|
@@ -176,10 +187,10 @@ function findBestFuzzyMatch(content: string, target: string, threshold: number):
|
|
|
176
187
|
const targetLines = target.split("\n");
|
|
177
188
|
|
|
178
189
|
if (targetLines.length === 0 || target.length === 0) {
|
|
179
|
-
return { aboveThresholdCount: 0 };
|
|
190
|
+
return { aboveThresholdCount: 0, secondBestScore: 0 };
|
|
180
191
|
}
|
|
181
192
|
if (targetLines.length > contentLines.length) {
|
|
182
|
-
return { aboveThresholdCount: 0 };
|
|
193
|
+
return { aboveThresholdCount: 0, secondBestScore: 0 };
|
|
183
194
|
}
|
|
184
195
|
|
|
185
196
|
const offsets = computeLineOffsets(contentLines);
|
|
@@ -224,10 +235,14 @@ export function findMatch(
|
|
|
224
235
|
if (idx === -1) break;
|
|
225
236
|
const lineNumber = content.slice(0, idx).split("\n").length;
|
|
226
237
|
occurrenceLines.push(lineNumber);
|
|
227
|
-
|
|
228
|
-
const
|
|
238
|
+
const start = Math.max(0, lineNumber - 1 - OCCURRENCE_PREVIEW_CONTEXT);
|
|
239
|
+
const end = Math.min(contentLines.length, lineNumber + OCCURRENCE_PREVIEW_CONTEXT + 1);
|
|
240
|
+
const previewLines = contentLines.slice(start, end);
|
|
229
241
|
const preview = previewLines
|
|
230
|
-
.map((line,
|
|
242
|
+
.map((line, idx) => {
|
|
243
|
+
const num = start + idx + 1;
|
|
244
|
+
return ` ${num} | ${line.length > OCCURRENCE_PREVIEW_MAX_LEN ? `${line.slice(0, OCCURRENCE_PREVIEW_MAX_LEN - 3)}...` : line}`;
|
|
245
|
+
})
|
|
231
246
|
.join("\n");
|
|
232
247
|
occurrencePreviews.push(preview);
|
|
233
248
|
searchStart = idx + 1;
|
|
@@ -247,14 +262,25 @@ export function findMatch(
|
|
|
247
262
|
|
|
248
263
|
// Try fuzzy match
|
|
249
264
|
const threshold = options.threshold ?? DEFAULT_FUZZY_THRESHOLD;
|
|
250
|
-
const { best, aboveThresholdCount } = findBestFuzzyMatch(content, target, threshold);
|
|
265
|
+
const { best, aboveThresholdCount, secondBestScore } = findBestFuzzyMatch(content, target, threshold);
|
|
251
266
|
|
|
252
267
|
if (!best) {
|
|
253
268
|
return {};
|
|
254
269
|
}
|
|
255
270
|
|
|
256
|
-
if (options.allowFuzzy && best.confidence >= threshold
|
|
257
|
-
|
|
271
|
+
if (options.allowFuzzy && best.confidence >= threshold) {
|
|
272
|
+
if (aboveThresholdCount === 1) {
|
|
273
|
+
return { match: best, closest: best };
|
|
274
|
+
}
|
|
275
|
+
const dominantDelta = 0.08;
|
|
276
|
+
const dominantMin = 0.97;
|
|
277
|
+
if (
|
|
278
|
+
aboveThresholdCount > 1 &&
|
|
279
|
+
best.confidence >= dominantMin &&
|
|
280
|
+
best.confidence - secondBestScore >= dominantDelta
|
|
281
|
+
) {
|
|
282
|
+
return { match: best, closest: best, fuzzyMatches: aboveThresholdCount, dominantFuzzy: true };
|
|
283
|
+
}
|
|
258
284
|
}
|
|
259
285
|
|
|
260
286
|
return { closest: best, fuzzyMatches: aboveThresholdCount };
|
|
@@ -350,7 +376,7 @@ export function seekSequence(
|
|
|
350
376
|
const allowFuzzy = options?.allowFuzzy ?? true;
|
|
351
377
|
// Empty pattern matches immediately
|
|
352
378
|
if (pattern.length === 0) {
|
|
353
|
-
return { index: start, confidence: 1.0 };
|
|
379
|
+
return { index: start, confidence: 1.0, strategy: "exact" };
|
|
354
380
|
}
|
|
355
381
|
|
|
356
382
|
// Pattern longer than available content cannot match
|
|
@@ -366,35 +392,35 @@ export function seekSequence(
|
|
|
366
392
|
// Pass 1: Exact match
|
|
367
393
|
for (let i = from; i <= to; i++) {
|
|
368
394
|
if (matchesAt(lines, pattern, i, (a, b) => a === b)) {
|
|
369
|
-
return { index: i, confidence: 1.0 };
|
|
395
|
+
return { index: i, confidence: 1.0, strategy: "exact" };
|
|
370
396
|
}
|
|
371
397
|
}
|
|
372
398
|
|
|
373
399
|
// Pass 2: Trailing whitespace stripped
|
|
374
400
|
for (let i = from; i <= to; i++) {
|
|
375
401
|
if (matchesAt(lines, pattern, i, (a, b) => a.trimEnd() === b.trimEnd())) {
|
|
376
|
-
return { index: i, confidence: 0.99 };
|
|
402
|
+
return { index: i, confidence: 0.99, strategy: "trim-trailing" };
|
|
377
403
|
}
|
|
378
404
|
}
|
|
379
405
|
|
|
380
406
|
// Pass 3: Both leading and trailing whitespace stripped
|
|
381
407
|
for (let i = from; i <= to; i++) {
|
|
382
408
|
if (matchesAt(lines, pattern, i, (a, b) => a.trim() === b.trim())) {
|
|
383
|
-
return { index: i, confidence: 0.98 };
|
|
409
|
+
return { index: i, confidence: 0.98, strategy: "trim" };
|
|
384
410
|
}
|
|
385
411
|
}
|
|
386
412
|
|
|
387
413
|
// Pass 3b: Comment-prefix normalized match
|
|
388
414
|
for (let i = from; i <= to; i++) {
|
|
389
415
|
if (matchesAt(lines, pattern, i, (a, b) => stripCommentPrefix(a) === stripCommentPrefix(b))) {
|
|
390
|
-
return { index: i, confidence: 0.975 };
|
|
416
|
+
return { index: i, confidence: 0.975, strategy: "comment-prefix" };
|
|
391
417
|
}
|
|
392
418
|
}
|
|
393
419
|
|
|
394
420
|
// Pass 4: Normalize unicode punctuation
|
|
395
421
|
for (let i = from; i <= to; i++) {
|
|
396
422
|
if (matchesAt(lines, pattern, i, (a, b) => normalizeUnicode(a) === normalizeUnicode(b))) {
|
|
397
|
-
return { index: i, confidence: 0.97 };
|
|
423
|
+
return { index: i, confidence: 0.97, strategy: "unicode" };
|
|
398
424
|
}
|
|
399
425
|
}
|
|
400
426
|
|
|
@@ -406,14 +432,16 @@ export function seekSequence(
|
|
|
406
432
|
{
|
|
407
433
|
let firstMatch: number | undefined;
|
|
408
434
|
let matchCount = 0;
|
|
435
|
+
const matchIndices: number[] = [];
|
|
409
436
|
for (let i = from; i <= to; i++) {
|
|
410
437
|
if (matchesAt(lines, pattern, i, lineStartsWithPattern)) {
|
|
411
438
|
if (firstMatch === undefined) firstMatch = i;
|
|
412
439
|
matchCount++;
|
|
440
|
+
if (matchIndices.length < 5) matchIndices.push(i);
|
|
413
441
|
}
|
|
414
442
|
}
|
|
415
443
|
if (matchCount > 0) {
|
|
416
|
-
return { index: firstMatch, confidence: 0.965, matchCount };
|
|
444
|
+
return { index: firstMatch, confidence: 0.965, matchCount, matchIndices, strategy: "prefix" };
|
|
417
445
|
}
|
|
418
446
|
}
|
|
419
447
|
|
|
@@ -421,14 +449,16 @@ export function seekSequence(
|
|
|
421
449
|
{
|
|
422
450
|
let firstMatch: number | undefined;
|
|
423
451
|
let matchCount = 0;
|
|
452
|
+
const matchIndices: number[] = [];
|
|
424
453
|
for (let i = from; i <= to; i++) {
|
|
425
454
|
if (matchesAt(lines, pattern, i, lineIncludesPattern)) {
|
|
426
455
|
if (firstMatch === undefined) firstMatch = i;
|
|
427
456
|
matchCount++;
|
|
457
|
+
if (matchIndices.length < 5) matchIndices.push(i);
|
|
428
458
|
}
|
|
429
459
|
}
|
|
430
460
|
if (matchCount > 0) {
|
|
431
|
-
return { index: firstMatch, confidence: 0.94, matchCount };
|
|
461
|
+
return { index: firstMatch, confidence: 0.94, matchCount, matchIndices, strategy: "substring" };
|
|
432
462
|
}
|
|
433
463
|
}
|
|
434
464
|
|
|
@@ -454,16 +484,22 @@ export function seekSequence(
|
|
|
454
484
|
// Pass 7: Fuzzy matching - find best match above threshold
|
|
455
485
|
let bestIndex: number | undefined;
|
|
456
486
|
let bestScore = 0;
|
|
487
|
+
let secondBestScore = 0;
|
|
457
488
|
let matchCount = 0;
|
|
489
|
+
const matchIndices: number[] = [];
|
|
458
490
|
|
|
459
491
|
for (let i = searchStart; i <= maxStart; i++) {
|
|
460
492
|
const score = fuzzyScoreAt(lines, pattern, i);
|
|
461
493
|
if (score >= SEQUENCE_FUZZY_THRESHOLD) {
|
|
462
494
|
matchCount++;
|
|
495
|
+
if (matchIndices.length < 5) matchIndices.push(i);
|
|
463
496
|
}
|
|
464
497
|
if (score > bestScore) {
|
|
498
|
+
secondBestScore = bestScore;
|
|
465
499
|
bestScore = score;
|
|
466
500
|
bestIndex = i;
|
|
501
|
+
} else if (score > secondBestScore) {
|
|
502
|
+
secondBestScore = score;
|
|
467
503
|
}
|
|
468
504
|
}
|
|
469
505
|
|
|
@@ -473,16 +509,31 @@ export function seekSequence(
|
|
|
473
509
|
const score = fuzzyScoreAt(lines, pattern, i);
|
|
474
510
|
if (score >= SEQUENCE_FUZZY_THRESHOLD) {
|
|
475
511
|
matchCount++;
|
|
512
|
+
if (matchIndices.length < 5) matchIndices.push(i);
|
|
476
513
|
}
|
|
477
514
|
if (score > bestScore) {
|
|
515
|
+
secondBestScore = bestScore;
|
|
478
516
|
bestScore = score;
|
|
479
517
|
bestIndex = i;
|
|
518
|
+
} else if (score > secondBestScore) {
|
|
519
|
+
secondBestScore = score;
|
|
480
520
|
}
|
|
481
521
|
}
|
|
482
522
|
}
|
|
483
523
|
|
|
484
524
|
if (bestIndex !== undefined && bestScore >= SEQUENCE_FUZZY_THRESHOLD) {
|
|
485
|
-
|
|
525
|
+
const dominantDelta = 0.08;
|
|
526
|
+
const dominantMin = 0.97;
|
|
527
|
+
if (matchCount > 1 && bestScore >= dominantMin && bestScore - secondBestScore >= dominantDelta) {
|
|
528
|
+
return {
|
|
529
|
+
index: bestIndex,
|
|
530
|
+
confidence: bestScore,
|
|
531
|
+
matchCount: 1,
|
|
532
|
+
matchIndices,
|
|
533
|
+
strategy: "fuzzy-dominant",
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
return { index: bestIndex, confidence: bestScore, matchCount, matchIndices, strategy: "fuzzy" };
|
|
486
537
|
}
|
|
487
538
|
|
|
488
539
|
// Pass 8: Character-based fuzzy matching via findMatch
|
|
@@ -500,13 +551,59 @@ export function seekSequence(
|
|
|
500
551
|
const matchedContent = contentText.substring(0, matchOutcome.match.startIndex);
|
|
501
552
|
const lineIndex = start + matchedContent.split("\n").length - 1;
|
|
502
553
|
const fallbackMatchCount = matchOutcome.occurrences ?? matchOutcome.fuzzyMatches ?? 1;
|
|
503
|
-
return {
|
|
554
|
+
return {
|
|
555
|
+
index: lineIndex,
|
|
556
|
+
confidence: matchOutcome.match.confidence,
|
|
557
|
+
matchCount: fallbackMatchCount,
|
|
558
|
+
strategy: "character",
|
|
559
|
+
};
|
|
504
560
|
}
|
|
505
561
|
|
|
506
562
|
const fallbackMatchCount = matchOutcome.occurrences ?? matchOutcome.fuzzyMatches;
|
|
507
563
|
return { index: undefined, confidence: bestScore, matchCount: fallbackMatchCount };
|
|
508
564
|
}
|
|
509
565
|
|
|
566
|
+
export function findClosestSequenceMatch(
|
|
567
|
+
lines: string[],
|
|
568
|
+
pattern: string[],
|
|
569
|
+
options?: { start?: number; eof?: boolean },
|
|
570
|
+
): { index: number | undefined; confidence: number; strategy: SequenceMatchStrategy } {
|
|
571
|
+
if (pattern.length === 0) {
|
|
572
|
+
return { index: options?.start ?? 0, confidence: 1, strategy: "exact" };
|
|
573
|
+
}
|
|
574
|
+
if (pattern.length > lines.length) {
|
|
575
|
+
return { index: undefined, confidence: 0, strategy: "fuzzy" };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const start = options?.start ?? 0;
|
|
579
|
+
const eof = options?.eof ?? false;
|
|
580
|
+
const maxStart = lines.length - pattern.length;
|
|
581
|
+
const searchStart = eof && lines.length >= pattern.length ? maxStart : start;
|
|
582
|
+
|
|
583
|
+
let bestIndex: number | undefined;
|
|
584
|
+
let bestScore = 0;
|
|
585
|
+
|
|
586
|
+
for (let i = searchStart; i <= maxStart; i++) {
|
|
587
|
+
const score = fuzzyScoreAt(lines, pattern, i);
|
|
588
|
+
if (score > bestScore) {
|
|
589
|
+
bestScore = score;
|
|
590
|
+
bestIndex = i;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (eof && searchStart > start) {
|
|
595
|
+
for (let i = start; i < searchStart; i++) {
|
|
596
|
+
const score = fuzzyScoreAt(lines, pattern, i);
|
|
597
|
+
if (score > bestScore) {
|
|
598
|
+
bestScore = score;
|
|
599
|
+
bestIndex = i;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return { index: bestIndex, confidence: bestScore, strategy: "fuzzy" };
|
|
605
|
+
}
|
|
606
|
+
|
|
510
607
|
/**
|
|
511
608
|
* Find a context line in the file using progressive matching strategies.
|
|
512
609
|
*
|
|
@@ -527,14 +624,16 @@ export function findContextLine(
|
|
|
527
624
|
{
|
|
528
625
|
let firstMatch: number | undefined;
|
|
529
626
|
let matchCount = 0;
|
|
627
|
+
const matchIndices: number[] = [];
|
|
530
628
|
for (let i = startFrom; i < lines.length; i++) {
|
|
531
629
|
if (lines[i] === context) {
|
|
532
630
|
if (firstMatch === undefined) firstMatch = i;
|
|
533
631
|
matchCount++;
|
|
632
|
+
if (matchIndices.length < 5) matchIndices.push(i);
|
|
534
633
|
}
|
|
535
634
|
}
|
|
536
635
|
if (matchCount > 0) {
|
|
537
|
-
return { index: firstMatch, confidence: 1.0, matchCount };
|
|
636
|
+
return { index: firstMatch, confidence: 1.0, matchCount, matchIndices, strategy: "exact" };
|
|
538
637
|
}
|
|
539
638
|
}
|
|
540
639
|
|
|
@@ -542,14 +641,16 @@ export function findContextLine(
|
|
|
542
641
|
{
|
|
543
642
|
let firstMatch: number | undefined;
|
|
544
643
|
let matchCount = 0;
|
|
644
|
+
const matchIndices: number[] = [];
|
|
545
645
|
for (let i = startFrom; i < lines.length; i++) {
|
|
546
646
|
if (lines[i].trim() === trimmedContext) {
|
|
547
647
|
if (firstMatch === undefined) firstMatch = i;
|
|
548
648
|
matchCount++;
|
|
649
|
+
if (matchIndices.length < 5) matchIndices.push(i);
|
|
549
650
|
}
|
|
550
651
|
}
|
|
551
652
|
if (matchCount > 0) {
|
|
552
|
-
return { index: firstMatch, confidence: 0.99, matchCount };
|
|
653
|
+
return { index: firstMatch, confidence: 0.99, matchCount, matchIndices, strategy: "trim" };
|
|
553
654
|
}
|
|
554
655
|
}
|
|
555
656
|
|
|
@@ -558,14 +659,16 @@ export function findContextLine(
|
|
|
558
659
|
{
|
|
559
660
|
let firstMatch: number | undefined;
|
|
560
661
|
let matchCount = 0;
|
|
662
|
+
const matchIndices: number[] = [];
|
|
561
663
|
for (let i = startFrom; i < lines.length; i++) {
|
|
562
664
|
if (normalizeUnicode(lines[i]) === normalizedContext) {
|
|
563
665
|
if (firstMatch === undefined) firstMatch = i;
|
|
564
666
|
matchCount++;
|
|
667
|
+
if (matchIndices.length < 5) matchIndices.push(i);
|
|
565
668
|
}
|
|
566
669
|
}
|
|
567
670
|
if (matchCount > 0) {
|
|
568
|
-
return { index: firstMatch, confidence: 0.98, matchCount };
|
|
671
|
+
return { index: firstMatch, confidence: 0.98, matchCount, matchIndices, strategy: "unicode" };
|
|
569
672
|
}
|
|
570
673
|
}
|
|
571
674
|
|
|
@@ -578,15 +681,17 @@ export function findContextLine(
|
|
|
578
681
|
if (contextNorm.length > 0) {
|
|
579
682
|
let firstMatch: number | undefined;
|
|
580
683
|
let matchCount = 0;
|
|
684
|
+
const matchIndices: number[] = [];
|
|
581
685
|
for (let i = startFrom; i < lines.length; i++) {
|
|
582
686
|
const lineNorm = normalizeForFuzzy(lines[i]);
|
|
583
687
|
if (lineNorm.startsWith(contextNorm)) {
|
|
584
688
|
if (firstMatch === undefined) firstMatch = i;
|
|
585
689
|
matchCount++;
|
|
690
|
+
if (matchIndices.length < 5) matchIndices.push(i);
|
|
586
691
|
}
|
|
587
692
|
}
|
|
588
693
|
if (matchCount > 0) {
|
|
589
|
-
return { index: firstMatch, confidence: 0.96, matchCount };
|
|
694
|
+
return { index: firstMatch, confidence: 0.96, matchCount, matchIndices, strategy: "prefix" };
|
|
590
695
|
}
|
|
591
696
|
}
|
|
592
697
|
|
|
@@ -603,10 +708,17 @@ export function findContextLine(
|
|
|
603
708
|
allSubstringMatches.push({ index: i, ratio });
|
|
604
709
|
}
|
|
605
710
|
}
|
|
711
|
+
const matchIndices = allSubstringMatches.slice(0, 5).map(match => match.index);
|
|
606
712
|
|
|
607
713
|
// If exactly one substring match, accept it regardless of ratio
|
|
608
714
|
if (allSubstringMatches.length === 1) {
|
|
609
|
-
return {
|
|
715
|
+
return {
|
|
716
|
+
index: allSubstringMatches[0].index,
|
|
717
|
+
confidence: 0.94,
|
|
718
|
+
matchCount: 1,
|
|
719
|
+
matchIndices,
|
|
720
|
+
strategy: "substring",
|
|
721
|
+
};
|
|
610
722
|
}
|
|
611
723
|
|
|
612
724
|
// Multiple matches: filter by ratio to disambiguate
|
|
@@ -619,13 +731,19 @@ export function findContextLine(
|
|
|
619
731
|
}
|
|
620
732
|
}
|
|
621
733
|
if (matchCount > 0) {
|
|
622
|
-
return { index: firstMatch, confidence: 0.94, matchCount };
|
|
734
|
+
return { index: firstMatch, confidence: 0.94, matchCount, matchIndices, strategy: "substring" };
|
|
623
735
|
}
|
|
624
736
|
|
|
625
737
|
// If we had substring matches but none passed ratio filter,
|
|
626
738
|
// return ambiguous result so caller knows matches exist
|
|
627
739
|
if (allSubstringMatches.length > 1) {
|
|
628
|
-
return {
|
|
740
|
+
return {
|
|
741
|
+
index: allSubstringMatches[0].index,
|
|
742
|
+
confidence: 0.94,
|
|
743
|
+
matchCount: allSubstringMatches.length,
|
|
744
|
+
matchIndices,
|
|
745
|
+
strategy: "substring",
|
|
746
|
+
};
|
|
629
747
|
}
|
|
630
748
|
}
|
|
631
749
|
|
|
@@ -633,12 +751,14 @@ export function findContextLine(
|
|
|
633
751
|
let bestIndex: number | undefined;
|
|
634
752
|
let bestScore = 0;
|
|
635
753
|
let matchCount = 0;
|
|
754
|
+
const matchIndices: number[] = [];
|
|
636
755
|
|
|
637
756
|
for (let i = startFrom; i < lines.length; i++) {
|
|
638
757
|
const lineNorm = normalizeForFuzzy(lines[i]);
|
|
639
758
|
const score = similarity(lineNorm, contextNorm);
|
|
640
759
|
if (score >= CONTEXT_FUZZY_THRESHOLD) {
|
|
641
760
|
matchCount++;
|
|
761
|
+
if (matchIndices.length < 5) matchIndices.push(i);
|
|
642
762
|
}
|
|
643
763
|
if (score > bestScore) {
|
|
644
764
|
bestScore = score;
|
|
@@ -647,7 +767,7 @@ export function findContextLine(
|
|
|
647
767
|
}
|
|
648
768
|
|
|
649
769
|
if (bestIndex !== undefined && bestScore >= CONTEXT_FUZZY_THRESHOLD) {
|
|
650
|
-
return { index: bestIndex, confidence: bestScore, matchCount };
|
|
770
|
+
return { index: bestIndex, confidence: bestScore, matchCount, matchIndices, strategy: "fuzzy" };
|
|
651
771
|
}
|
|
652
772
|
|
|
653
773
|
if (!options?.skipFunctionFallback && trimmedContext.endsWith("()")) {
|
package/src/patch/index.ts
CHANGED
|
@@ -167,6 +167,27 @@ class LspFileSystem implements FileSystem {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
function mergeDiagnosticsWithWarnings(
|
|
171
|
+
diagnostics: FileDiagnosticsResult | undefined,
|
|
172
|
+
warnings: string[],
|
|
173
|
+
): FileDiagnosticsResult | undefined {
|
|
174
|
+
if (warnings.length === 0) return diagnostics;
|
|
175
|
+
const warningMessages = warnings.map(warning => `patch: ${warning}`);
|
|
176
|
+
if (!diagnostics) {
|
|
177
|
+
return {
|
|
178
|
+
server: "patch",
|
|
179
|
+
messages: warningMessages,
|
|
180
|
+
summary: `Patch warnings: ${warnings.length}`,
|
|
181
|
+
errored: false,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
...diagnostics,
|
|
186
|
+
messages: [...warningMessages, ...diagnostics.messages],
|
|
187
|
+
summary: `${diagnostics.summary}; Patch warnings: ${warnings.length}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
170
191
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
171
192
|
// Tool Class
|
|
172
193
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -319,9 +340,11 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
319
340
|
const flushedDiagnostics = await flushLspWritethroughBatch(batchRequest.id, this.session.cwd, signal);
|
|
320
341
|
diagnostics ??= flushedDiagnostics;
|
|
321
342
|
}
|
|
343
|
+
const patchWarnings = result.warnings ?? [];
|
|
344
|
+
const mergedDiagnostics = mergeDiagnosticsWithWarnings(diagnostics, patchWarnings);
|
|
322
345
|
|
|
323
346
|
const meta = outputMeta()
|
|
324
|
-
.diagnostics(
|
|
347
|
+
.diagnostics(mergedDiagnostics?.summary ?? "", mergedDiagnostics?.messages ?? [])
|
|
325
348
|
.get();
|
|
326
349
|
|
|
327
350
|
return {
|
|
@@ -329,7 +352,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
329
352
|
details: {
|
|
330
353
|
diff: diffResult.diff,
|
|
331
354
|
firstChangedLine: diffResult.firstChangedLine,
|
|
332
|
-
diagnostics,
|
|
355
|
+
diagnostics: mergedDiagnostics,
|
|
333
356
|
op,
|
|
334
357
|
rename: effRename,
|
|
335
358
|
meta,
|
package/src/patch/parser.ts
CHANGED
|
@@ -328,6 +328,11 @@ function parseOneHunk(lines: string[], lineNumber: number, allowMissingContext:
|
|
|
328
328
|
for (let i = startIndex; i < lines.length; i++) {
|
|
329
329
|
const line = lines[i];
|
|
330
330
|
const trimmed = line.trim();
|
|
331
|
+
const nextLine = lines[i + 1];
|
|
332
|
+
|
|
333
|
+
if (line === "" && parsedLines > 0 && nextLine?.trimStart().startsWith("@@")) {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
331
336
|
|
|
332
337
|
if (!isDiffContentLine(line) && line.trimEnd() === EOF_MARKER && line.startsWith(EOF_MARKER)) {
|
|
333
338
|
if (parsedLines === 0) {
|
package/src/patch/types.ts
CHANGED
|
@@ -46,9 +46,23 @@ export interface MatchOutcome {
|
|
|
46
46
|
occurrencePreviews?: string[];
|
|
47
47
|
/** Number of fuzzy matches above threshold */
|
|
48
48
|
fuzzyMatches?: number;
|
|
49
|
+
/** True when a dominant fuzzy match was accepted despite multiple candidates */
|
|
50
|
+
dominantFuzzy?: boolean;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
/** Result of a sequence search */
|
|
54
|
+
export type SequenceMatchStrategy =
|
|
55
|
+
| "exact"
|
|
56
|
+
| "trim-trailing"
|
|
57
|
+
| "trim"
|
|
58
|
+
| "comment-prefix"
|
|
59
|
+
| "unicode"
|
|
60
|
+
| "prefix"
|
|
61
|
+
| "substring"
|
|
62
|
+
| "fuzzy"
|
|
63
|
+
| "fuzzy-dominant"
|
|
64
|
+
| "character";
|
|
65
|
+
|
|
52
66
|
export interface SequenceSearchResult {
|
|
53
67
|
/** Starting line index of the match (0-indexed) */
|
|
54
68
|
index: number | undefined;
|
|
@@ -56,9 +70,15 @@ export interface SequenceSearchResult {
|
|
|
56
70
|
confidence: number;
|
|
57
71
|
/** Number of matches at the same confidence level (for ambiguity detection) */
|
|
58
72
|
matchCount?: number;
|
|
73
|
+
/** Sample of matching indices (0-indexed, up to a small limit) */
|
|
74
|
+
matchIndices?: number[];
|
|
75
|
+
/** Matching strategy used */
|
|
76
|
+
strategy?: SequenceMatchStrategy;
|
|
59
77
|
}
|
|
60
78
|
|
|
61
79
|
/** Result of a context line search */
|
|
80
|
+
export type ContextMatchStrategy = "exact" | "trim" | "unicode" | "prefix" | "substring" | "fuzzy";
|
|
81
|
+
|
|
62
82
|
export interface ContextLineResult {
|
|
63
83
|
/** Index of the matching line (0-indexed) */
|
|
64
84
|
index: number | undefined;
|
|
@@ -66,6 +86,10 @@ export interface ContextLineResult {
|
|
|
66
86
|
confidence: number;
|
|
67
87
|
/** Number of matches at the same confidence level (for ambiguity detection) */
|
|
68
88
|
matchCount?: number;
|
|
89
|
+
/** Sample of matching indices (0-indexed, up to a small limit) */
|
|
90
|
+
matchIndices?: number[];
|
|
91
|
+
/** Matching strategy used */
|
|
92
|
+
strategy?: ContextMatchStrategy;
|
|
69
93
|
}
|
|
70
94
|
|
|
71
95
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -133,6 +157,7 @@ export interface FileChange {
|
|
|
133
157
|
/** Result of applying a patch */
|
|
134
158
|
export interface ApplyPatchResult {
|
|
135
159
|
change: FileChange;
|
|
160
|
+
warnings?: string[];
|
|
136
161
|
}
|
|
137
162
|
|
|
138
163
|
/** Options for applying a patch */
|
package/src/sdk.ts
CHANGED
|
@@ -166,6 +166,8 @@ export interface CreateAgentSessionOptions {
|
|
|
166
166
|
|
|
167
167
|
/** Enable LSP integration (tool, formatting, diagnostics, warmup). Default: true */
|
|
168
168
|
enableLsp?: boolean;
|
|
169
|
+
/** Skip Python kernel availability check and prelude warmup */
|
|
170
|
+
skipPythonPreflight?: boolean;
|
|
169
171
|
|
|
170
172
|
/** Tool names explicitly requested (enables disabled-by-default tools) */
|
|
171
173
|
toolNames?: string[];
|
|
@@ -734,6 +736,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
734
736
|
cwd,
|
|
735
737
|
hasUI: options.hasUI ?? false,
|
|
736
738
|
enableLsp,
|
|
739
|
+
skipPythonPreflight: options.skipPythonPreflight,
|
|
740
|
+
contextFiles,
|
|
741
|
+
skills,
|
|
737
742
|
eventBus,
|
|
738
743
|
outputSchema: options.outputSchema,
|
|
739
744
|
requireCompleteTool: options.requireCompleteTool,
|
|
@@ -989,6 +994,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
989
994
|
|
|
990
995
|
const promptTemplates = options.promptTemplates ?? (await discoverPromptTemplates(cwd, agentDir));
|
|
991
996
|
time("discoverPromptTemplates");
|
|
997
|
+
toolSession.promptTemplates = promptTemplates;
|
|
992
998
|
|
|
993
999
|
const slashCommands = options.slashCommands ?? (await discoverSlashCommands(cwd));
|
|
994
1000
|
time("discoverSlashCommands");
|
|
@@ -205,6 +205,7 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
205
205
|
input: async (_title, _placeholder, _dialogOptions) => undefined,
|
|
206
206
|
notify: () => {},
|
|
207
207
|
setStatus: () => {},
|
|
208
|
+
setWorkingMessage: () => {},
|
|
208
209
|
setWidget: () => {},
|
|
209
210
|
setTitle: () => {},
|
|
210
211
|
custom: async () => undefined as never,
|
|
@@ -1027,6 +1027,9 @@ export class SessionManager {
|
|
|
1027
1027
|
const explicitPath = this.sessionFile;
|
|
1028
1028
|
this._newSessionSync();
|
|
1029
1029
|
this.sessionFile = explicitPath; // preserve explicit path from --session flag
|
|
1030
|
+
await this._rewriteFile();
|
|
1031
|
+
this.flushed = true;
|
|
1032
|
+
return;
|
|
1030
1033
|
}
|
|
1031
1034
|
}
|
|
1032
1035
|
|