fix(tool): align multiedit schema with single-file edits

This commit is contained in:
Kit Langton
2026-04-18 12:54:29 -04:00
parent a49b5adfbd
commit f2cbcacf00
4 changed files with 83 additions and 19 deletions

View File

@@ -10,7 +10,6 @@ export const Parameters = Schema.Struct({
edits: Schema.mutable(
Schema.Array(
Schema.Struct({
filePath: Schema.String.annotate({ description: "The absolute path to the file to modify" }),
oldString: Schema.String.annotate({ description: "The text to replace" }),
newString: Schema.String.annotate({
description: "The text to replace it with (must be different from oldString)",
@@ -32,17 +31,10 @@ export const MultiEditTool = Tool.define(
return {
description: DESCRIPTION,
parameters: Parameters,
execute: (
params: {
filePath: string
edits: Array<{ filePath: string; oldString: string; newString: string; replaceAll?: boolean }>
},
ctx: Tool.Context,
) =>
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
Effect.gen(function* () {
const results = []
for (const [, entry] of params.edits.entries()) {
const result = yield* edit.execute(
const results = yield* Effect.forEach(params.edits, (entry) =>
edit.execute(
{
filePath: params.filePath,
oldString: entry.oldString,
@@ -51,8 +43,7 @@ export const MultiEditTool = Tool.define(
},
ctx,
)
results.push(result)
}
)
return {
title: path.relative(Instance.worktree, params.filePath),
metadata: {

View File

@@ -228,10 +228,6 @@ exports[`tool parameters JSON Schema (wire shape) multiedit 1`] = `
"description": "Array of edit operations to perform sequentially on the file",
"items": {
"properties": {
"filePath": {
"description": "The absolute path to the file to modify",
"type": "string",
},
"newString": {
"description": "The text to replace it with (must be different from oldString)",
"type": "string",
@@ -246,7 +242,6 @@ exports[`tool parameters JSON Schema (wire shape) multiedit 1`] = `
},
},
"required": [
"filePath",
"oldString",
"newString",
],

View File

@@ -0,0 +1,78 @@
import { afterEach, describe, expect } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { Effect, Layer } from "effect"
import { Agent } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Format } from "../../src/format"
import { LSP } from "../../src/lsp"
import { Instance } from "../../src/project/instance"
import { MessageID, SessionID } from "../../src/session/schema"
import { MultiEditTool } from "../../src/tool/multiedit"
import { Truncate, Tool } from "../../src/tool"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const ctx = {
sessionID: SessionID.make("ses_test-multiedit-session"),
messageID: MessageID.make(""),
callID: "",
agent: "build",
abort: AbortSignal.any([]),
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
}
afterEach(async () => {
await Instance.disposeAll()
})
const it = testEffect(
Layer.mergeAll(
LSP.defaultLayer,
AppFileSystem.defaultLayer,
Format.defaultLayer,
Bus.layer,
CrossSpawnSpawner.defaultLayer,
Truncate.defaultLayer,
Agent.defaultLayer,
),
)
const init = Effect.fn("MultiEditToolTest.init")(function* () {
const info = yield* MultiEditTool
return yield* info.init()
})
const run = Effect.fn("MultiEditToolTest.run")(function* (
args: Tool.InferParameters<typeof MultiEditTool>,
next: Tool.Context = ctx,
) {
const tool = yield* init()
return yield* tool.execute(args, next)
})
describe("tool.multiedit", () => {
it.live("applies multiple edits to the same file in sequence", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
const filePath = path.join(dir, "file.txt")
yield* Effect.promise(() => fs.writeFile(filePath, "alpha\nbeta\ngamma\n", "utf-8"))
yield* run({
filePath,
edits: [
{ oldString: "alpha", newString: "delta" },
{ oldString: "gamma", newString: "omega" },
],
})
const content = yield* Effect.promise(() => fs.readFile(filePath, "utf-8"))
expect(content).toBe("delta\nbeta\nomega\n")
}),
),
)
})

View File

@@ -179,7 +179,7 @@ describe("tool parameters", () => {
test("accepts an edit entry", () => {
const parsed = parse(MultiEdit, {
filePath: "/a",
edits: [{ filePath: "/a", oldString: "x", newString: "y" }],
edits: [{ oldString: "x", newString: "y" }],
})
expect(parsed.edits.length).toBe(1)
})