feat(core): improve edit tool output (#39211)

This commit is contained in:
Aiden Cline 2026-07-27 19:11:04 -05:00 committed by GitHub
commit f15398efc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 54 deletions

View file

@ -60,23 +60,6 @@ const countOccurrences = (content: string, search: string) => {
return count
}
const previewLines = (value: string, prefix: "+" | "-") => {
const lines = normalizeLineEndings(value).split("\n")
const shown = lines.slice(0, 6).map((line) => `${prefix}${line.length > 240 ? `${line.slice(0, 240)}...` : line}`)
if (lines.length > shown.length) shown.push(`${prefix}...`)
return shown
}
export const toModelOutput = (output: Output, oldString: string, newString: string) =>
[
`Edited file successfully: ${output.files[0]?.file}`,
`Replacements: ${output.replacements}`,
"```diff",
...previewLines(oldString, "-"),
...previewLines(newString, "+"),
"```",
].join("\n")
/** Deferred edit behavior and UX integrations remain visible at the model-facing seam. */
// TODO: Port V1 fuzzy correction strategies only after exact-edit behavior is established: line-trimmed matching, block-anchor fallback, indentation correction, and similarity-threshold review.
// TODO: Add formatter integration after formatter runtime exists.
@ -103,11 +86,6 @@ export const Plugin = {
input: Input,
output: Output,
execute: (input, context) => {
const unableToEdit = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
effect.pipe(
Effect.mapError((error) => new ToolFailure({ message: `Unable to edit ${input.path}`, error })),
)
return Effect.gen(function* () {
const permissionSource = {
type: "tool" as const,
@ -125,44 +103,46 @@ export const Plugin = {
})
}
const target = yield* unableToEdit(mutation.resolve({ path: input.path, kind: "file" }))
const target = yield* mutation.resolve({ path: input.path, kind: "file" })
const external = target.externalDirectory
if (external) {
yield* unableToEdit(
permission.assert({
...LocationMutation.externalDirectoryPermission(external),
sessionID: context.sessionID,
agent: context.agent,
source: permissionSource,
}),
)
}
yield* unableToEdit(
permission.assert({
action: "edit",
resources: [target.resource],
save: ["*"],
yield* permission.assert({
...LocationMutation.externalDirectoryPermission(external),
sessionID: context.sessionID,
agent: context.agent,
source: permissionSource,
}),
})
}
yield* permission.assert({
action: "edit",
resources: [target.resource],
save: ["*"],
sessionID: context.sessionID,
agent: context.agent,
source: permissionSource,
})
const info = yield* fs.stat(target.canonical).pipe(
Effect.catchReason("PlatformError", "NotFound", () =>
Effect.fail(new ToolFailure({ message: `File not found: ${input.path}` })),
),
)
const source = decodeUtf8(yield* unableToEdit(fs.readFile(target.canonical)))
if (info.type === "Directory") {
return yield* new ToolFailure({ message: `Path is a directory, not a file: ${input.path}` })
}
const source = decodeUtf8(yield* fs.readFile(target.canonical))
const ending = detectLineEnding(source.text)
const oldString = convertToLineEnding(input.oldString, ending)
const newString = convertToLineEnding(input.newString, ending)
const replacements = countOccurrences(source.text, oldString)
if (replacements === 0) {
return yield* new ToolFailure({
message:
"Could not find oldString in the file. It must match exactly, including whitespace and indentation.",
message: `Could not find oldString in ${input.path}. It must match exactly, including whitespace and indentation.`,
})
}
if (replacements > 1 && input.replaceAll !== true) {
return yield* new ToolFailure({
message:
"Found multiple exact matches for oldString. Provide more surrounding context or set replaceAll to true.",
message: `Found ${replacements} matches for oldString, but expected exactly one. Add more surrounding context to make oldString unique, or set replaceAll to true to replace every occurrence.`,
})
}
@ -178,12 +158,10 @@ export const Plugin = {
{ additions: 0, deletions: 0 },
)
const next = splitBom(replaced)
const result = yield* unableToEdit(
files.write({
target,
content: joinBom(next.text, source.bom || next.bom),
}),
)
const result = yield* files.write({
target,
content: joinBom(next.text, source.bom || next.bom),
})
return {
files: [
{
@ -198,9 +176,14 @@ export const Plugin = {
}).pipe(
Effect.map((output) => ({
output,
content: toModelOutput(output, input.oldString, input.newString),
content: `Edited ${output.files[0]?.file} (${output.replacements} replacement${output.replacements === 1 ? "" : "s"})`,
metadata: { files: output.files },
})),
Effect.mapError((error) =>
error instanceof ToolFailure
? error
: new ToolFailure({ message: `Unable to edit ${input.path}`, error }),
),
)
},
}),