@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2
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 +316 -1
- package/package.json +86 -24
- package/scripts/format-prompts.ts +2 -2
- package/src/autoresearch/apply-contract-to-state.ts +24 -0
- package/src/autoresearch/contract.ts +0 -44
- package/src/autoresearch/dashboard.ts +1 -2
- package/src/autoresearch/git.ts +116 -30
- package/src/autoresearch/helpers.ts +49 -0
- package/src/autoresearch/index.ts +28 -187
- package/src/autoresearch/prompt.md +26 -9
- package/src/autoresearch/state.ts +0 -6
- package/src/autoresearch/tools/init-experiment.ts +202 -117
- package/src/autoresearch/tools/log-experiment.ts +123 -178
- package/src/autoresearch/tools/run-experiment.ts +48 -10
- package/src/autoresearch/types.ts +2 -2
- package/src/capability/index.ts +4 -2
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/grep-cli.ts +8 -8
- package/src/cli/grievances-cli.ts +78 -0
- package/src/cli/read-cli.ts +67 -0
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/update-cli.ts +3 -3
- package/src/cli.ts +2 -0
- package/src/commands/grep.ts +6 -1
- package/src/commands/grievances.ts +20 -0
- package/src/commands/read.ts +33 -0
- package/src/commit/agentic/agent.ts +5 -8
- package/src/commit/agentic/index.ts +22 -26
- package/src/commit/agentic/tools/analyze-file.ts +3 -3
- package/src/commit/agentic/tools/git-file-diff.ts +3 -6
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +6 -9
- package/src/commit/agentic/tools/index.ts +6 -8
- package/src/commit/agentic/tools/propose-commit.ts +4 -7
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/split-commit.ts +4 -4
- package/src/commit/agentic/validation.ts +1 -1
- package/src/commit/analysis/conventional.ts +4 -4
- package/src/commit/analysis/summary.ts +3 -3
- package/src/commit/changelog/generate.ts +4 -4
- package/src/commit/changelog/index.ts +5 -9
- package/src/commit/map-reduce/map-phase.ts +4 -4
- package/src/commit/map-reduce/reduce-phase.ts +4 -4
- package/src/commit/pipeline.ts +13 -16
- package/src/config/keybindings.ts +7 -6
- package/src/config/prompt-templates.ts +44 -226
- package/src/config/resolve-config-value.ts +4 -2
- package/src/config/settings-schema.ts +98 -2
- package/src/config/settings.ts +25 -26
- package/src/dap/client.ts +674 -0
- package/src/dap/config.ts +150 -0
- package/src/dap/defaults.json +211 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1255 -0
- package/src/dap/types.ts +600 -0
- package/src/debug/log-viewer.ts +3 -2
- package/src/discovery/builtin.ts +1 -2
- package/src/discovery/codex.ts +2 -2
- package/src/discovery/github.ts +2 -1
- package/src/discovery/helpers.ts +2 -2
- package/src/discovery/opencode.ts +2 -2
- package/src/edit/diff.ts +818 -0
- package/src/edit/index.ts +309 -0
- package/src/edit/line-hash.ts +67 -0
- package/src/edit/modes/chunk.ts +454 -0
- package/src/{patch → edit/modes}/hashline.ts +741 -361
- package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
- package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
- package/src/{patch → edit}/normalize.ts +97 -76
- package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
- package/src/exec/bash-executor.ts +4 -2
- package/src/exec/idle-timeout-watchdog.ts +126 -0
- package/src/exec/non-interactive-env.ts +5 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
- package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
- package/src/extensibility/custom-commands/loader.ts +1 -2
- package/src/extensibility/custom-tools/loader.ts +34 -11
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +9 -4
- package/src/extensibility/extensions/runner.ts +24 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/hooks/loader.ts +5 -6
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/plugins/doctor.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
- package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
- package/src/extensibility/slash-commands.ts +3 -7
- package/src/index.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/ipy/executor.ts +58 -17
- package/src/ipy/gateway-coordinator.ts +6 -4
- package/src/ipy/kernel.ts +45 -22
- package/src/ipy/runtime.ts +2 -2
- package/src/lsp/client.ts +7 -4
- package/src/lsp/clients/lsp-linter-client.ts +4 -4
- package/src/lsp/config.ts +2 -2
- package/src/lsp/defaults.json +688 -154
- package/src/lsp/index.ts +234 -45
- package/src/lsp/lspmux.ts +2 -2
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +12 -1
- package/src/lsp/utils.ts +8 -1
- package/src/main.ts +125 -47
- package/src/memories/index.ts +4 -5
- package/src/modes/acp/acp-agent.ts +563 -163
- package/src/modes/acp/acp-event-mapper.ts +9 -1
- package/src/modes/acp/acp-mode.ts +4 -2
- package/src/modes/components/agent-dashboard.ts +3 -4
- package/src/modes/components/diff.ts +6 -7
- package/src/modes/components/footer.ts +9 -29
- package/src/modes/components/hook-editor.ts +3 -3
- package/src/modes/components/hook-selector.ts +6 -1
- package/src/modes/components/read-tool-group.ts +6 -12
- package/src/modes/components/session-observer-overlay.ts +472 -0
- package/src/modes/components/settings-defs.ts +24 -0
- package/src/modes/components/status-line.ts +15 -61
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/btw-controller.ts +2 -2
- package/src/modes/controllers/command-controller.ts +4 -2
- package/src/modes/controllers/event-controller.ts +59 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +15 -8
- package/src/modes/controllers/selector-controller.ts +26 -0
- package/src/modes/index.ts +20 -2
- package/src/modes/interactive-mode.ts +278 -69
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/rpc-client.ts +178 -13
- package/src/modes/rpc/rpc-mode.ts +73 -3
- package/src/modes/rpc/rpc-types.ts +53 -1
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +0 -42
- package/src/modes/theme/theme.ts +80 -8
- package/src/modes/types.ts +4 -2
- package/src/modes/utils/keybinding-matchers.ts +9 -0
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +8 -1
- package/src/prompts/tools/chunk-edit.md +219 -0
- package/src/prompts/tools/debug.md +43 -0
- package/src/prompts/tools/grep.md +3 -0
- package/src/prompts/tools/lsp.md +5 -5
- package/src/prompts/tools/read-chunk.md +17 -0
- package/src/prompts/tools/read.md +19 -5
- package/src/sdk.ts +216 -165
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +25 -17
- package/src/session/agent-session.ts +381 -286
- package/src/session/agent-storage.ts +12 -12
- package/src/session/compaction/branch-summarization.ts +3 -3
- package/src/session/compaction/compaction.ts +5 -6
- package/src/session/compaction/utils.ts +3 -3
- package/src/session/history-storage.ts +62 -19
- package/src/session/messages.ts +3 -3
- package/src/session/session-dump-format.ts +203 -0
- package/src/session/session-manager.ts +15 -5
- package/src/session/session-storage.ts +4 -2
- package/src/session/streaming-output.ts +1 -1
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +56 -8
- package/src/ssh/connection-manager.ts +2 -2
- package/src/ssh/sshfs-mount.ts +5 -5
- package/src/stt/downloader.ts +4 -4
- package/src/stt/recorder.ts +4 -4
- package/src/stt/transcriber.ts +2 -2
- package/src/system-prompt.ts +25 -13
- package/src/task/agents.ts +5 -6
- package/src/task/commands.ts +2 -5
- package/src/task/executor.ts +32 -4
- package/src/task/index.ts +91 -82
- package/src/task/template.ts +2 -2
- package/src/task/types.ts +25 -0
- package/src/task/worktree.ts +131 -149
- package/src/tools/ask.ts +2 -3
- package/src/tools/ast-edit.ts +7 -7
- package/src/tools/ast-grep.ts +7 -7
- package/src/tools/auto-generated-guard.ts +36 -41
- package/src/tools/await-tool.ts +2 -2
- package/src/tools/bash.ts +5 -23
- package/src/tools/browser.ts +4 -5
- package/src/tools/calculator.ts +2 -3
- package/src/tools/cancel-job.ts +2 -2
- package/src/tools/checkpoint.ts +3 -3
- package/src/tools/debug.ts +1007 -0
- package/src/tools/exit-plan-mode.ts +3 -3
- package/src/tools/fetch.ts +67 -3
- package/src/tools/find.ts +4 -5
- package/src/tools/fs-cache-invalidation.ts +5 -0
- package/src/tools/gemini-image.ts +13 -5
- package/src/tools/gh.ts +130 -308
- package/src/tools/grep.ts +57 -9
- package/src/tools/index.ts +44 -22
- package/src/tools/inspect-image.ts +4 -4
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/python.ts +19 -6
- package/src/tools/read.ts +211 -146
- package/src/tools/render-mermaid.ts +2 -3
- package/src/tools/render-utils.ts +20 -6
- package/src/tools/renderers.ts +3 -1
- package/src/tools/report-tool-issue.ts +80 -0
- package/src/tools/resolve.ts +70 -39
- package/src/tools/search-tool-bm25.ts +2 -2
- package/src/tools/ssh.ts +2 -2
- package/src/tools/todo-write.ts +2 -2
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/write.ts +5 -6
- package/src/tui/tree-list.ts +3 -1
- package/src/utils/clipboard.ts +80 -0
- package/src/utils/commit-message-generator.ts +2 -3
- package/src/utils/edit-mode.ts +49 -0
- package/src/utils/external-editor.ts +11 -5
- package/src/utils/file-display-mode.ts +6 -5
- package/src/utils/file-mentions.ts +8 -7
- package/src/utils/git.ts +1400 -0
- package/src/utils/image-loading.ts +98 -0
- package/src/utils/title-generator.ts +2 -3
- package/src/utils/tools-manager.ts +6 -6
- package/src/web/scrapers/choosealicense.ts +1 -1
- package/src/web/search/index.ts +3 -3
- package/src/web/search/render.ts +6 -4
- package/src/autoresearch/command-initialize.md +0 -34
- package/src/commit/git/errors.ts +0 -9
- package/src/commit/git/index.ts +0 -210
- package/src/commit/git/operations.ts +0 -54
- package/src/patch/diff.ts +0 -433
- package/src/patch/index.ts +0 -888
- package/src/patch/parser.ts +0 -532
- package/src/patch/types.ts +0 -292
- package/src/prompts/agents/oracle.md +0 -77
- package/src/tools/gh-cli.ts +0 -125
- package/src/tools/pending-action.ts +0 -49
- package/src/utils/child-process.ts +0 -88
- package/src/utils/frontmatter.ts +0 -117
- package/src/utils/image-input.ts +0 -274
- package/src/utils/mime.ts +0 -53
- package/src/utils/prompt-format.ts +0 -170
|
@@ -4,8 +4,152 @@
|
|
|
4
4
|
* Provides both character-level and line-level fuzzy matching with progressive
|
|
5
5
|
* fallback strategies for finding text in files.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
7
|
+
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
8
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
10
|
+
import type { WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
|
|
11
|
+
import type { ToolSession } from "../../tools";
|
|
12
|
+
import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
|
|
13
|
+
import { outputMeta } from "../../tools/output-meta";
|
|
14
|
+
import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
|
|
15
|
+
import { generateDiffString, replaceText } from "../diff";
|
|
16
|
+
import {
|
|
17
|
+
countLeadingWhitespace,
|
|
18
|
+
detectLineEnding,
|
|
19
|
+
normalizeForFuzzy,
|
|
20
|
+
normalizeToLF,
|
|
21
|
+
normalizeUnicode,
|
|
22
|
+
restoreLineEndings,
|
|
23
|
+
stripBom,
|
|
24
|
+
} from "../normalize";
|
|
25
|
+
import type { EditToolDetails, LspBatchRequest } from "../renderer";
|
|
26
|
+
|
|
27
|
+
export interface FuzzyMatch {
|
|
28
|
+
actualText: string;
|
|
29
|
+
startIndex: number;
|
|
30
|
+
startLine: number;
|
|
31
|
+
confidence: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MatchOutcome {
|
|
35
|
+
match?: FuzzyMatch;
|
|
36
|
+
closest?: FuzzyMatch;
|
|
37
|
+
occurrences?: number;
|
|
38
|
+
occurrenceLines?: number[];
|
|
39
|
+
occurrencePreviews?: string[];
|
|
40
|
+
fuzzyMatches?: number;
|
|
41
|
+
dominantFuzzy?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type SequenceMatchStrategy =
|
|
45
|
+
| "exact"
|
|
46
|
+
| "trim-trailing"
|
|
47
|
+
| "trim"
|
|
48
|
+
| "comment-prefix"
|
|
49
|
+
| "unicode"
|
|
50
|
+
| "prefix"
|
|
51
|
+
| "substring"
|
|
52
|
+
| "fuzzy"
|
|
53
|
+
| "fuzzy-dominant"
|
|
54
|
+
| "character";
|
|
55
|
+
|
|
56
|
+
export interface SequenceSearchResult {
|
|
57
|
+
index: number | undefined;
|
|
58
|
+
confidence: number;
|
|
59
|
+
matchCount?: number;
|
|
60
|
+
matchIndices?: number[];
|
|
61
|
+
strategy?: SequenceMatchStrategy;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type ContextMatchStrategy = "exact" | "trim" | "unicode" | "prefix" | "substring" | "fuzzy";
|
|
65
|
+
|
|
66
|
+
export interface ContextLineResult {
|
|
67
|
+
index: number | undefined;
|
|
68
|
+
confidence: number;
|
|
69
|
+
matchCount?: number;
|
|
70
|
+
matchIndices?: number[];
|
|
71
|
+
strategy?: ContextMatchStrategy;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class EditMatchError extends Error {
|
|
75
|
+
constructor(
|
|
76
|
+
readonly path: string,
|
|
77
|
+
readonly searchText: string,
|
|
78
|
+
readonly closest: FuzzyMatch | undefined,
|
|
79
|
+
readonly options: { allowFuzzy: boolean; threshold: number; fuzzyMatches?: number },
|
|
80
|
+
) {
|
|
81
|
+
super(EditMatchError.formatMessage(path, searchText, closest, options));
|
|
82
|
+
this.name = "EditMatchError";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static formatMessage(
|
|
86
|
+
path: string,
|
|
87
|
+
searchText: string,
|
|
88
|
+
closest: FuzzyMatch | undefined,
|
|
89
|
+
options: { allowFuzzy: boolean; threshold: number; fuzzyMatches?: number },
|
|
90
|
+
): string {
|
|
91
|
+
if (!closest) {
|
|
92
|
+
return options.allowFuzzy
|
|
93
|
+
? `Could not find a close enough match in ${path}.`
|
|
94
|
+
: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const similarity = Math.round(closest.confidence * 100);
|
|
98
|
+
const searchLines = searchText.split("\n");
|
|
99
|
+
const actualLines = closest.actualText.split("\n");
|
|
100
|
+
const { oldLine, newLine } = findFirstDifferentLine(searchLines, actualLines);
|
|
101
|
+
const thresholdPercent = Math.round(options.threshold * 100);
|
|
102
|
+
|
|
103
|
+
const hint = options.allowFuzzy
|
|
104
|
+
? options.fuzzyMatches && options.fuzzyMatches > 1
|
|
105
|
+
? `Found ${options.fuzzyMatches} high-confidence matches. Provide more context to make it unique.`
|
|
106
|
+
: `Closest match was below the ${thresholdPercent}% similarity threshold.`
|
|
107
|
+
: "Fuzzy matching is disabled. Enable 'Edit fuzzy match' in settings to accept high-confidence matches.";
|
|
108
|
+
|
|
109
|
+
return [
|
|
110
|
+
options.allowFuzzy
|
|
111
|
+
? `Could not find a close enough match in ${path}.`
|
|
112
|
+
: `Could not find the exact text in ${path}.`,
|
|
113
|
+
``,
|
|
114
|
+
`Closest match (${similarity}% similar) at line ${closest.startLine}:`,
|
|
115
|
+
` - ${oldLine}`,
|
|
116
|
+
` + ${newLine}`,
|
|
117
|
+
hint,
|
|
118
|
+
].join("\n");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findFirstDifferentLine(oldLines: string[], newLines: string[]): { oldLine: string; newLine: string } {
|
|
123
|
+
const max = Math.max(oldLines.length, newLines.length);
|
|
124
|
+
for (let i = 0; i < max; i++) {
|
|
125
|
+
const oldLine = oldLines[i] ?? "";
|
|
126
|
+
const newLine = newLines[i] ?? "";
|
|
127
|
+
if (oldLine !== newLine) {
|
|
128
|
+
return { oldLine, newLine };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { oldLine: oldLines[0] ?? "", newLine: newLines[0] ?? "" };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatOccurrenceError(path: string, matchOutcome: MatchOutcome): string {
|
|
135
|
+
const previews = matchOutcome.occurrencePreviews?.join("\n\n") ?? "";
|
|
136
|
+
const moreMsg =
|
|
137
|
+
matchOutcome.occurrences && matchOutcome.occurrences > MAX_RECORDED_MATCHES
|
|
138
|
+
? ` (showing first ${MAX_RECORDED_MATCHES} of ${matchOutcome.occurrences})`
|
|
139
|
+
: "";
|
|
140
|
+
return `Found ${matchOutcome.occurrences} occurrences in ${path}${moreMsg}:\n\n${previews}\n\nAdd more context lines to disambiguate.`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readReplaceFileContent(absolutePath: string, path: string): Promise<string> {
|
|
144
|
+
try {
|
|
145
|
+
return await Bun.file(absolutePath).text();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (isEnoent(error)) {
|
|
148
|
+
throw new Error(`File not found: ${path}`);
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
9
153
|
|
|
10
154
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
155
|
// Constants
|
|
@@ -35,6 +179,135 @@ const OCCURRENCE_PREVIEW_CONTEXT = 5;
|
|
|
35
179
|
/** Maximum line length for ambiguous match previews */
|
|
36
180
|
const OCCURRENCE_PREVIEW_MAX_LEN = 80;
|
|
37
181
|
|
|
182
|
+
/** Maximum number of match indices or previews to retain for diagnostics */
|
|
183
|
+
const MAX_RECORDED_MATCHES = 5;
|
|
184
|
+
|
|
185
|
+
/** Minimum confidence for a dominant fuzzy match to be auto-selected */
|
|
186
|
+
const DOMINANT_FUZZY_MIN_CONFIDENCE = 0.97;
|
|
187
|
+
|
|
188
|
+
/** Minimum score gap between the best and second-best fuzzy matches */
|
|
189
|
+
const DOMINANT_FUZZY_DELTA = 0.08;
|
|
190
|
+
|
|
191
|
+
interface IndexedMatches {
|
|
192
|
+
firstMatch: number | undefined;
|
|
193
|
+
matchCount: number;
|
|
194
|
+
matchIndices: number[];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface PreviewWindowOptions {
|
|
198
|
+
context: number;
|
|
199
|
+
maxLen: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function collectIndexedMatches(
|
|
203
|
+
start: number,
|
|
204
|
+
endInclusive: number,
|
|
205
|
+
predicate: (index: number) => boolean,
|
|
206
|
+
): IndexedMatches {
|
|
207
|
+
let firstMatch: number | undefined;
|
|
208
|
+
let matchCount = 0;
|
|
209
|
+
const matchIndices: number[] = [];
|
|
210
|
+
|
|
211
|
+
for (let index = start; index <= endInclusive; index++) {
|
|
212
|
+
if (!predicate(index)) continue;
|
|
213
|
+
if (firstMatch === undefined) {
|
|
214
|
+
firstMatch = index;
|
|
215
|
+
}
|
|
216
|
+
matchCount++;
|
|
217
|
+
if (matchIndices.length < MAX_RECORDED_MATCHES) {
|
|
218
|
+
matchIndices.push(index);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { firstMatch, matchCount, matchIndices };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function toSingleMatchResult<TStrategy extends SequenceMatchStrategy | ContextMatchStrategy>(
|
|
226
|
+
matches: IndexedMatches,
|
|
227
|
+
confidence: number,
|
|
228
|
+
strategy: TStrategy,
|
|
229
|
+
): { index: number; confidence: number; strategy: TStrategy } | undefined {
|
|
230
|
+
if (matches.firstMatch === undefined) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
index: matches.firstMatch,
|
|
235
|
+
confidence,
|
|
236
|
+
strategy,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function toAmbiguousMatchResult<TStrategy extends SequenceMatchStrategy | ContextMatchStrategy>(
|
|
241
|
+
matches: IndexedMatches,
|
|
242
|
+
confidence: number,
|
|
243
|
+
strategy: TStrategy,
|
|
244
|
+
): { index: number; confidence: number; matchCount: number; matchIndices: number[]; strategy: TStrategy } | undefined {
|
|
245
|
+
if (matches.firstMatch === undefined) {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
index: matches.firstMatch,
|
|
250
|
+
confidence,
|
|
251
|
+
matchCount: matches.matchCount,
|
|
252
|
+
matchIndices: matches.matchIndices,
|
|
253
|
+
strategy,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function formatPreviewWindow(lines: string[], centerIndex: number, options: PreviewWindowOptions): string {
|
|
258
|
+
const start = Math.max(0, centerIndex - options.context);
|
|
259
|
+
const end = Math.min(lines.length, centerIndex + options.context + 1);
|
|
260
|
+
return lines
|
|
261
|
+
.slice(start, end)
|
|
262
|
+
.map((line, index) => {
|
|
263
|
+
const num = start + index + 1;
|
|
264
|
+
const truncated = line.length > options.maxLen ? `${line.slice(0, options.maxLen - 1)}…` : line;
|
|
265
|
+
return ` ${num} | ${truncated}`;
|
|
266
|
+
})
|
|
267
|
+
.join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function findExactMatchOutcome(content: string, target: string): MatchOutcome | undefined {
|
|
271
|
+
const exactIndex = content.indexOf(target);
|
|
272
|
+
if (exactIndex === -1) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const occurrences = content.split(target).length - 1;
|
|
277
|
+
if (occurrences > 1) {
|
|
278
|
+
const contentLines = content.split("\n");
|
|
279
|
+
const occurrenceLines: number[] = [];
|
|
280
|
+
const occurrencePreviews: string[] = [];
|
|
281
|
+
let searchStart = 0;
|
|
282
|
+
|
|
283
|
+
for (let i = 0; i < MAX_RECORDED_MATCHES; i++) {
|
|
284
|
+
const idx = content.indexOf(target, searchStart);
|
|
285
|
+
if (idx === -1) break;
|
|
286
|
+
const lineNumber = content.slice(0, idx).split("\n").length;
|
|
287
|
+
occurrenceLines.push(lineNumber);
|
|
288
|
+
occurrencePreviews.push(
|
|
289
|
+
formatPreviewWindow(contentLines, lineNumber - 1, {
|
|
290
|
+
context: OCCURRENCE_PREVIEW_CONTEXT,
|
|
291
|
+
maxLen: OCCURRENCE_PREVIEW_MAX_LEN,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
searchStart = idx + 1;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return { occurrences, occurrenceLines, occurrencePreviews };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const startLine = content.slice(0, exactIndex).split("\n").length;
|
|
301
|
+
return {
|
|
302
|
+
match: {
|
|
303
|
+
actualText: target,
|
|
304
|
+
startIndex: exactIndex,
|
|
305
|
+
startLine,
|
|
306
|
+
confidence: 1,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
38
311
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
312
|
// Core Algorithms
|
|
40
313
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -220,44 +493,9 @@ export function findMatch(
|
|
|
220
493
|
return {};
|
|
221
494
|
}
|
|
222
495
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const occurrences = content.split(target).length - 1;
|
|
227
|
-
if (occurrences > 1) {
|
|
228
|
-
// Find line numbers and previews for each occurrence (up to 5)
|
|
229
|
-
const contentLines = content.split("\n");
|
|
230
|
-
const occurrenceLines: number[] = [];
|
|
231
|
-
const occurrencePreviews: string[] = [];
|
|
232
|
-
let searchStart = 0;
|
|
233
|
-
for (let i = 0; i < 5; i++) {
|
|
234
|
-
const idx = content.indexOf(target, searchStart);
|
|
235
|
-
if (idx === -1) break;
|
|
236
|
-
const lineNumber = content.slice(0, idx).split("\n").length;
|
|
237
|
-
occurrenceLines.push(lineNumber);
|
|
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);
|
|
241
|
-
const preview = previewLines
|
|
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 - 1)}…` : line}`;
|
|
245
|
-
})
|
|
246
|
-
.join("\n");
|
|
247
|
-
occurrencePreviews.push(preview);
|
|
248
|
-
searchStart = idx + 1;
|
|
249
|
-
}
|
|
250
|
-
return { occurrences, occurrenceLines, occurrencePreviews };
|
|
251
|
-
}
|
|
252
|
-
const startLine = content.slice(0, exactIndex).split("\n").length;
|
|
253
|
-
return {
|
|
254
|
-
match: {
|
|
255
|
-
actualText: target,
|
|
256
|
-
startIndex: exactIndex,
|
|
257
|
-
startLine,
|
|
258
|
-
confidence: 1,
|
|
259
|
-
},
|
|
260
|
-
};
|
|
496
|
+
const exactMatch = findExactMatchOutcome(content, target);
|
|
497
|
+
if (exactMatch) {
|
|
498
|
+
return exactMatch;
|
|
261
499
|
}
|
|
262
500
|
|
|
263
501
|
// Try fuzzy match
|
|
@@ -272,12 +510,10 @@ export function findMatch(
|
|
|
272
510
|
if (aboveThresholdCount === 1) {
|
|
273
511
|
return { match: best, closest: best };
|
|
274
512
|
}
|
|
275
|
-
const dominantDelta = 0.08;
|
|
276
|
-
const dominantMin = 0.97;
|
|
277
513
|
if (
|
|
278
514
|
aboveThresholdCount > 1 &&
|
|
279
|
-
best.confidence >=
|
|
280
|
-
best.confidence - secondBestScore >=
|
|
515
|
+
best.confidence >= DOMINANT_FUZZY_MIN_CONFIDENCE &&
|
|
516
|
+
best.confidence - secondBestScore >= DOMINANT_FUZZY_DELTA
|
|
281
517
|
) {
|
|
282
518
|
return { match: best, closest: best, fuzzyMatches: aboveThresholdCount, dominantFuzzy: true };
|
|
283
519
|
}
|
|
@@ -389,38 +625,31 @@ export function seekSequence(
|
|
|
389
625
|
const maxStart = lines.length - pattern.length;
|
|
390
626
|
|
|
391
627
|
const runExactPasses = (from: number, to: number): SequenceSearchResult | undefined => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Pass 3b: Comment-prefix normalized match
|
|
414
|
-
for (let i = from; i <= to; i++) {
|
|
415
|
-
if (matchesAt(lines, pattern, i, (a, b) => stripCommentPrefix(a) === stripCommentPrefix(b))) {
|
|
416
|
-
return { index: i, confidence: 0.975, strategy: "comment-prefix" };
|
|
417
|
-
}
|
|
418
|
-
}
|
|
628
|
+
const comparisonPasses: Array<{
|
|
629
|
+
compare: (a: string, b: string) => boolean;
|
|
630
|
+
confidence: number;
|
|
631
|
+
strategy: SequenceMatchStrategy;
|
|
632
|
+
}> = [
|
|
633
|
+
{ compare: (a, b) => a === b, confidence: 1.0, strategy: "exact" },
|
|
634
|
+
{ compare: (a, b) => a.trimEnd() === b.trimEnd(), confidence: 0.99, strategy: "trim-trailing" },
|
|
635
|
+
{ compare: (a, b) => a.trim() === b.trim(), confidence: 0.98, strategy: "trim" },
|
|
636
|
+
{
|
|
637
|
+
compare: (a, b) => stripCommentPrefix(a) === stripCommentPrefix(b),
|
|
638
|
+
confidence: 0.975,
|
|
639
|
+
strategy: "comment-prefix",
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
compare: (a, b) => normalizeUnicode(a) === normalizeUnicode(b),
|
|
643
|
+
confidence: 0.97,
|
|
644
|
+
strategy: "unicode",
|
|
645
|
+
},
|
|
646
|
+
];
|
|
419
647
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
648
|
+
for (const pass of comparisonPasses) {
|
|
649
|
+
const matches = collectIndexedMatches(from, to, i => matchesAt(lines, pattern, i, pass.compare));
|
|
650
|
+
const result = toSingleMatchResult(matches, pass.confidence, pass.strategy);
|
|
651
|
+
if (result) {
|
|
652
|
+
return result;
|
|
424
653
|
}
|
|
425
654
|
}
|
|
426
655
|
|
|
@@ -428,37 +657,20 @@ export function seekSequence(
|
|
|
428
657
|
return undefined;
|
|
429
658
|
}
|
|
430
659
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (
|
|
444
|
-
return
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Pass 6: Partial line substring match (track all matches for ambiguity detection)
|
|
449
|
-
{
|
|
450
|
-
let firstMatch: number | undefined;
|
|
451
|
-
let matchCount = 0;
|
|
452
|
-
const matchIndices: number[] = [];
|
|
453
|
-
for (let i = from; i <= to; i++) {
|
|
454
|
-
if (matchesAt(lines, pattern, i, lineIncludesPattern)) {
|
|
455
|
-
if (firstMatch === undefined) firstMatch = i;
|
|
456
|
-
matchCount++;
|
|
457
|
-
if (matchIndices.length < 5) matchIndices.push(i);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
if (matchCount > 0) {
|
|
461
|
-
return { index: firstMatch, confidence: 0.94, matchCount, matchIndices, strategy: "substring" };
|
|
660
|
+
const partialPasses: Array<{
|
|
661
|
+
compare: (line: string, patternLine: string) => boolean;
|
|
662
|
+
confidence: number;
|
|
663
|
+
strategy: SequenceMatchStrategy;
|
|
664
|
+
}> = [
|
|
665
|
+
{ compare: lineStartsWithPattern, confidence: 0.965, strategy: "prefix" },
|
|
666
|
+
{ compare: lineIncludesPattern, confidence: 0.94, strategy: "substring" },
|
|
667
|
+
];
|
|
668
|
+
|
|
669
|
+
for (const pass of partialPasses) {
|
|
670
|
+
const matches = collectIndexedMatches(from, to, i => matchesAt(lines, pattern, i, pass.compare));
|
|
671
|
+
const result = toAmbiguousMatchResult(matches, pass.confidence, pass.strategy);
|
|
672
|
+
if (result) {
|
|
673
|
+
return result;
|
|
462
674
|
}
|
|
463
675
|
}
|
|
464
676
|
|
|
@@ -482,34 +694,26 @@ export function seekSequence(
|
|
|
482
694
|
}
|
|
483
695
|
|
|
484
696
|
// Pass 7: Fuzzy matching - find best match above threshold
|
|
485
|
-
let bestIndex: number | undefined;
|
|
486
697
|
let bestScore = 0;
|
|
487
698
|
let secondBestScore = 0;
|
|
488
|
-
let
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
matchCount++;
|
|
495
|
-
if (matchIndices.length < 5) matchIndices.push(i);
|
|
496
|
-
}
|
|
497
|
-
if (score > bestScore) {
|
|
498
|
-
secondBestScore = bestScore;
|
|
499
|
-
bestScore = score;
|
|
500
|
-
bestIndex = i;
|
|
501
|
-
} else if (score > secondBestScore) {
|
|
502
|
-
secondBestScore = score;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
699
|
+
let bestIndex: number | undefined;
|
|
700
|
+
const fuzzyMatches: IndexedMatches = {
|
|
701
|
+
firstMatch: undefined,
|
|
702
|
+
matchCount: 0,
|
|
703
|
+
matchIndices: [],
|
|
704
|
+
};
|
|
505
705
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
for (let i = start; i < searchStart; i++) {
|
|
706
|
+
const scoreFuzzyRange = (from: number, to: number): void => {
|
|
707
|
+
for (let i = from; i <= to; i++) {
|
|
509
708
|
const score = fuzzyScoreAt(lines, pattern, i);
|
|
510
709
|
if (score >= SEQUENCE_FUZZY_THRESHOLD) {
|
|
511
|
-
|
|
512
|
-
|
|
710
|
+
if (fuzzyMatches.firstMatch === undefined) {
|
|
711
|
+
fuzzyMatches.firstMatch = i;
|
|
712
|
+
}
|
|
713
|
+
fuzzyMatches.matchCount++;
|
|
714
|
+
if (fuzzyMatches.matchIndices.length < MAX_RECORDED_MATCHES) {
|
|
715
|
+
fuzzyMatches.matchIndices.push(i);
|
|
716
|
+
}
|
|
513
717
|
}
|
|
514
718
|
if (score > bestScore) {
|
|
515
719
|
secondBestScore = bestScore;
|
|
@@ -519,21 +723,36 @@ export function seekSequence(
|
|
|
519
723
|
secondBestScore = score;
|
|
520
724
|
}
|
|
521
725
|
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
scoreFuzzyRange(searchStart, maxStart);
|
|
729
|
+
|
|
730
|
+
// Also search from start if eof mode started from end
|
|
731
|
+
if (eof && searchStart > start) {
|
|
732
|
+
scoreFuzzyRange(start, searchStart - 1);
|
|
522
733
|
}
|
|
523
734
|
|
|
524
735
|
if (bestIndex !== undefined && bestScore >= SEQUENCE_FUZZY_THRESHOLD) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
736
|
+
if (
|
|
737
|
+
fuzzyMatches.matchCount > 1 &&
|
|
738
|
+
bestScore >= DOMINANT_FUZZY_MIN_CONFIDENCE &&
|
|
739
|
+
bestScore - secondBestScore >= DOMINANT_FUZZY_DELTA
|
|
740
|
+
) {
|
|
528
741
|
return {
|
|
529
742
|
index: bestIndex,
|
|
530
743
|
confidence: bestScore,
|
|
531
744
|
matchCount: 1,
|
|
532
|
-
matchIndices,
|
|
745
|
+
matchIndices: fuzzyMatches.matchIndices,
|
|
533
746
|
strategy: "fuzzy-dominant",
|
|
534
747
|
};
|
|
535
748
|
}
|
|
536
|
-
return {
|
|
749
|
+
return {
|
|
750
|
+
index: bestIndex,
|
|
751
|
+
confidence: bestScore,
|
|
752
|
+
matchCount: fuzzyMatches.matchCount,
|
|
753
|
+
matchIndices: fuzzyMatches.matchIndices,
|
|
754
|
+
strategy: "fuzzy",
|
|
755
|
+
};
|
|
537
756
|
}
|
|
538
757
|
|
|
539
758
|
// Pass 8: Character-based fuzzy matching via findMatch
|
|
@@ -620,56 +839,34 @@ export function findContextLine(
|
|
|
620
839
|
const allowFuzzy = options?.allowFuzzy ?? true;
|
|
621
840
|
const trimmedContext = context.trim();
|
|
622
841
|
|
|
623
|
-
|
|
624
|
-
{
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Pass 2: Trimmed match
|
|
641
|
-
{
|
|
642
|
-
let firstMatch: number | undefined;
|
|
643
|
-
let matchCount = 0;
|
|
644
|
-
const matchIndices: number[] = [];
|
|
645
|
-
for (let i = startFrom; i < lines.length; i++) {
|
|
646
|
-
if (lines[i].trim() === trimmedContext) {
|
|
647
|
-
if (firstMatch === undefined) firstMatch = i;
|
|
648
|
-
matchCount++;
|
|
649
|
-
if (matchIndices.length < 5) matchIndices.push(i);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
if (matchCount > 0) {
|
|
653
|
-
return { index: firstMatch, confidence: 0.99, matchCount, matchIndices, strategy: "trim" };
|
|
842
|
+
const endIndex = lines.length - 1;
|
|
843
|
+
const exactPasses: Array<{
|
|
844
|
+
confidence: number;
|
|
845
|
+
strategy: ContextMatchStrategy;
|
|
846
|
+
predicate: (index: number) => boolean;
|
|
847
|
+
}> = [
|
|
848
|
+
{ confidence: 1.0, strategy: "exact", predicate: i => lines[i] === context },
|
|
849
|
+
{ confidence: 0.99, strategy: "trim", predicate: i => lines[i].trim() === trimmedContext },
|
|
850
|
+
];
|
|
851
|
+
|
|
852
|
+
for (const pass of exactPasses) {
|
|
853
|
+
const matches = collectIndexedMatches(startFrom, endIndex, pass.predicate);
|
|
854
|
+
const result = toAmbiguousMatchResult(matches, pass.confidence, pass.strategy);
|
|
855
|
+
if (result) {
|
|
856
|
+
return result;
|
|
654
857
|
}
|
|
655
858
|
}
|
|
656
859
|
|
|
657
860
|
// Pass 3: Unicode normalization match
|
|
658
861
|
const normalizedContext = normalizeUnicode(context);
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
if (matchIndices.length < 5) matchIndices.push(i);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
if (matchCount > 0) {
|
|
671
|
-
return { index: firstMatch, confidence: 0.98, matchCount, matchIndices, strategy: "unicode" };
|
|
672
|
-
}
|
|
862
|
+
const unicodeMatches = collectIndexedMatches(
|
|
863
|
+
startFrom,
|
|
864
|
+
endIndex,
|
|
865
|
+
i => normalizeUnicode(lines[i]) === normalizedContext,
|
|
866
|
+
);
|
|
867
|
+
const unicodeResult = toAmbiguousMatchResult(unicodeMatches, 0.98, "unicode");
|
|
868
|
+
if (unicodeResult) {
|
|
869
|
+
return unicodeResult;
|
|
673
870
|
}
|
|
674
871
|
|
|
675
872
|
if (!allowFuzzy) {
|
|
@@ -679,19 +876,12 @@ export function findContextLine(
|
|
|
679
876
|
// Pass 4: Prefix match (file line starts with context)
|
|
680
877
|
const contextNorm = normalizeForFuzzy(context);
|
|
681
878
|
if (contextNorm.length > 0) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (firstMatch === undefined) firstMatch = i;
|
|
689
|
-
matchCount++;
|
|
690
|
-
if (matchIndices.length < 5) matchIndices.push(i);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
if (matchCount > 0) {
|
|
694
|
-
return { index: firstMatch, confidence: 0.96, matchCount, matchIndices, strategy: "prefix" };
|
|
879
|
+
const prefixMatches = collectIndexedMatches(startFrom, endIndex, i =>
|
|
880
|
+
normalizeForFuzzy(lines[i]).startsWith(contextNorm),
|
|
881
|
+
);
|
|
882
|
+
const prefixResult = toAmbiguousMatchResult(prefixMatches, 0.96, "prefix");
|
|
883
|
+
if (prefixResult) {
|
|
884
|
+
return prefixResult;
|
|
695
885
|
}
|
|
696
886
|
}
|
|
697
887
|
|
|
@@ -750,15 +940,23 @@ export function findContextLine(
|
|
|
750
940
|
// Pass 6: Fuzzy match using similarity
|
|
751
941
|
let bestIndex: number | undefined;
|
|
752
942
|
let bestScore = 0;
|
|
753
|
-
|
|
754
|
-
|
|
943
|
+
const fuzzyMatches: IndexedMatches = {
|
|
944
|
+
firstMatch: undefined,
|
|
945
|
+
matchCount: 0,
|
|
946
|
+
matchIndices: [],
|
|
947
|
+
};
|
|
755
948
|
|
|
756
949
|
for (let i = startFrom; i < lines.length; i++) {
|
|
757
950
|
const lineNorm = normalizeForFuzzy(lines[i]);
|
|
758
951
|
const score = similarity(lineNorm, contextNorm);
|
|
759
952
|
if (score >= CONTEXT_FUZZY_THRESHOLD) {
|
|
760
|
-
|
|
761
|
-
|
|
953
|
+
if (fuzzyMatches.firstMatch === undefined) {
|
|
954
|
+
fuzzyMatches.firstMatch = i;
|
|
955
|
+
}
|
|
956
|
+
fuzzyMatches.matchCount++;
|
|
957
|
+
if (fuzzyMatches.matchIndices.length < MAX_RECORDED_MATCHES) {
|
|
958
|
+
fuzzyMatches.matchIndices.push(i);
|
|
959
|
+
}
|
|
762
960
|
}
|
|
763
961
|
if (score > bestScore) {
|
|
764
962
|
bestScore = score;
|
|
@@ -767,7 +965,13 @@ export function findContextLine(
|
|
|
767
965
|
}
|
|
768
966
|
|
|
769
967
|
if (bestIndex !== undefined && bestScore >= CONTEXT_FUZZY_THRESHOLD) {
|
|
770
|
-
return {
|
|
968
|
+
return {
|
|
969
|
+
index: bestIndex,
|
|
970
|
+
confidence: bestScore,
|
|
971
|
+
matchCount: fuzzyMatches.matchCount,
|
|
972
|
+
matchIndices: fuzzyMatches.matchIndices,
|
|
973
|
+
strategy: "fuzzy",
|
|
974
|
+
};
|
|
771
975
|
}
|
|
772
976
|
|
|
773
977
|
if (!options?.skipFunctionFallback && trimmedContext.endsWith("()")) {
|
|
@@ -782,3 +986,121 @@ export function findContextLine(
|
|
|
782
986
|
|
|
783
987
|
return { index: undefined, confidence: bestScore };
|
|
784
988
|
}
|
|
989
|
+
|
|
990
|
+
export const replaceEditSchema = Type.Object({
|
|
991
|
+
path: Type.String({ description: "File path (relative or absolute)" }),
|
|
992
|
+
old_text: Type.String({ description: "Text to find (fuzzy whitespace matching enabled)" }),
|
|
993
|
+
new_text: Type.String({ description: "Replacement text" }),
|
|
994
|
+
all: Type.Optional(Type.Boolean({ description: "Replace all occurrences (default: unique match required)" })),
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
export type ReplaceParams = Static<typeof replaceEditSchema>;
|
|
998
|
+
|
|
999
|
+
interface ExecuteReplaceModeOptions {
|
|
1000
|
+
session: ToolSession;
|
|
1001
|
+
params: ReplaceParams;
|
|
1002
|
+
signal?: AbortSignal;
|
|
1003
|
+
batchRequest?: LspBatchRequest;
|
|
1004
|
+
allowFuzzy: boolean;
|
|
1005
|
+
fuzzyThreshold: number;
|
|
1006
|
+
writethrough: WritethroughCallback;
|
|
1007
|
+
beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
export function isReplaceParams(params: unknown): params is ReplaceParams {
|
|
1011
|
+
return typeof params === "object" && params !== null && "old_text" in params && "new_text" in params;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export async function executeReplaceMode(
|
|
1015
|
+
options: ExecuteReplaceModeOptions,
|
|
1016
|
+
): Promise<AgentToolResult<EditToolDetails, typeof replaceEditSchema>> {
|
|
1017
|
+
const {
|
|
1018
|
+
session,
|
|
1019
|
+
params,
|
|
1020
|
+
signal,
|
|
1021
|
+
batchRequest,
|
|
1022
|
+
allowFuzzy,
|
|
1023
|
+
fuzzyThreshold,
|
|
1024
|
+
writethrough,
|
|
1025
|
+
beginDeferredDiagnosticsForPath,
|
|
1026
|
+
} = options;
|
|
1027
|
+
const { path, old_text, new_text, all } = params;
|
|
1028
|
+
|
|
1029
|
+
enforcePlanModeWrite(session, path);
|
|
1030
|
+
|
|
1031
|
+
if (path.endsWith(".ipynb")) {
|
|
1032
|
+
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (old_text.length === 0) {
|
|
1036
|
+
throw new Error("old_text must not be empty.");
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const absolutePath = resolvePlanPath(session, path);
|
|
1040
|
+
const rawContent = await readReplaceFileContent(absolutePath, path);
|
|
1041
|
+
const { bom, text: content } = stripBom(rawContent);
|
|
1042
|
+
const originalEnding = detectLineEnding(content);
|
|
1043
|
+
const normalizedContent = normalizeToLF(content);
|
|
1044
|
+
const normalizedOldText = normalizeToLF(old_text);
|
|
1045
|
+
const normalizedNewText = normalizeToLF(new_text);
|
|
1046
|
+
|
|
1047
|
+
const result = replaceText(normalizedContent, normalizedOldText, normalizedNewText, {
|
|
1048
|
+
fuzzy: allowFuzzy,
|
|
1049
|
+
all: all ?? false,
|
|
1050
|
+
threshold: fuzzyThreshold,
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
if (result.count === 0) {
|
|
1054
|
+
const matchOutcome = findMatch(normalizedContent, normalizedOldText, {
|
|
1055
|
+
allowFuzzy,
|
|
1056
|
+
threshold: fuzzyThreshold,
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
1060
|
+
throw new Error(formatOccurrenceError(path, matchOutcome));
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
throw new EditMatchError(path, normalizedOldText, matchOutcome.closest, {
|
|
1064
|
+
allowFuzzy,
|
|
1065
|
+
threshold: fuzzyThreshold,
|
|
1066
|
+
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (normalizedContent === result.content) {
|
|
1071
|
+
throw new Error(
|
|
1072
|
+
`No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const finalContent = bom + restoreLineEndings(result.content, originalEnding);
|
|
1077
|
+
const diagnostics = await writethrough(
|
|
1078
|
+
absolutePath,
|
|
1079
|
+
finalContent,
|
|
1080
|
+
signal,
|
|
1081
|
+
Bun.file(absolutePath),
|
|
1082
|
+
batchRequest,
|
|
1083
|
+
dst => (dst === absolutePath ? beginDeferredDiagnosticsForPath(absolutePath) : undefined),
|
|
1084
|
+
);
|
|
1085
|
+
invalidateFsScanAfterWrite(absolutePath);
|
|
1086
|
+
|
|
1087
|
+
const diffResult = generateDiffString(normalizedContent, result.content);
|
|
1088
|
+
const resultText =
|
|
1089
|
+
result.count > 1
|
|
1090
|
+
? `Successfully replaced ${result.count} occurrences in ${path}.`
|
|
1091
|
+
: `Successfully replaced text in ${path}.`;
|
|
1092
|
+
|
|
1093
|
+
const meta = outputMeta()
|
|
1094
|
+
.diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
|
|
1095
|
+
.get();
|
|
1096
|
+
|
|
1097
|
+
return {
|
|
1098
|
+
content: [{ type: "text", text: resultText }],
|
|
1099
|
+
details: {
|
|
1100
|
+
diff: diffResult.diff,
|
|
1101
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
1102
|
+
diagnostics,
|
|
1103
|
+
meta,
|
|
1104
|
+
},
|
|
1105
|
+
};
|
|
1106
|
+
}
|