Skip to content

Commit d308423

Browse files
Composer V2: Replace replaceInFile with editFile as the primary targeted-edit tool (#2305)
* feat(editFile): write-first with inline diff in chat Replace the blocking accept/reject modal in editFile with a write-first approach: changes are written to disk immediately, and a unified diff is returned inline in the chat as a fenced \`\`\`diff block. Multiple editFile calls in a single agent turn each produce their own diff without requiring any user interaction. Also cleans up associated rename/refactor of writeToFileTool → writeFileTool across chain runner, streamer, and related tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(editFile): add Show Diff button and side-by-side diff view in ApplyView - Pipe editFileDiffs through message types (ChatMessage, StoredMessage) and MessageRepository so writeFile/editFile diffs are stored as a typed field (same pattern as sources) - Collect diffs in AutonomousAgentChainRunner.runReActLoop() by parsing the JSON result of successful writeFile/editFile tool calls, then pass through ReActLoopResult → handleResponse() → ChatMessage.editFileDiffs - ChatSingleMessage reads message.editFileDiffs directly (instead of trying to parse TOOL_CALL markers which don't exist in agent mode) - ChatButtons shows a "Show diff" button when editFileDiffs is present; clicking it opens ApplyView for each diff - ApplyView redesigned: renders change blocks side-by-side (original left with red highlights, modified right with green highlights) using the SideBySideBlock component, unchanged blocks shown as plain text between change blocks — restores the classic diff block presentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Revert "feat(editFile): add Show Diff button and side-by-side diff view in ApplyView" This reverts commit 8094549. * feat(editFile): show ApplyView with simple mode (no per-block buttons) - editFile now shows the Apply View preview and blocks until user accepts/rejects - Respects autoAcceptEdits setting (bypasses preview when enabled) - Adds `simple` flag to ApplyViewState to hide per-block accept/reject/revert buttons - Removes unused createPatch import Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(tools): update writeFile description to "Create or rewrite files in your vault" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address Codex review — GPT prompt, fuzzy match safety, tool ID migration - modelAdapter: update GPT prompt to use editFile's oldText/newText params instead of the old SEARCH/REPLACE block syntax - ComposerTools: fix fuzzy match to replace only the matched span in the original content; previously the entire file was rebuilt from the fuzzy-normalized string, inadvertently applying trimEnd and quote/dash normalization to text outside the matched span - settings/model: migrate persisted autonomousAgentEnabledToolIds from writeToFile→writeFile and replaceInFile→editFile on upgrade Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(editFile): add unit tests for fuzzy match via applyEditToContent Extract pure replacement logic into applyEditToContent() for testability, then wire editFileTool.func to call it (removes duplicated logic). Tests cover: - Exact match: basic replacement, deletion, NOT_FOUND, AMBIGUOUS - Fuzzy via smart quotes (single, double), en-dash, NBSP - Fuzzy via trailing whitespace (spaces, tabs) - Key invariant: fuzzy match only replaces the matched span — smart quotes and trailing spaces outside the span are left untouched - Multiline fuzzy match with mixed artifacts - CRLF and LF preservation - BOM preservation Also fixes mapFuzzyIndexToNormal to extend matchEnd to the full original line length when the fuzzy match ends at end-of-line, preventing orphaned trailing whitespace after replacement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address Codex review — NFKC position mapping and legacy tag compat - ComposerTools: fix mapFuzzyIndexToNormal to handle NFKC expansion (e.g. Ⅳ → IV) by walking char-by-char within each line instead of assuming a 1:1 column mapping; adds two unit tests covering this case - ChatSingleMessage: normalise legacy <writeToFile> tags to <writeFile> before rendering so saved chat history from before the tool rename continues to display correctly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(modelAdapter): update assertions for editFile oldText/newText prompt Replace stale SEARCH/REPLACE references with the new parameter names. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(editFile): replace sentinel strings with ApplyEditResult discriminated union - `applyEditToContent` now returns `{ ok: true; content: string } | { ok: false; reason: 'NOT_FOUND' } | { ok: false; reason: 'AMBIGUOUS'; occurrences: number }` instead of raw sentinel strings, eliminating false positives when file content matches 'NOT_FOUND' or starts with 'AMBIGUOUS:' - Update `editFileTool` to pattern-match on the discriminated union - Update `cleanMessageForCopy` regexes to also strip legacy `<writeToFile>` tags from saved chat history - Update all `applyEditToContent` test assertions to use the new structured return type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editFile): sanitize file path before vault lookup Apply `sanitizeFilePath` in `editFileTool` to match the same treatment `writeFileTool` already does, preventing "File not found" failures when the path has a basename that exceeds 255 bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editFile): guard against degenerate fuzzy match inside NFKC expansion When `oldText` fuzzy-matches a span whose boundaries both map to the same position in the original (e.g. searching "I" inside fuzzy "IV" derived from "Ⅳ"), `matchStart >= matchEnd` and the replacement would be zero-width or corrupt. Return NOT_FOUND instead of silently producing a wrong edit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(builtinTools): remove invalid daily:append/daily:prepend from prompt The obsidianDailyNote tool schema only accepts 'daily', 'daily:read', and 'daily:path'. The prompt was incorrectly advertising non-existent daily:append and daily:prepend commands, which would cause LLM-generated tool calls to fail schema validation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editFile): add Stage 3 to tolerate trailing newline mismatch at EOF LLMs frequently append \n to the last line of oldText even when the file has no final newline. Neither exact nor fuzzy match would find this, causing avoidable NOT_FOUND failures. Stage 3 retries with one trailing \n stripped from oldText (and mirrors the trim on newText to preserve the file's no-trailing-newline format). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tests): update editFile integration test to oldText/newText shape The argumentValidator was still asserting the legacy args.diff SEARCH/REPLACE payload. Updated to check args.oldText and args.newText to match the new editFile tool contract. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editFile): count overlapping matches to prevent wrong-location edits countOccurrences was advancing by searchText.length, so overlapping patterns like "aba" in "ababa" were counted as 1 (non-overlapping) instead of 2. This caused an ambiguous match to be silently applied at the first position rather than returning AMBIGUOUS. Advance by 1 to catch all overlapping occurrences. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editFile): round-trip check guards partial NFKC-expansion fuzzy matches The existing matchStart >= matchEnd guard only caught degenerate zero-width spans (e.g. "I" in "Ⅳ"). A partial match like "V" against "Ⅳ" produces a valid non-zero span [0,1) but would replace the entire source character even though "V" doesn't exist standalone in the file. Fix: after mapping indices back to the original, verify that normalizeForFuzzyMatch(original[matchStart:matchEnd]) === workingSearch. If not, the match straddled an NFKC expansion boundary → return NOT_FOUND. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editFile): return structured failed result on edit errors Replace plain string returns with `{ result: "failed", message }` objects for all failure paths in editFileTool (file not found, text not found, ambiguous match, and exceptions), matching writeFileTool's pattern so the LLM and result formatters can reliably detect failures via the `result` field. No-op case returns `result: "accepted"` since the edit technically succeeded. Also fix a stale tool name reference in builtinTools prompt instructions (writeToFile → writeFile). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e3d47ad commit d308423

20 files changed

+797
-819
lines changed

src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { logInfo, logWarn } from "@/logger";
1717
import { checkIsPlusUser } from "@/plusUtils";
1818
import { getSettings } from "@/settings/model";
1919
import { getSystemPromptWithMemory } from "@/system-prompts/systemPromptBuilder";
20-
import { writeToFileTool } from "@/tools/ComposerTools";
20+
import { writeFileTool } from "@/tools/ComposerTools";
2121
import { ToolManager } from "@/tools/toolManager";
2222
import { ToolResultFormatter } from "@/tools/ToolResultFormatter";
2323
import { ToolRegistry } from "@/tools/ToolRegistry";
@@ -697,7 +697,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
697697
contextEnvelope: userMessage.contextEnvelope,
698698
});
699699

700-
const actionStreamer = new ActionBlockStreamer(ToolManager, writeToFileTool);
700+
const actionStreamer = new ActionBlockStreamer(ToolManager, writeFileTool);
701701

702702
// Wrap the stream call with warning suppression
703703
const chatStream = await withSuppressedTokenWarnings(() =>

src/LLMProviders/chainRunner/utils/ActionBlockStreamer.test.ts

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ const MockedToolManager = ToolManager as jest.Mocked<typeof ToolManager>;
1010
const MockedToolResultFormatter = ToolResultFormatter as jest.Mocked<typeof ToolResultFormatter>;
1111

1212
describe("ActionBlockStreamer", () => {
13-
let writeToFileTool: any;
13+
let writeFileTool: any;
1414
let streamer: ActionBlockStreamer;
1515

1616
beforeEach(() => {
17-
writeToFileTool = { name: "writeToFile" };
17+
writeFileTool = { name: "writeFile" };
1818
MockedToolManager.callTool.mockClear();
1919

2020
// Mock ToolResultFormatter to return the raw result without "File change result: " prefix
2121
MockedToolResultFormatter.format = jest.fn((_toolName, result) => result);
2222

23-
streamer = new ActionBlockStreamer(MockedToolManager, writeToFileTool);
23+
streamer = new ActionBlockStreamer(MockedToolManager, writeFileTool);
2424
});
2525

2626
// Helper function to process chunks and collect results
@@ -35,7 +35,7 @@ describe("ActionBlockStreamer", () => {
3535
return outputContents;
3636
}
3737

38-
it("should pass through chunks without writeToFile tags unchanged", async () => {
38+
it("should pass through chunks without writeFile tags unchanged", async () => {
3939
const chunks = [{ content: "Hello " }, { content: "world, this is " }, { content: "a test." }];
4040
const output = await processChunks(chunks);
4141

@@ -46,112 +46,112 @@ describe("ActionBlockStreamer", () => {
4646
expect(MockedToolManager.callTool).not.toHaveBeenCalled();
4747
});
4848

49-
it("should handle a complete writeToFile block in a single chunk", async () => {
49+
it("should handle a complete writeFile block in a single chunk", async () => {
5050
MockedToolManager.callTool.mockResolvedValue("File written successfully.");
5151
const chunks = [
5252
{
5353
content:
54-
"Some text before <writeToFile><path>file.txt</path><content>content</content></writeToFile> and some text after.",
54+
"Some text before <writeFile><path>file.txt</path><content>content</content></writeFile> and some text after.",
5555
},
5656
];
5757
const output = await processChunks(chunks);
5858

5959
// Should yield original chunk plus tool result
6060
expect(output).toEqual([
61-
"Some text before <writeToFile><path>file.txt</path><content>content</content></writeToFile> and some text after.",
61+
"Some text before <writeFile><path>file.txt</path><content>content</content></writeFile> and some text after.",
6262
"\nFile written successfully.\n",
6363
]);
6464

65-
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeToFileTool, {
65+
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeFileTool, {
6666
path: "file.txt",
6767
content: "content",
6868
});
6969
});
7070

71-
it("should handle a complete xml-wrapped writeToFile block", async () => {
71+
it("should handle a complete xml-wrapped writeFile block", async () => {
7272
MockedToolManager.callTool.mockResolvedValue("XML file written.");
7373
const chunks = [
7474
{
7575
content:
76-
"```xml\n<writeToFile><path>file.xml</path><content>xml content</content></writeToFile>\n```",
76+
"```xml\n<writeFile><path>file.xml</path><content>xml content</content></writeFile>\n```",
7777
},
7878
];
7979
const output = await processChunks(chunks);
8080

8181
expect(output).toEqual([
82-
"```xml\n<writeToFile><path>file.xml</path><content>xml content</content></writeToFile>\n```",
82+
"```xml\n<writeFile><path>file.xml</path><content>xml content</content></writeFile>\n```",
8383
"\nXML file written.\n",
8484
]);
8585

86-
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeToFileTool, {
86+
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeFileTool, {
8787
path: "file.xml",
8888
content: "xml content",
8989
});
9090
});
9191

92-
it("should handle a writeToFile block split across multiple chunks", async () => {
92+
it("should handle a writeFile block split across multiple chunks", async () => {
9393
MockedToolManager.callTool.mockResolvedValue("Split file written.");
9494
const chunks = [
95-
{ content: "Here is a file <writeToFile><path>split.txt</path>" },
95+
{ content: "Here is a file <writeFile><path>split.txt</path>" },
9696
{ content: "<content>split content</content>" },
97-
{ content: "</writeToFile> That was it." },
97+
{ content: "</writeFile> That was it." },
9898
];
9999
const output = await processChunks(chunks);
100100

101101
// All chunks should be yielded as-is, plus tool result when complete block is detected
102102
expect(output).toEqual([
103-
"Here is a file <writeToFile><path>split.txt</path>",
103+
"Here is a file <writeFile><path>split.txt</path>",
104104
"<content>split content</content>",
105-
"</writeToFile> That was it.",
105+
"</writeFile> That was it.",
106106
"\nSplit file written.\n",
107107
]);
108108

109-
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeToFileTool, {
109+
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeFileTool, {
110110
path: "split.txt",
111111
content: "split content",
112112
});
113113
});
114114

115-
it("should handle multiple writeToFile blocks in the stream", async () => {
115+
it("should handle multiple writeFile blocks in the stream", async () => {
116116
MockedToolManager.callTool
117117
.mockResolvedValueOnce("File 1 written.")
118118
.mockResolvedValueOnce("File 2 written.");
119119
const chunks = [
120120
{
121121
content:
122-
"<writeToFile><path>f1.txt</path><content>c1</content></writeToFile>Some text<writeToFile><path>f2.txt</path><content>c2</content></writeToFile>",
122+
"<writeFile><path>f1.txt</path><content>c1</content></writeFile>Some text<writeFile><path>f2.txt</path><content>c2</content></writeFile>",
123123
},
124124
];
125125
const output = await processChunks(chunks);
126126

127127
// Should yield original chunk plus both tool results
128128
expect(output).toEqual([
129-
"<writeToFile><path>f1.txt</path><content>c1</content></writeToFile>Some text<writeToFile><path>f2.txt</path><content>c2</content></writeToFile>",
129+
"<writeFile><path>f1.txt</path><content>c1</content></writeFile>Some text<writeFile><path>f2.txt</path><content>c2</content></writeFile>",
130130
"\nFile 1 written.\n",
131131
"\nFile 2 written.\n",
132132
]);
133133

134134
expect(MockedToolManager.callTool).toHaveBeenCalledTimes(2);
135-
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeToFileTool, {
135+
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeFileTool, {
136136
path: "f1.txt",
137137
content: "c1",
138138
});
139-
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeToFileTool, {
139+
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeFileTool, {
140140
path: "f2.txt",
141141
content: "c2",
142142
});
143143
});
144144

145145
it("should handle unclosed tags without calling tools", async () => {
146146
const chunks = [
147-
{ content: "Starting... <writeToFile><path>unclosed.txt</path>" },
147+
{ content: "Starting... <writeFile><path>unclosed.txt</path>" },
148148
{ content: "<content>this will not be closed" },
149149
];
150150
const output = await processChunks(chunks);
151151

152152
// Should yield all chunks as-is
153153
expect(output).toEqual([
154-
"Starting... <writeToFile><path>unclosed.txt</path>",
154+
"Starting... <writeFile><path>unclosed.txt</path>",
155155
"<content>this will not be closed",
156156
]);
157157

@@ -163,17 +163,17 @@ describe("ActionBlockStreamer", () => {
163163
MockedToolManager.callTool.mockRejectedValue(new Error("Tool error"));
164164
const chunks = [
165165
{
166-
content: "<writeToFile><path>error.txt</path><content>content</content></writeToFile>",
166+
content: "<writeFile><path>error.txt</path><content>content</content></writeFile>",
167167
},
168168
];
169169
const output = await processChunks(chunks);
170170

171171
expect(output).toEqual([
172-
"<writeToFile><path>error.txt</path><content>content</content></writeToFile>",
172+
"<writeFile><path>error.txt</path><content>content</content></writeFile>",
173173
"\nError: Tool error\n",
174174
]);
175175

176-
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeToFileTool, {
176+
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeFileTool, {
177177
path: "error.txt",
178178
content: "content",
179179
});
@@ -197,12 +197,12 @@ describe("ActionBlockStreamer", () => {
197197
const chunks = [
198198
{
199199
content:
200-
"<writeToFile><path> spaced.txt </path><content> content with spaces </content></writeToFile>",
200+
"<writeFile><path> spaced.txt </path><content> content with spaces </content></writeFile>",
201201
},
202202
];
203203
await processChunks(chunks);
204204

205-
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeToFileTool, {
205+
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeFileTool, {
206206
path: "spaced.txt",
207207
content: "content with spaces",
208208
});
@@ -212,19 +212,19 @@ describe("ActionBlockStreamer", () => {
212212
MockedToolManager.callTool.mockResolvedValue("Malformed handled.");
213213
const chunks = [
214214
{
215-
content: "<writeToFile><path>missing-content.txt</path></writeToFile>",
215+
content: "<writeFile><path>missing-content.txt</path></writeFile>",
216216
},
217217
];
218218
const output = await processChunks(chunks);
219219

220220
// Should yield chunk as-is plus tool result
221221
expect(output).toEqual([
222-
"<writeToFile><path>missing-content.txt</path></writeToFile>",
222+
"<writeFile><path>missing-content.txt</path></writeFile>",
223223
"\nMalformed handled.\n",
224224
]);
225225

226226
// Tool should be called with undefined content
227-
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeToFileTool, {
227+
expect(MockedToolManager.callTool).toHaveBeenCalledWith(writeFileTool, {
228228
path: "missing-content.txt",
229229
content: undefined,
230230
});

src/LLMProviders/chainRunner/utils/ActionBlockStreamer.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,24 @@ import { ToolManager } from "@/tools/toolManager";
22
import { ToolResultFormatter } from "@/tools/ToolResultFormatter";
33

44
/**
5-
* ActionBlockStreamer processes streaming chunks to detect and handle writeToFile blocks.
5+
* ActionBlockStreamer processes streaming chunks to detect and handle writeFile blocks.
66
*
77
* 1. Accumulates chunks in a buffer
8-
* 2. Detects complete writeToFile blocks
9-
* 3. Calls the writeToFile tool when a complete block is found
8+
* 2. Detects complete writeFile blocks
9+
* 3. Calls the writeFile tool when a complete block is found
1010
* 4. Returns chunks as-is otherwise
1111
*/
1212
export class ActionBlockStreamer {
1313
private buffer = "";
1414

1515
constructor(
1616
private toolManager: typeof ToolManager,
17-
private writeToFileTool: any
17+
private writeFileTool: any
1818
) {}
1919

2020
private findCompleteBlock(str: string) {
2121
// Regex for both formats
22-
const regex = /<writeToFile>[\s\S]*?<\/writeToFile>/;
22+
const regex = /<writeFile>[\s\S]*?<\/writeFile>/;
2323
const match = str.match(regex);
2424

2525
if (!match || match.index === undefined) {
@@ -71,13 +71,13 @@ export class ActionBlockStreamer {
7171

7272
// Call the tool
7373
try {
74-
const result = await this.toolManager.callTool(this.writeToFileTool, {
74+
const result = await this.toolManager.callTool(this.writeFileTool, {
7575
path: filePath,
7676
content: fileContent,
7777
});
7878

7979
// Format tool result using ToolResultFormatter for consistency with agent mode
80-
const formattedResult = ToolResultFormatter.format("writeToFile", result);
80+
const formattedResult = ToolResultFormatter.format("writeFile", result);
8181
yield { ...chunk, content: `\n${formattedResult}\n` };
8282
} catch (err: any) {
8383
yield { ...chunk, content: `\nError: ${err?.message || err}\n` };

src/LLMProviders/chainRunner/utils/AgentReasoningState.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,8 @@ export function summarizeToolResult(
252252
return "Indexed vault";
253253
case "updateMemory":
254254
return "Updated memory";
255-
case "writeToFile":
256-
case "replaceInFile": {
255+
case "writeFile":
256+
case "editFile": {
257257
// Parse the result to check if accepted/rejected
258258
const filePath = args?.path as string | undefined;
259259
const fileName = filePath ? filePath.split("/").pop() || filePath : "file";
@@ -269,7 +269,7 @@ export function summarizeToolResult(
269269
// TODO(@wenzhengjiang): Handle no-op cases (e.g., "File is too small", "Search text not found")
270270
// Requires ComposerTools to return structured results instead of plain strings.
271271
// See docs/TODO-composer-tool-redesign.md
272-
return toolName === "writeToFile" ? `Wrote to "${fileName}"` : `Edited "${fileName}"`;
272+
return toolName === "writeFile" ? `Wrote to "${fileName}"` : `Edited "${fileName}"`;
273273
}
274274
default:
275275
return "Done";
@@ -422,15 +422,15 @@ export function summarizeToolCall(
422422
return "Indexing vault";
423423
case "updateMemory":
424424
return "Saving to memory";
425-
case "writeToFile": {
425+
case "writeFile": {
426426
const filePath = args?.path as string | undefined;
427427
if (filePath) {
428428
const fileName = filePath.split("/").pop() || filePath;
429429
return `Writing to "${fileName}"`;
430430
}
431431
return "Writing to file";
432432
}
433-
case "replaceInFile": {
433+
case "editFile": {
434434
const filePath = args?.path as string | undefined;
435435
if (filePath) {
436436
const fileName = filePath.split("/").pop() || filePath;

src/LLMProviders/chainRunner/utils/modelAdapter.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ describe("ModelAdapter", () => {
2121
const toolMetadata: ToolMetadata[] = [
2222
createToolMetadata("localSearch", "LocalSearch specific instructions"),
2323
createToolMetadata("webSearch", "WebSearch specific instructions"),
24-
createToolMetadata("writeToFile", "WriteToFile specific instructions"),
24+
createToolMetadata("writeFile", "WriteToFile specific instructions"),
2525
];
2626

2727
const enhancedPrompt = adapter.enhanceSystemPrompt(
2828
basePrompt,
2929
toolDescriptions,
30-
["localSearch", "webSearch", "writeToFile"],
30+
["localSearch", "webSearch", "writeFile"],
3131
toolMetadata
3232
);
3333

@@ -44,7 +44,7 @@ describe("ModelAdapter", () => {
4444
const toolMetadata: ToolMetadata[] = [
4545
createToolMetadata("localSearch", "LocalSearch specific instructions"),
4646
createToolMetadata("webSearch", "WebSearch specific instructions"),
47-
createToolMetadata("writeToFile", "WriteToFile specific instructions"),
47+
createToolMetadata("writeFile", "WriteToFile specific instructions"),
4848
];
4949

5050
// Only pass localSearch as enabled
@@ -129,15 +129,15 @@ describe("ModelAdapter", () => {
129129
const enhancedPrompt = adapter.enhanceSystemPrompt(
130130
basePrompt,
131131
toolDescriptions,
132-
["replaceInFile", "writeToFile"],
132+
["editFile", "writeFile"],
133133
[]
134134
);
135135

136136
// Check for composer-specific GPT instructions (simplified without XML examples)
137137
expect(enhancedPrompt).toContain("FILE EDITING WITH COMPOSER TOOLS");
138-
expect(enhancedPrompt).toContain("replaceInFile");
139-
expect(enhancedPrompt).toContain("writeToFile");
140-
expect(enhancedPrompt).toContain("SEARCH/REPLACE format");
138+
expect(enhancedPrompt).toContain("editFile");
139+
expect(enhancedPrompt).toContain("writeFile");
140+
expect(enhancedPrompt).toContain("oldText");
141141
});
142142

143143
it("should rebuild enhanceSystemPrompt output from section metadata", () => {
@@ -164,8 +164,8 @@ describe("ModelAdapter", () => {
164164
const enhanced = adapter.enhanceUserMessage(editMessage, true);
165165

166166
expect(enhanced).toContain("GPT REMINDER");
167-
expect(enhanced).toContain("replaceInFile");
168-
expect(enhanced).toContain("SEARCH/REPLACE blocks");
167+
expect(enhanced).toContain("editFile");
168+
expect(enhanced).toContain("oldText/newText parameters");
169169
});
170170
});
171171
});

0 commit comments

Comments
 (0)