From 79db69d9e5b507c21cbbfd83cc235ac61c7b7bdb Mon Sep 17 00:00:00 2001 From: "tobias@tobias-weiss.org" Date: Sat, 11 Apr 2026 07:22:54 +0200 Subject: [PATCH] fix(windows): normalize backslash paths to posix in tool titles/patterns On Windows, path.relative() returns backslash-separated paths which look wrong in tool call titles and may cause issues in patterns arrays. Add toPosix() helper to convert \\ to / in edit, write, read, glob, ls, multiedit, lsp, and plan tools wherever paths appear in titles or permission patterns. --- packages/opencode/src/tool/edit.ts | 10 +++++++--- packages/opencode/src/tool/glob.ts | 6 +++++- packages/opencode/src/tool/ls.ts | 6 +++++- packages/opencode/src/tool/lsp.ts | 6 +++++- packages/opencode/src/tool/multiedit.ts | 6 +++++- packages/opencode/src/tool/plan.ts | 6 +++++- packages/opencode/src/tool/read.ts | 6 +++++- packages/opencode/src/tool/write.ts | 8 ++++++-- 8 files changed, 43 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index a835714c69c5..79451d7daa9f 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -17,6 +17,10 @@ import { Format } from "../format" import { FileTime } from "../file/time" import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" + +function toPosix(p: string): string { + return p.replaceAll("\\", "/") +} import { Snapshot } from "@/snapshot" import { assertExternalDirectoryEffect } from "./external-directory" import { AppFileSystem } from "../filesystem" @@ -79,7 +83,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: [toPosix(path.relative(Instance.worktree, filePath))], always: ["*"], metadata: { filepath: filePath, @@ -119,7 +123,7 @@ export const EditTool = Tool.define( ) yield* ctx.ask({ permission: "edit", - patterns: [path.relative(Instance.worktree, filePath)], + patterns: [toPosix(path.relative(Instance.worktree, filePath))], always: ["*"], metadata: { filepath: filePath, @@ -179,7 +183,7 @@ export const EditTool = Tool.define( diff, filediff, }, - title: `${path.relative(Instance.worktree, filePath)}`, + title: toPosix(path.relative(Instance.worktree, filePath)), output, } }), diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index a3ff5aef71be..6ecfcb3c3c60 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -9,6 +9,10 @@ import { Instance } from "../project/instance" import { assertExternalDirectoryEffect } from "./external-directory" import { AppFileSystem } from "../filesystem" +function toPosix(p: string): string { + return p.replaceAll("\\", "/") +} + export const GlobTool = Tool.define( "glob", Effect.gen(function* () { @@ -81,7 +85,7 @@ export const GlobTool = Tool.define( } return { - title: path.relative(Instance.worktree, search), + title: toPosix(path.relative(Instance.worktree, search)), metadata: { count: files.length, truncated, diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts index 600a5532aa65..6fe8ab9c4ac2 100644 --- a/packages/opencode/src/tool/ls.ts +++ b/packages/opencode/src/tool/ls.ts @@ -8,6 +8,10 @@ import { Instance } from "../project/instance" import { Ripgrep } from "../file/ripgrep" import { assertExternalDirectoryEffect } from "./external-directory" +function toPosix(p: string): string { + return p.replaceAll("\\", "/") +} + export const IGNORE_PATTERNS = [ "node_modules/", "__pycache__/", @@ -121,7 +125,7 @@ export const ListTool = Tool.define( const output = `${searchPath}/\n` + renderDir(".", 0) return { - title: path.relative(Instance.worktree, searchPath), + title: toPosix(path.relative(Instance.worktree, searchPath)), metadata: { count: files.length, truncated: files.length >= LIMIT, diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index c5a5d6f8197e..f94cf945f609 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -9,6 +9,10 @@ import { pathToFileURL } from "url" import { assertExternalDirectoryEffect } from "./external-directory" import { AppFileSystem } from "../filesystem" +function toPosix(p: string): string { + return p.replaceAll("\\", "/") +} + const operations = [ "goToDefinition", "findReferences", @@ -46,7 +50,7 @@ export const LspTool = Tool.define( const uri = pathToFileURL(file).href const position = { file, line: args.line - 1, character: args.character - 1 } - const relPath = path.relative(Instance.worktree, file) + const relPath = toPosix(path.relative(Instance.worktree, file)) const title = `${args.operation} ${relPath}:${args.line}:${args.character}` const exists = yield* fs.existsSafe(file) diff --git a/packages/opencode/src/tool/multiedit.ts b/packages/opencode/src/tool/multiedit.ts index 449df33430eb..90f39f013371 100644 --- a/packages/opencode/src/tool/multiedit.ts +++ b/packages/opencode/src/tool/multiedit.ts @@ -6,6 +6,10 @@ import DESCRIPTION from "./multiedit.txt" import path from "path" import { Instance } from "../project/instance" +function toPosix(p: string): string { + return p.replaceAll("\\", "/") +} + export const MultiEditTool = Tool.define( "multiedit", Effect.gen(function* () { @@ -49,7 +53,7 @@ export const MultiEditTool = Tool.define( results.push(result) } return { - title: path.relative(Instance.worktree, params.filePath), + title: toPosix(path.relative(Instance.worktree, params.filePath)), metadata: { results: results.map((r) => r.metadata), }, diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts index 1613821fe0bd..94ece0973b0c 100644 --- a/packages/opencode/src/tool/plan.ts +++ b/packages/opencode/src/tool/plan.ts @@ -10,6 +10,10 @@ import { Instance } from "../project/instance" import { type SessionID, MessageID, PartID } from "../session/schema" import EXIT_DESCRIPTION from "./plan-exit.txt" +function toPosix(p: string): string { + return p.replaceAll("\\", "/") +} + function getLastModel(sessionID: SessionID) { for (const item of MessageV2.stream(sessionID)) { if (item.info.role === "user" && item.info.model) return item.info.model @@ -30,7 +34,7 @@ export const PlanExitTool = Tool.define( execute: (_params: {}, ctx: Tool.Context) => Effect.gen(function* () { const info = yield* session.get(ctx.sessionID) - const plan = path.relative(Instance.worktree, Session.plan(info)) + const plan = toPosix(path.relative(Instance.worktree, Session.plan(info))) const answers = yield* question.ask({ sessionID: ctx.sessionID, questions: [ diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 501a8c97ed3f..7a1d29c2def5 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -13,6 +13,10 @@ import { Instance } from "../project/instance" import { assertExternalDirectoryEffect } from "./external-directory" import { Instruction } from "../session/instruction" +function toPosix(p: string): string { + return p.replaceAll("\\", "/") +} + const DEFAULT_READ_LIMIT = 2000 const MAX_LINE_LENGTH = 2000 const MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)` @@ -92,7 +96,7 @@ export const ReadTool = Tool.define( if (process.platform === "win32") { filepath = AppFileSystem.normalizePath(filepath) } - const title = path.relative(Instance.worktree, filepath) + const title = toPosix(path.relative(Instance.worktree, filepath)) const stat = yield* fs.stat(filepath).pipe( Effect.catchIf( diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 7a9d82cf8b05..77571c7eb790 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -17,6 +17,10 @@ import { assertExternalDirectoryEffect } from "./external-directory" const MAX_PROJECT_DIAGNOSTICS_FILES = 5 +function toPosix(p: string): string { + return p.replaceAll("\\", "/") +} + export const WriteTool = Tool.define( "write", Effect.gen(function* () { @@ -46,7 +50,7 @@ export const WriteTool = Tool.define( const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content)) yield* ctx.ask({ permission: "edit", - patterns: [path.relative(Instance.worktree, filepath)], + patterns: [toPosix(path.relative(Instance.worktree, filepath))], always: ["*"], metadata: { filepath, @@ -82,7 +86,7 @@ export const WriteTool = Tool.define( } return { - title: path.relative(Instance.worktree, filepath), + title: toPosix(path.relative(Instance.worktree, filepath)), metadata: { diagnostics, filepath,