Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/opencode/src/tool/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ export const WriteTool = Tool.define("write", {
output += `\n\nLSP errors detected in other files:\n<diagnostics file="${file}">\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</diagnostics>`
}

// Send metadata update to signal tool completion (fixes issue #15675)
ctx.metadata({
metadata: {
diagnostics,
filepath,
exists: exists,
},
})

return {
title: path.relative(Instance.worktree, filepath),
metadata: {
Expand Down
71 changes: 55 additions & 16 deletions packages/opencode/test/tool/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import { WriteTool } from "../../src/tool/write"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"

const ctx = {
const createCtx = (metadataSpy: (args?: any) => void = () => {}) => ({
sessionID: "test-write-session",
messageID: "",
callID: "",
agent: "build",
abort: AbortSignal.any([]),
messages: [],
metadata: () => {},
metadata: metadataSpy,
ask: async () => {},
}
})

// Default context for tests that don't need to spy on metadata
const ctx = createCtx()

describe("tool.write", () => {
describe("new file creation", () => {
Expand All @@ -31,7 +34,7 @@ describe("tool.write", () => {
filePath: filepath,
content: "Hello, World!",
},
ctx,
createCtx(),
)

expect(result.output).toContain("Wrote file successfully")
Expand All @@ -43,6 +46,42 @@ describe("tool.write", () => {
})
})

test("calls ctx.metadata to signal tool completion (issue #15675)", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "newfile.txt")
let metadataCalled = false
let metadataArgs: any = null

const ctx = createCtx((args: any) => {
metadataCalled = true
metadataArgs = args
})

await Instance.provide({
directory: tmp.path,
fn: async () => {
const write = await WriteTool.init()
const result = await write.execute(
{
filePath: filepath,
content: "Hello, World!",
},
ctx,
)

expect(metadataCalled).toBe(true)
expect(metadataArgs).toHaveProperty("metadata")
expect(metadataArgs.metadata).toHaveProperty("filepath", filepath)
expect(metadataArgs.metadata).toHaveProperty("exists", false)
// Verify metadata contains expected fields (truncated is auto-added by Tool.define)
expect(result.metadata).toMatchObject({
filepath,
exists: false,
})
},
})
})

test("creates parent directories if needed", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "nested", "deep", "file.txt")
Expand All @@ -56,7 +95,7 @@ describe("tool.write", () => {
filePath: filepath,
content: "nested content",
},
ctx,
createCtx(),
)

const content = await fs.readFile(filepath, "utf-8")
Expand All @@ -77,7 +116,7 @@ describe("tool.write", () => {
filePath: "relative.txt",
content: "relative content",
},
ctx,
createCtx(),
)

const content = await fs.readFile(path.join(tmp.path, "relative.txt"), "utf-8")
Expand Down Expand Up @@ -106,7 +145,7 @@ describe("tool.write", () => {
filePath: filepath,
content: "new content",
},
ctx,
createCtx(),
)

expect(result.output).toContain("Wrote file successfully")
Expand Down Expand Up @@ -135,7 +174,7 @@ describe("tool.write", () => {
filePath: filepath,
content: "new",
},
ctx,
createCtx(),
)

// Diff should be in metadata
Expand All @@ -160,7 +199,7 @@ describe("tool.write", () => {
filePath: filepath,
content: JSON.stringify({ secret: "data" }),
},
ctx,
createCtx(),
)

// On Unix systems, check permissions
Expand Down Expand Up @@ -188,7 +227,7 @@ describe("tool.write", () => {
filePath: filepath,
content: JSON.stringify(data, null, 2),
},
ctx,
createCtx(),
)

const content = await fs.readFile(filepath, "utf-8")
Expand All @@ -211,7 +250,7 @@ describe("tool.write", () => {
filePath: filepath,
content,
},
ctx,
createCtx(),
)

const buf = await fs.readFile(filepath)
Expand All @@ -233,7 +272,7 @@ describe("tool.write", () => {
filePath: filepath,
content: "",
},
ctx,
createCtx(),
)

const content = await fs.readFile(filepath, "utf-8")
Expand All @@ -259,7 +298,7 @@ describe("tool.write", () => {
filePath: filepath,
content: lines,
},
ctx,
createCtx(),
)

const content = await fs.readFile(filepath, "utf-8")
Expand All @@ -282,7 +321,7 @@ describe("tool.write", () => {
filePath: filepath,
content,
},
ctx,
createCtx(),
)

const buf = await fs.readFile(filepath)
Expand Down Expand Up @@ -314,7 +353,7 @@ describe("tool.write", () => {
filePath: readonlyPath,
content: "new content",
},
ctx,
createCtx(),
),
).rejects.toThrow()
},
Expand All @@ -337,7 +376,7 @@ describe("tool.write", () => {
filePath: filepath,
content: "export const Button = () => {}",
},
ctx,
createCtx(),
)

expect(result.title).toEndWith(path.join("src", "components", "Button.tsx"))
Expand Down
Loading