fix(core): return write diffs
This commit is contained in:
parent
a8d2e8fa81
commit
1957e167dc
5 changed files with 75 additions and 13 deletions
|
|
@ -44,6 +44,11 @@ export interface WriteResult {
|
|||
readonly existed: boolean
|
||||
}
|
||||
|
||||
export interface TextWriteResult extends WriteResult {
|
||||
readonly before: string
|
||||
readonly after: string
|
||||
}
|
||||
|
||||
export interface RemoveResult {
|
||||
readonly operation: "remove"
|
||||
readonly target: string
|
||||
|
|
@ -56,7 +61,7 @@ export interface Interface {
|
|||
readonly create: (input: WriteInput) => Effect.Effect<WriteResult, TargetExistsError | FSUtil.Error>
|
||||
readonly write: (input: WriteInput) => Effect.Effect<WriteResult, FSUtil.Error>
|
||||
/** Write text while retaining an existing UTF-8 BOM and emitting at most one BOM. */
|
||||
readonly writeTextPreservingBom: (input: TextWriteInput) => Effect.Effect<WriteResult, FSUtil.Error>
|
||||
readonly writeTextPreservingBom: (input: TextWriteInput) => Effect.Effect<TextWriteResult, FSUtil.Error>
|
||||
/** Commit only if an existing target still has the expected bytes. */
|
||||
readonly writeIfUnchanged: (
|
||||
input: ConditionalWriteInput,
|
||||
|
|
@ -112,11 +117,13 @@ const layer = Layer.effect(
|
|||
const current = yield* fs
|
||||
.readFile(input.target.canonical)
|
||||
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)))
|
||||
yield* fs.writeWithDirs(
|
||||
input.target.canonical,
|
||||
joinBom(next.text, Boolean(current && hasUtf8Bom(current)) || next.bom),
|
||||
)
|
||||
return writeResult(input.target, current !== undefined)
|
||||
const content = joinBom(next.text, Boolean(current && hasUtf8Bom(current)) || next.bom)
|
||||
yield* fs.writeWithDirs(input.target.canonical, content)
|
||||
return {
|
||||
...writeResult(input.target, current !== undefined),
|
||||
before: current ? new TextDecoder().decode(current).replace(/^\uFEFF/, "") : "",
|
||||
after: content.replace(/^\uFEFF/, ""),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export * as WriteTool from "./write"
|
|||
|
||||
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import { createTwoFilesPatch, diffLines } from "diff"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { FileMutation } from "../file-mutation"
|
||||
import { LocationMutation } from "../location-mutation"
|
||||
|
|
@ -30,6 +32,7 @@ export const Output = Schema.Struct({
|
|||
target: Schema.String,
|
||||
resource: Schema.String,
|
||||
existed: Schema.Boolean,
|
||||
files: Schema.Array(FileDiff.Info),
|
||||
})
|
||||
export type Output = typeof Output.Type
|
||||
|
||||
|
|
@ -84,7 +87,28 @@ export const Plugin = {
|
|||
agent: context.agent,
|
||||
source,
|
||||
})
|
||||
return yield* files.writeTextPreservingBom({ target, content: input.content })
|
||||
const result = yield* files.writeTextPreservingBom({ target, content: input.content })
|
||||
const counts = diffLines(result.before, result.after).reduce(
|
||||
(total, item) => ({
|
||||
additions: total.additions + (item.added ? (item.count ?? 0) : 0),
|
||||
deletions: total.deletions + (item.removed ? (item.count ?? 0) : 0),
|
||||
}),
|
||||
{ additions: 0, deletions: 0 },
|
||||
)
|
||||
return {
|
||||
operation: result.operation,
|
||||
target: result.target,
|
||||
resource: result.resource,
|
||||
existed: result.existed,
|
||||
files: [
|
||||
{
|
||||
file: result.resource,
|
||||
patch: createTwoFilesPatch(result.resource, result.resource, result.before, result.after),
|
||||
status: result.existed ? "modified" : "added",
|
||||
...counts,
|
||||
},
|
||||
],
|
||||
} satisfies Output
|
||||
}).pipe(
|
||||
Effect.mapError((error) => new ToolFailure({ message: `Unable to write ${input.path}`, error })),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -80,9 +80,14 @@ describe("FileMutation", () => {
|
|||
const created = yield* (yield* LocationMutation.Service).resolve({ path: "created.txt" })
|
||||
const files = yield* FileMutation.Service
|
||||
|
||||
yield* files.writeTextPreservingBom({ target: preserved, content: "\uFEFFafter" })
|
||||
yield* files.writeTextPreservingBom({ target: created, content: "\uFEFF\uFEFF\uFEFFcreated" })
|
||||
const preservedResult = yield* files.writeTextPreservingBom({ target: preserved, content: "\uFEFFafter" })
|
||||
const createdResult = yield* files.writeTextPreservingBom({
|
||||
target: created,
|
||||
content: "\uFEFF\uFEFF\uFEFFcreated",
|
||||
})
|
||||
|
||||
expect(preservedResult).toMatchObject({ existed: true, before: "before", after: "after" })
|
||||
expect(createdResult).toMatchObject({ existed: false, before: "", after: "created" })
|
||||
expect(yield* Effect.promise(() => fs.readFile(preservedPath, "utf8"))).toBe("\uFEFFafter")
|
||||
expect(yield* Effect.promise(() => fs.readFile(created.canonical, "utf8"))).toBe("\uFEFFcreated")
|
||||
}).pipe(provide(directory)),
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ describe("WriteTool", () => {
|
|||
Effect.gen(function* () {
|
||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["write"])
|
||||
const settled = yield* settleTool(registry, call({ path: "src/new.txt", content: "created" }))
|
||||
expect(settled).toEqual({
|
||||
expect(settled).toMatchObject({
|
||||
result: { type: "text", value: "Created file successfully: src/new.txt" },
|
||||
output: {
|
||||
structured: {
|
||||
|
|
@ -128,6 +128,14 @@ describe("WriteTool", () => {
|
|||
target: path.join(yield* Effect.promise(() => fs.realpath(tmp.path)), "src", "new.txt"),
|
||||
resource: "src/new.txt",
|
||||
existed: false,
|
||||
files: [
|
||||
{
|
||||
file: "src/new.txt",
|
||||
status: "added",
|
||||
additions: 1,
|
||||
deletions: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
content: [{ type: "text", text: "Created file successfully: src/new.txt" }],
|
||||
},
|
||||
|
|
@ -156,7 +164,21 @@ describe("WriteTool", () => {
|
|||
Effect.andThen((settled) =>
|
||||
Effect.gen(function* () {
|
||||
expect(settled.result).toEqual({ type: "text", value: "Wrote file successfully: existing.txt" })
|
||||
expect(settled.output?.structured).toMatchObject({ resource: "existing.txt", existed: true })
|
||||
expect(settled.output?.structured).toMatchObject({
|
||||
resource: "existing.txt",
|
||||
existed: true,
|
||||
files: [
|
||||
{
|
||||
file: "existing.txt",
|
||||
status: "modified",
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
const structured = settled.output?.structured as WriteTool.Output
|
||||
expect(structured.files[0]?.patch).toContain("-before")
|
||||
expect(structured.files[0]?.patch).toContain("+after")
|
||||
expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "existing.txt"), "utf8"))).toBe(
|
||||
"after",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2654,6 +2654,10 @@ function Write(props: ToolProps) {
|
|||
const code = createMemo(() => {
|
||||
return stringValue(props.input.content) ?? ""
|
||||
})
|
||||
const file = createMemo(() => parseApplyPatchFiles(props.metadata.files)[0])
|
||||
const patch = createMemo(
|
||||
() => file()?.patch ?? createTwoFilesPatch("", stringValue(props.input.path) ?? "", "", code()),
|
||||
)
|
||||
const complete = createMemo(() => props.part.state.status === "completed")
|
||||
const view = createMemo(() => {
|
||||
if (ctx.config.diffs?.view === "unified") return "unified"
|
||||
|
|
@ -2671,10 +2675,10 @@ function Write(props: ToolProps) {
|
|||
}}
|
||||
part={props.part}
|
||||
>
|
||||
<Show when={code()}>
|
||||
<Show when={code() || file()?.additions || file()?.deletions}>
|
||||
<box paddingLeft={1}>
|
||||
<diff
|
||||
diff={createTwoFilesPatch("", stringValue(props.input.path) ?? "", "", code())}
|
||||
diff={patch()}
|
||||
view={view()}
|
||||
filetype={filetype(stringValue(props.input.path))}
|
||||
syntaxStyle={syntax()}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue