mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: use old_text/new_text in edit_files tool schema (#25658)
Models frequently confuse the search and replace fields in the
edit_files tool (CODAGT-312). Rename the model-facing JSON fields
to old_text/new_text so the intent is unambiguous.
Backend: custom UnmarshalJSON on editFileEdit falls back to
deprecated search/replace when old_text/new_text are empty. The
workspace agent API is unchanged; toSDKFiles maps old_text/new_text
back to search/replace for agent/agentfiles.
Frontend: normalizeEdit in parseEditFilesArgs accepts both
old_text/new_text and search/replace, normalizing to the internal
{ search, replace } representation so streaming diff rendering
works with either field naming convention.
This commit is contained in:
@@ -763,6 +763,57 @@ describe("parseEditFilesArgs", () => {
|
||||
expect(parsed[0].edits[0].replace).toBe("");
|
||||
});
|
||||
|
||||
it("accepts old_text/new_text field names", () => {
|
||||
const args = {
|
||||
files: [
|
||||
{
|
||||
path: "a.ts",
|
||||
edits: [{ old_text: "before", new_text: "after" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = parseEditFilesArgs(args);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].edits).toHaveLength(1);
|
||||
expect(result[0].edits[0]).toEqual({ search: "before", replace: "after" });
|
||||
});
|
||||
|
||||
it("prefers old_text/new_text over search/replace when both present", () => {
|
||||
const args = {
|
||||
files: [
|
||||
{
|
||||
path: "a.ts",
|
||||
edits: [
|
||||
{
|
||||
old_text: "from-old-text",
|
||||
new_text: "from-new-text",
|
||||
search: "from-search",
|
||||
replace: "from-replace",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = parseEditFilesArgs(args);
|
||||
expect(result[0].edits[0]).toEqual({
|
||||
search: "from-old-text",
|
||||
replace: "from-new-text",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves deletion via old_text/new_text (empty new_text)", () => {
|
||||
const args = {
|
||||
files: [
|
||||
{
|
||||
path: "a.ts",
|
||||
edits: [{ old_text: "remove me", new_text: "" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = parseEditFilesArgs(args);
|
||||
expect(result[0].edits[0]).toEqual({ search: "remove me", replace: "" });
|
||||
});
|
||||
|
||||
// During streaming the model may emit a file entry before any
|
||||
// edit is complete. Every edit has a missing replace, so all are
|
||||
// filtered out. The file entry survives with an empty edits
|
||||
|
||||
@@ -12,12 +12,29 @@ export interface EditFilesFileEntry {
|
||||
edits: Array<{ search: string; replace: string }>;
|
||||
}
|
||||
|
||||
const searchReplaceSchema = Yup.object({
|
||||
search: Yup.string().required(),
|
||||
replace: Yup.string().defined(),
|
||||
}).required();
|
||||
|
||||
type SearchReplace = Yup.InferType<typeof searchReplaceSchema>;
|
||||
// Validates that the edit has at least the shape of an object with
|
||||
// string-typed text fields. Accepts both current field names
|
||||
// (old_text/new_text) and deprecated names (search/replace).
|
||||
const normalizeEdit = (
|
||||
e: unknown,
|
||||
): { search: string; replace: string } | null => {
|
||||
if (typeof e !== "object" || e === null) return null;
|
||||
const raw = e as Record<string, unknown>;
|
||||
const search =
|
||||
typeof raw.old_text === "string"
|
||||
? raw.old_text
|
||||
: typeof raw.search === "string"
|
||||
? raw.search
|
||||
: null;
|
||||
const replace =
|
||||
typeof raw.new_text === "string"
|
||||
? raw.new_text
|
||||
: typeof raw.replace === "string"
|
||||
? raw.replace
|
||||
: null;
|
||||
if (!search || replace === null) return null;
|
||||
return { search, replace };
|
||||
};
|
||||
|
||||
const fileEntrySchema = Yup.object({
|
||||
path: Yup.string().required(),
|
||||
@@ -688,15 +705,15 @@ export const parseEditFilesArgs = (args: unknown): EditFilesFileEntry[] => {
|
||||
.filter((f): f is FileEntry => isValid(fileEntrySchema, f))
|
||||
.map((f) => ({
|
||||
path: f.path,
|
||||
edits: f.edits.filter((e): e is SearchReplace =>
|
||||
isValid(searchReplaceSchema, e),
|
||||
),
|
||||
edits: f.edits
|
||||
.map(normalizeEdit)
|
||||
.filter((e): e is { search: string; replace: string } => e !== null),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a synthetic unified diff from search/replace edit pairs
|
||||
* for a single file. Each edit becomes a separate
|
||||
* Builds a synthetic unified diff from edit pairs (normalized to
|
||||
* search/replace) for a single file. Each edit becomes a separate
|
||||
* `Diff.createPatch` call; the patches are concatenated and
|
||||
* parsed into a single FileDiffMetadata.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user