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
13 changes: 7 additions & 6 deletions packages/opencode/src/tool/apply_patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { LSP } from "../lsp"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import DESCRIPTION from "./apply_patch.txt"
import { File } from "../file"
import { relative } from "./relative"
import { Format } from "../format"

const PatchParams = z.object({
Expand Down Expand Up @@ -177,7 +178,7 @@ export const ApplyPatchTool = Tool.define(
// Build per-file metadata for UI rendering (used for both permission and result)
const files = fileChanges.map((change) => ({
filePath: change.filePath,
relativePath: path.relative(Instance.worktree, change.movePath ?? change.filePath).replaceAll("\\", "/"),
relativePath: relative(change.movePath ?? change.filePath).replaceAll("\\", "/"),
type: change.type,
patch: change.diff,
additions: change.additions,
Expand All @@ -186,7 +187,7 @@ export const ApplyPatchTool = Tool.define(
}))

// Check permissions if needed
const relativePaths = fileChanges.map((c) => path.relative(Instance.worktree, c.filePath).replaceAll("\\", "/"))
const relativePaths = fileChanges.map((c) => relative(c.filePath).replaceAll("\\", "/"))
yield* ctx.ask({
permission: "edit",
patterns: relativePaths,
Expand Down Expand Up @@ -255,13 +256,13 @@ export const ApplyPatchTool = Tool.define(
// Generate output summary
const summaryLines = fileChanges.map((change) => {
if (change.type === "add") {
return `A ${path.relative(Instance.worktree, change.filePath).replaceAll("\\", "/")}`
return `A ${relative(change.filePath).replaceAll("\\", "/")}`
}
if (change.type === "delete") {
return `D ${path.relative(Instance.worktree, change.filePath).replaceAll("\\", "/")}`
return `D ${relative(change.filePath).replaceAll("\\", "/")}`
}
const target = change.movePath ?? change.filePath
return `M ${path.relative(Instance.worktree, target).replaceAll("\\", "/")}`
return `M ${relative(target).replaceAll("\\", "/")}`
})
let output = `Success. Updated the following files:\n${summaryLines.join("\n")}`

Expand All @@ -270,7 +271,7 @@ export const ApplyPatchTool = Tool.define(
const target = change.movePath ?? change.filePath
const block = LSP.Diagnostic.report(target, diagnostics[AppFileSystem.normalizePath(target)] ?? [])
if (!block) continue
const rel = path.relative(Instance.worktree, target).replaceAll("\\", "/")
const rel = relative(target).replaceAll("\\", "/")
output += `\n\nLSP errors detected in ${rel}, please fix:\n${block}`
}

Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/tool/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Format } from "../format"
import { Instance } from "../project/instance"
import { Snapshot } from "@/snapshot"
import { assertExternalDirectoryEffect } from "./external-directory"
import { relative } from "./relative"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"

function normalizeLineEndings(text: string): string {
Expand Down Expand Up @@ -64,6 +65,7 @@ export const EditTool = Tool.define(
? params.filePath
: path.join(Instance.directory, params.filePath)
yield* assertExternalDirectoryEffect(ctx, filePath)
const rel = relative(filePath)

let diff = ""
let contentOld = ""
Expand All @@ -75,7 +77,7 @@ export const EditTool = Tool.define(
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
yield* ctx.ask({
permission: "edit",
patterns: [path.relative(Instance.worktree, filePath)],
patterns: [rel],
always: ["*"],
metadata: {
filepath: filePath,
Expand Down Expand Up @@ -171,7 +173,7 @@ export const EditTool = Tool.define(
diff,
filediff,
},
title: `${path.relative(Instance.worktree, filePath)}`,
title: rel,
output,
}
}),
Expand Down
5 changes: 2 additions & 3 deletions packages/opencode/src/tool/multiedit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { Effect } from "effect"
import * as Tool from "./tool"
import { EditTool } from "./edit"
import DESCRIPTION from "./multiedit.txt"
import path from "path"
import { Instance } from "../project/instance"
import { relative } from "./relative"

export const MultiEditTool = Tool.define(
"multiedit",
Expand Down Expand Up @@ -49,7 +48,7 @@ export const MultiEditTool = Tool.define(
results.push(result)
}
return {
title: path.relative(Instance.worktree, params.filePath),
title: relative(params.filePath),
metadata: {
results: results.map((r) => r.metadata),
},
Expand Down
11 changes: 6 additions & 5 deletions packages/opencode/src/tool/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import DESCRIPTION from "./read.txt"
import { Instance } from "../project/instance"
import { assertExternalDirectoryEffect } from "./external-directory"
import { Instruction } from "../session/instruction"
import { relative } from "./relative"

const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
Expand Down Expand Up @@ -89,7 +90,7 @@ export const ReadTool = Tool.define(
if (process.platform === "win32") {
filepath = AppFileSystem.normalizePath(filepath)
}
const title = path.relative(Instance.worktree, filepath)
const rel = relative(filepath)

const stat = yield* fs.stat(filepath).pipe(
Effect.catchIf(
Expand All @@ -105,7 +106,7 @@ export const ReadTool = Tool.define(

yield* ctx.ask({
permission: "read",
patterns: [filepath],
patterns: [rel],
always: ["*"],
metadata: {},
})
Expand All @@ -121,7 +122,7 @@ export const ReadTool = Tool.define(
const truncated = start + sliced.length < items.length

return {
title,
title: rel,
output: [
`<path>${filepath}</path>`,
`<type>directory</type>`,
Expand All @@ -148,7 +149,7 @@ export const ReadTool = Tool.define(
if (isImage || isPdf) {
const msg = `${isImage ? "Image" : "PDF"} read successfully`
return {
title,
title: rel,
output: msg,
metadata: {
preview: msg,
Expand Down Expand Up @@ -200,7 +201,7 @@ export const ReadTool = Tool.define(
}

return {
title,
title: rel,
output,
metadata: {
preview: file.raw.slice(0, 20).join("\n"),
Expand Down
7 changes: 7 additions & 0 deletions packages/opencode/src/tool/relative.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import path from "path"
import { Instance } from "../project/instance"

export function relative(file: string) {
const root = Instance.worktree === "/" ? Instance.directory : Instance.worktree
return path.relative(root, file)
}
7 changes: 4 additions & 3 deletions packages/opencode/src/tool/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Instance } from "../project/instance"
import { trimDiff } from "./edit"
import { assertExternalDirectoryEffect } from "./external-directory"
import { relative } from "./relative"

const MAX_PROJECT_DIAGNOSTICS_FILES = 5

Expand All @@ -39,11 +40,11 @@ export const WriteTool = Tool.define(

const exists = yield* fs.existsSafe(filepath)
const contentOld = exists ? yield* fs.readFileString(filepath) : ""

const rel = relative(filepath)
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content))
yield* ctx.ask({
permission: "edit",
patterns: [path.relative(Instance.worktree, filepath)],
patterns: [rel],
always: ["*"],
metadata: {
filepath,
Expand Down Expand Up @@ -78,7 +79,7 @@ export const WriteTool = Tool.define(
}

return {
title: path.relative(Instance.worktree, filepath),
title: rel,
metadata: {
diagnostics,
filepath,
Expand Down
18 changes: 18 additions & 0 deletions packages/opencode/test/tool/apply_patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,24 @@ describe("tool.apply_patch freeform", () => {
})
})

test("uses directory-relative permission paths in non-git projects", async () => {
await using fixture = await tmpdir()
const { ctx, calls } = makeCtx()

await Instance.provide({
directory: fixture.path,
fn: async () => {
const patchText = "*** Begin Patch\n*** Add File: .agents/new.txt\n+created\n*** End Patch"

await execute({ patchText }, ctx)

expect(calls).toHaveLength(1)
expect(calls[0]?.patterns).toEqual([".agents/new.txt"])
expect(calls[0]?.metadata.files[0]?.relativePath).toBe(".agents/new.txt")
},
})
})

test("applies multiple hunks to one file", async () => {
await using fixture = await tmpdir()
const { ctx } = makeCtx()
Expand Down
32 changes: 32 additions & 0 deletions packages/opencode/test/tool/edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,38 @@ describe("tool.edit", () => {
})
})

test("uses directory-relative permission paths in non-git projects", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, ".agents", "file.txt")
const calls: Array<{ patterns: readonly string[] }> = []

await Instance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "",
newString: "new content",
},
{
...ctx,
ask: (input) =>
Effect.sync(() => {
calls.push({ patterns: input.patterns })
}),
},
),
)

expect(calls).toHaveLength(1)
expect(calls[0]?.patterns).toEqual([path.join(".agents", "file.txt")])
},
})
})

test("emits add event for new files", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "new.txt")
Expand Down
15 changes: 15 additions & 0 deletions packages/opencode/test/tool/read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ const asks = () => {
}

describe("tool.read external_directory permission", () => {
it.live("uses directory-relative read permission paths in non-git projects", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
yield* put(path.join(dir, ".agents", "test.txt"), "hello world")

const { items, next } = asks()

const result = yield* exec(dir, { filePath: path.join(dir, ".agents", "test.txt") }, next)
expect(result.output).toContain("hello world")
const read = items.find((item) => item.permission === "read")
expect(read).toBeDefined()
expect(read!.patterns).toEqual([path.join(".agents", "test.txt")])
}),
)

it.live("allows reading absolute path inside project directory", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
Expand Down
26 changes: 26 additions & 0 deletions packages/opencode/test/tool/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,32 @@ describe("tool.write", () => {
}),
),
)

it.live("uses directory-relative permission paths in non-git projects", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
const filepath = path.join(dir, ".agents", "file.txt")
const calls: Array<{ patterns: readonly string[] }> = []

yield* run(
{
filePath: filepath,
content: "content",
},
{
...ctx,
ask: (input) =>
Effect.sync(() => {
calls.push({ patterns: input.patterns })
}),
},
)

expect(calls).toHaveLength(1)
expect(calls[0]?.patterns).toEqual([path.join(".agents", "file.txt")])
}),
),
)
})

describe("existing file overwrite", () => {
Expand Down
Loading