@oh-my-pi/pi-coding-agent 3.6.1337 → 3.8.1337
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 +27 -0
- package/package.json +4 -4
- package/src/core/bash-executor.ts +115 -154
- package/src/core/index.ts +2 -0
- package/src/core/session-manager.ts +16 -6
- package/src/core/tools/edit-diff.ts +45 -33
- package/src/core/tools/edit.ts +70 -182
- package/src/core/tools/find.ts +141 -160
- package/src/core/tools/index.ts +10 -9
- package/src/core/tools/ls.ts +64 -82
- package/src/core/tools/lsp/client.ts +63 -0
- package/src/core/tools/lsp/edits.ts +13 -4
- package/src/core/tools/lsp/index.ts +191 -85
- package/src/core/tools/notebook.ts +89 -144
- package/src/core/tools/read.ts +110 -158
- package/src/core/tools/write.ts +22 -115
- package/src/core/utils.ts +187 -0
- package/src/modes/interactive/components/tool-execution.ts +14 -14
- package/src/modes/interactive/interactive-mode.ts +23 -54
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.8.1337] - 2026-01-04
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added automatic browser opening after exporting session to HTML
|
|
9
|
+
- Added automatic browser opening after sharing session as a Gist
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed session titles not persisting to file when set before first flush
|
|
14
|
+
|
|
15
|
+
## [3.7.1337] - 2026-01-04
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Added `EditMatchError` class for structured error handling in edit operations
|
|
19
|
+
- Added `utils` module export with `once` and `untilAborted` helper functions
|
|
20
|
+
- Added in-memory LSP content sync via `syncContent` and `notifySaved` client methods
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Refactored LSP integration to use writethrough callbacks for edit and write tools, improving performance by syncing content in-memory before disk writes
|
|
25
|
+
- Simplified FileDiagnosticsResult interface with renamed fields: `diagnostics` → `messages`, `hasErrors` → `errored`, `serverName` → `server`
|
|
26
|
+
- Session title generation now triggers before sending the first message rather than after agent work begins
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Fixed potential text decoding issues in bash executor by using streaming TextDecoder instead of Buffer.toString()
|
|
31
|
+
|
|
5
32
|
## [3.6.1337] - 2026-01-03
|
|
6
33
|
|
|
7
34
|
## [3.5.1337] - 2026-01-03
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.1337",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
43
|
-
"@oh-my-pi/pi-ai": "3.
|
|
44
|
-
"@oh-my-pi/pi-tui": "3.
|
|
42
|
+
"@oh-my-pi/pi-agent-core": "3.8.1337",
|
|
43
|
+
"@oh-my-pi/pi-ai": "3.8.1337",
|
|
44
|
+
"@oh-my-pi/pi-tui": "3.8.1337",
|
|
45
45
|
"@sinclair/typebox": "^0.34.46",
|
|
46
46
|
"ajv": "^8.17.1",
|
|
47
47
|
"chalk": "^5.5.0",
|
|
@@ -14,6 +14,7 @@ import stripAnsi from "strip-ansi";
|
|
|
14
14
|
import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../utils/shell";
|
|
15
15
|
import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
|
|
16
16
|
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate";
|
|
17
|
+
import { ScopeSignal } from "./utils";
|
|
17
18
|
|
|
18
19
|
// ============================================================================
|
|
19
20
|
// Types
|
|
@@ -47,6 +48,70 @@ export interface BashResult {
|
|
|
47
48
|
// Implementation
|
|
48
49
|
// ============================================================================
|
|
49
50
|
|
|
51
|
+
function createSanitizer(): TransformStream<Uint8Array, string> {
|
|
52
|
+
const decoder = new TextDecoder();
|
|
53
|
+
return new TransformStream({
|
|
54
|
+
transform(chunk, controller) {
|
|
55
|
+
const text = sanitizeBinaryOutput(stripAnsi(decoder.decode(chunk, { stream: true }))).replace(/\r/g, "");
|
|
56
|
+
controller.enqueue(text);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createOutputSink(
|
|
62
|
+
spillThreshold: number,
|
|
63
|
+
maxBuffer: number,
|
|
64
|
+
onChunk?: (text: string) => void,
|
|
65
|
+
): WritableStream<string> & {
|
|
66
|
+
dump: (annotation?: string) => { output: string; truncated: boolean; fullOutputPath?: string };
|
|
67
|
+
} {
|
|
68
|
+
const chunks: string[] = [];
|
|
69
|
+
let chunkBytes = 0;
|
|
70
|
+
let totalBytes = 0;
|
|
71
|
+
let fullOutputPath: string | undefined;
|
|
72
|
+
let fullOutputStream: WriteStream | undefined;
|
|
73
|
+
|
|
74
|
+
const sink = new WritableStream<string>({
|
|
75
|
+
write(text) {
|
|
76
|
+
totalBytes += text.length;
|
|
77
|
+
|
|
78
|
+
// Spill to temp file if needed
|
|
79
|
+
if (totalBytes > spillThreshold && !fullOutputPath) {
|
|
80
|
+
fullOutputPath = join(tmpdir(), `omp-${crypto.randomUUID()}.buffer`);
|
|
81
|
+
const ts = createWriteStream(fullOutputPath);
|
|
82
|
+
chunks.forEach((c) => {
|
|
83
|
+
ts.write(c);
|
|
84
|
+
});
|
|
85
|
+
fullOutputStream = ts;
|
|
86
|
+
}
|
|
87
|
+
fullOutputStream?.write(text);
|
|
88
|
+
|
|
89
|
+
// Rolling buffer
|
|
90
|
+
chunks.push(text);
|
|
91
|
+
chunkBytes += text.length;
|
|
92
|
+
while (chunkBytes > maxBuffer && chunks.length > 1) {
|
|
93
|
+
chunkBytes -= chunks.shift()!.length;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onChunk?.(text);
|
|
97
|
+
},
|
|
98
|
+
close() {
|
|
99
|
+
fullOutputStream?.end();
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return Object.assign(sink, {
|
|
104
|
+
dump(annotation?: string) {
|
|
105
|
+
if (annotation) {
|
|
106
|
+
chunks.push(`\n\n${annotation}`);
|
|
107
|
+
}
|
|
108
|
+
const full = chunks.join("");
|
|
109
|
+
const { content, truncated } = truncateTail(full);
|
|
110
|
+
return { output: truncated ? content : full, truncated, fullOutputPath: fullOutputPath };
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
50
115
|
/**
|
|
51
116
|
* Execute a bash command with optional streaming and cancellation support.
|
|
52
117
|
*
|
|
@@ -72,165 +137,61 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
72
137
|
const prefixedCommand = prefix ? `${prefix} ${command}` : command;
|
|
73
138
|
const finalCommand = `${snapshotPrefix}${prefixedCommand}`;
|
|
74
139
|
|
|
75
|
-
|
|
76
|
-
const child: Subprocess = Bun.spawn([shell, ...args, finalCommand], {
|
|
77
|
-
cwd: options?.cwd,
|
|
78
|
-
stdin: "ignore",
|
|
79
|
-
stdout: "pipe",
|
|
80
|
-
stderr: "pipe",
|
|
81
|
-
env,
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Track sanitized output for truncation
|
|
85
|
-
const outputChunks: string[] = [];
|
|
86
|
-
let outputBytes = 0;
|
|
87
|
-
const maxOutputBytes = DEFAULT_MAX_BYTES * 2;
|
|
88
|
-
|
|
89
|
-
// Temp file for large output
|
|
90
|
-
let tempFilePath: string | undefined;
|
|
91
|
-
let tempFileStream: WriteStream | undefined;
|
|
92
|
-
let totalBytes = 0;
|
|
93
|
-
let timedOut = false;
|
|
94
|
-
|
|
95
|
-
// Handle abort signal and timeout
|
|
96
|
-
const abortHandler = () => {
|
|
97
|
-
killProcessTree(child.pid);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// Set up timeout if specified
|
|
101
|
-
let timeoutHandle: Timer | undefined;
|
|
102
|
-
if (options?.timeout && options.timeout > 0) {
|
|
103
|
-
timeoutHandle = setTimeout(() => {
|
|
104
|
-
timedOut = true;
|
|
105
|
-
abortHandler();
|
|
106
|
-
}, options.timeout);
|
|
107
|
-
}
|
|
140
|
+
using signal = new ScopeSignal(options);
|
|
108
141
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
exitCode: undefined,
|
|
117
|
-
cancelled: true,
|
|
118
|
-
truncated: false,
|
|
119
|
-
});
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const handleData = (data: Buffer) => {
|
|
126
|
-
totalBytes += data.length;
|
|
127
|
-
|
|
128
|
-
// Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines
|
|
129
|
-
const text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\r/g, "");
|
|
130
|
-
|
|
131
|
-
// Start writing to temp file if exceeds threshold
|
|
132
|
-
if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
|
|
133
|
-
const randomId = crypto.getRandomValues(new Uint8Array(8));
|
|
134
|
-
const id = Array.from(randomId, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
135
|
-
tempFilePath = join(tmpdir(), `omp-bash-${id}.log`);
|
|
136
|
-
tempFileStream = createWriteStream(tempFilePath);
|
|
137
|
-
// Write already-buffered chunks to temp file
|
|
138
|
-
for (const chunk of outputChunks) {
|
|
139
|
-
tempFileStream.write(chunk);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
+
const child: Subprocess = Bun.spawn([shell, ...args, finalCommand], {
|
|
143
|
+
cwd: options?.cwd,
|
|
144
|
+
stdin: "ignore",
|
|
145
|
+
stdout: "pipe",
|
|
146
|
+
stderr: "pipe",
|
|
147
|
+
env,
|
|
148
|
+
});
|
|
142
149
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
signal.catch(() => {
|
|
151
|
+
killProcessTree(child.pid);
|
|
152
|
+
});
|
|
146
153
|
|
|
147
|
-
|
|
148
|
-
outputChunks.push(text);
|
|
149
|
-
outputBytes += text.length;
|
|
150
|
-
while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
|
|
151
|
-
const removed = outputChunks.shift()!;
|
|
152
|
-
outputBytes -= removed.length;
|
|
153
|
-
}
|
|
154
|
+
const sink = createOutputSink(DEFAULT_MAX_BYTES, DEFAULT_MAX_BYTES * 2, options?.onChunk);
|
|
154
155
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Read streams asynchronously
|
|
162
|
-
(async () => {
|
|
156
|
+
const writer = sink.getWriter();
|
|
157
|
+
try {
|
|
158
|
+
async function pumpStream(readable: ReadableStream<Uint8Array>) {
|
|
159
|
+
const reader = readable.pipeThrough(createSanitizer()).getReader();
|
|
163
160
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
(async () => {
|
|
169
|
-
while (true) {
|
|
170
|
-
const { done, value } = await stdoutReader.read();
|
|
171
|
-
if (done) break;
|
|
172
|
-
handleData(Buffer.from(value));
|
|
173
|
-
}
|
|
174
|
-
})(),
|
|
175
|
-
(async () => {
|
|
176
|
-
while (true) {
|
|
177
|
-
const { done, value } = await stderrReader.read();
|
|
178
|
-
if (done) break;
|
|
179
|
-
handleData(Buffer.from(value));
|
|
180
|
-
}
|
|
181
|
-
})(),
|
|
182
|
-
]);
|
|
183
|
-
|
|
184
|
-
const exitCode = await child.exited;
|
|
185
|
-
|
|
186
|
-
// Clean up
|
|
187
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
188
|
-
if (options?.signal) {
|
|
189
|
-
options.signal.removeEventListener("abort", abortHandler);
|
|
190
|
-
}
|
|
191
|
-
if (tempFileStream) {
|
|
192
|
-
tempFileStream.end();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Combine buffered chunks for truncation (already sanitized)
|
|
196
|
-
const fullOutput = outputChunks.join("");
|
|
197
|
-
const truncationResult = truncateTail(fullOutput);
|
|
198
|
-
|
|
199
|
-
// Handle timeout
|
|
200
|
-
if (timedOut) {
|
|
201
|
-
const timeoutSecs = Math.round((options?.timeout || 0) / 1000);
|
|
202
|
-
resolve({
|
|
203
|
-
output: `${fullOutput}\n\nCommand timed out after ${timeoutSecs} seconds`,
|
|
204
|
-
exitCode: undefined,
|
|
205
|
-
cancelled: true,
|
|
206
|
-
truncated: truncationResult.truncated,
|
|
207
|
-
fullOutputPath: tempFilePath,
|
|
208
|
-
});
|
|
209
|
-
return;
|
|
161
|
+
while (true) {
|
|
162
|
+
const { done, value } = await reader.read();
|
|
163
|
+
if (done) break;
|
|
164
|
+
await writer.write(value);
|
|
210
165
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const cancelled = exitCode === null || (exitCode !== 0 && (options?.signal?.aborted ?? false));
|
|
214
|
-
|
|
215
|
-
resolve({
|
|
216
|
-
output: truncationResult.truncated ? truncationResult.content : fullOutput,
|
|
217
|
-
exitCode: cancelled ? undefined : exitCode,
|
|
218
|
-
cancelled,
|
|
219
|
-
truncated: truncationResult.truncated,
|
|
220
|
-
fullOutputPath: tempFilePath,
|
|
221
|
-
});
|
|
222
|
-
} catch (err) {
|
|
223
|
-
// Clean up
|
|
224
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
225
|
-
if (options?.signal) {
|
|
226
|
-
options.signal.removeEventListener("abort", abortHandler);
|
|
227
|
-
}
|
|
228
|
-
if (tempFileStream) {
|
|
229
|
-
tempFileStream.end();
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
reject(err);
|
|
166
|
+
} finally {
|
|
167
|
+
reader.releaseLock();
|
|
233
168
|
}
|
|
234
|
-
}
|
|
235
|
-
|
|
169
|
+
}
|
|
170
|
+
await Promise.all([
|
|
171
|
+
pumpStream(child.stdout as ReadableStream<Uint8Array>),
|
|
172
|
+
pumpStream(child.stderr as ReadableStream<Uint8Array>),
|
|
173
|
+
]);
|
|
174
|
+
} finally {
|
|
175
|
+
await writer.close();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Non-zero exit codes or signal-killed processes are considered cancelled if killed via signal
|
|
179
|
+
const exitCode = await child.exited;
|
|
180
|
+
|
|
181
|
+
const cancelled = exitCode === null || (exitCode !== 0 && (options?.signal?.aborted ?? false));
|
|
182
|
+
|
|
183
|
+
if (signal.timedOut()) {
|
|
184
|
+
const secs = Math.round(options!.timeout! / 1000);
|
|
185
|
+
return {
|
|
186
|
+
exitCode: undefined,
|
|
187
|
+
cancelled: true,
|
|
188
|
+
...sink.dump(`Command timed out after ${secs} seconds`),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
exitCode: cancelled ? undefined : exitCode,
|
|
194
|
+
cancelled,
|
|
195
|
+
...sink.dump(),
|
|
196
|
+
};
|
|
236
197
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -518,7 +518,10 @@ function getSortedSessions(sessionDir: string): RecentSessionInfo[] {
|
|
|
518
518
|
// Find end of first JSON line
|
|
519
519
|
const eol = sub.indexOf("}\n");
|
|
520
520
|
if (eol <= 0) return null;
|
|
521
|
-
|
|
521
|
+
const header = JSON.parse(sub.toString("utf8", 0, eol + 1));
|
|
522
|
+
// Validate session header
|
|
523
|
+
if (header.type !== "session" || typeof header.id !== "string") return null;
|
|
524
|
+
return header;
|
|
522
525
|
};
|
|
523
526
|
|
|
524
527
|
return readdirSync(sessionDir)
|
|
@@ -705,16 +708,23 @@ export class SessionManager {
|
|
|
705
708
|
|
|
706
709
|
setSessionTitle(title: string): void {
|
|
707
710
|
this.sessionTitle = title;
|
|
708
|
-
|
|
711
|
+
|
|
712
|
+
// Update the in-memory header (so first flush includes title)
|
|
713
|
+
const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
|
|
714
|
+
if (header) {
|
|
715
|
+
header.title = title;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Update the session file header with the title (if already flushed)
|
|
709
719
|
if (this.persist && this.sessionFile && existsSync(this.sessionFile)) {
|
|
710
720
|
try {
|
|
711
721
|
const content = readFileSync(this.sessionFile, "utf-8");
|
|
712
722
|
const lines = content.split("\n");
|
|
713
723
|
if (lines.length > 0) {
|
|
714
|
-
const
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
lines[0] = JSON.stringify(
|
|
724
|
+
const fileHeader = JSON.parse(lines[0]) as SessionHeader;
|
|
725
|
+
if (fileHeader.type === "session") {
|
|
726
|
+
fileHeader.title = title;
|
|
727
|
+
lines[0] = JSON.stringify(fileHeader);
|
|
718
728
|
writeFileSync(this.sessionFile, lines.join("\n"));
|
|
719
729
|
}
|
|
720
730
|
}
|
|
@@ -249,40 +249,52 @@ function findFirstDifferentLine(oldLines: string[], newLines: string[]): { oldLi
|
|
|
249
249
|
return { oldLine: oldLines[0] ?? "", newLine: newLines[0] ?? "" };
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
export
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`;
|
|
252
|
+
export class EditMatchError extends Error {
|
|
253
|
+
constructor(
|
|
254
|
+
public readonly path: string,
|
|
255
|
+
public readonly normalizedOldText: string,
|
|
256
|
+
public readonly closest: EditMatch | undefined,
|
|
257
|
+
public readonly options: { allowFuzzy: boolean; similarityThreshold: number; fuzzyMatches?: number },
|
|
258
|
+
) {
|
|
259
|
+
super(EditMatchError.formatMessage(path, normalizedOldText, closest, options));
|
|
260
|
+
this.name = "EditMatchError";
|
|
262
261
|
}
|
|
263
262
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
263
|
+
static formatMessage(
|
|
264
|
+
path: string,
|
|
265
|
+
normalizedOldText: string,
|
|
266
|
+
closest: EditMatch | undefined,
|
|
267
|
+
options: { allowFuzzy: boolean; similarityThreshold: number; fuzzyMatches?: number },
|
|
268
|
+
): string {
|
|
269
|
+
if (!closest) {
|
|
270
|
+
return options.allowFuzzy
|
|
271
|
+
? `Could not find a close enough match in ${path}.`
|
|
272
|
+
: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const similarity = Math.round(closest.confidence * 100);
|
|
276
|
+
const oldLines = normalizedOldText.split("\n");
|
|
277
|
+
const actualLines = closest.actualText.split("\n");
|
|
278
|
+
const { oldLine, newLine } = findFirstDifferentLine(oldLines, actualLines);
|
|
279
|
+
const thresholdPercent = Math.round(options.similarityThreshold * 100);
|
|
280
|
+
|
|
281
|
+
const hint = options.allowFuzzy
|
|
282
|
+
? options.fuzzyMatches && options.fuzzyMatches > 1
|
|
283
|
+
? `Found ${options.fuzzyMatches} high-confidence matches. Provide more context to make it unique.`
|
|
284
|
+
: `Closest match was below the ${thresholdPercent}% similarity threshold.`
|
|
285
|
+
: "Fuzzy matching is disabled. Enable 'Edit fuzzy match' in settings to accept high-confidence matches.";
|
|
286
|
+
|
|
287
|
+
return [
|
|
288
|
+
options.allowFuzzy
|
|
289
|
+
? `Could not find a close enough match in ${path}.`
|
|
290
|
+
: `Could not find the exact text in ${path}.`,
|
|
291
|
+
``,
|
|
292
|
+
`Closest match (${similarity}% similar) at line ${closest.startLine}:`,
|
|
293
|
+
` - ${oldLine}`,
|
|
294
|
+
` + ${newLine}`,
|
|
295
|
+
hint,
|
|
296
|
+
].join("\n");
|
|
297
|
+
}
|
|
286
298
|
}
|
|
287
299
|
|
|
288
300
|
/**
|
|
@@ -444,7 +456,7 @@ export async function computeEditDiff(
|
|
|
444
456
|
|
|
445
457
|
if (!matchOutcome.match) {
|
|
446
458
|
return {
|
|
447
|
-
error:
|
|
459
|
+
error: EditMatchError.formatMessage(path, normalizedOldText, matchOutcome.closest, {
|
|
448
460
|
allowFuzzy: fuzzy,
|
|
449
461
|
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
450
462
|
fuzzyMatches: matchOutcome.fuzzyMatches,
|