@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/package.json +6 -6
  3. package/src/config/model-registry.ts +8 -0
  4. package/src/discovery/helpers.ts +12 -2
  5. package/src/extensibility/extensions/runner.ts +1 -0
  6. package/src/extensibility/extensions/types.ts +3 -0
  7. package/src/extensibility/extensions/wrapper.ts +4 -0
  8. package/src/extensibility/hooks/tool-wrapper.ts +4 -0
  9. package/src/ipy/executor.ts +4 -0
  10. package/src/lsp/index.ts +6 -4
  11. package/src/lsp/render.ts +115 -19
  12. package/src/lsp/types.ts +1 -0
  13. package/src/modes/components/tool-execution.ts +22 -6
  14. package/src/modes/controllers/event-controller.ts +1 -0
  15. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  16. package/src/modes/controllers/input-controller.ts +2 -2
  17. package/src/modes/interactive-mode.ts +30 -0
  18. package/src/modes/rpc/rpc-mode.ts +4 -0
  19. package/src/modes/theme/theme.ts +10 -107
  20. package/src/modes/types.ts +2 -0
  21. package/src/patch/applicator.ts +209 -41
  22. package/src/patch/fuzzy.ts +148 -28
  23. package/src/patch/index.ts +25 -2
  24. package/src/patch/parser.ts +5 -0
  25. package/src/patch/types.ts +25 -0
  26. package/src/sdk.ts +6 -0
  27. package/src/session/agent-session.ts +1 -0
  28. package/src/session/session-manager.ts +3 -0
  29. package/src/task/agents.ts +1 -1
  30. package/src/task/executor.ts +49 -17
  31. package/src/task/index.ts +16 -3
  32. package/src/task/types.ts +3 -3
  33. package/src/task/worker-protocol.ts +8 -0
  34. package/src/task/worker.ts +11 -0
  35. package/src/tools/bash.ts +1 -0
  36. package/src/tools/fetch.ts +1 -0
  37. package/src/tools/index.ts +19 -1
  38. package/src/tools/write.ts +82 -83
  39. package/src/tui/output-block.ts +26 -13
@@ -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
- // Extract 3 lines starting from match (0-indexed)
228
- const previewLines = contentLines.slice(lineNumber - 1, lineNumber + 2);
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, i) => ` ${lineNumber + i} | ${line.length > 60 ? `${line.slice(0, 57)}...` : 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 && aboveThresholdCount === 1) {
257
- return { match: best, closest: best };
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
- return { index: bestIndex, confidence: bestScore, matchCount };
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 { index: lineIndex, confidence: matchOutcome.match.confidence, matchCount: fallbackMatchCount };
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 { index: allSubstringMatches[0].index, confidence: 0.94, matchCount: 1 };
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 { index: allSubstringMatches[0].index, confidence: 0.94, matchCount: allSubstringMatches.length };
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("()")) {
@@ -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(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
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,
@@ -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) {
@@ -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
 
@@ -18,7 +18,7 @@ interface AgentFrontmatter {
18
18
  name: string;
19
19
  description: string;
20
20
  spawns?: string;
21
- model?: string;
21
+ model?: string | string[];
22
22
  thinkingLevel?: string;
23
23
  }
24
24