diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts index 0efeff544f6..b87ad555286 100644 --- a/packages/opencode/src/patch/index.ts +++ b/packages/opencode/src/patch/index.ts @@ -79,23 +79,23 @@ export namespace Patch { const line = lines[startIdx] if (line.startsWith("*** Add File:")) { - const filePath = line.split(":", 2)[1]?.trim() + const filePath = line.slice("*** Add File:".length).trim() return filePath ? { filePath, nextIdx: startIdx + 1 } : null } if (line.startsWith("*** Delete File:")) { - const filePath = line.split(":", 2)[1]?.trim() + const filePath = line.slice("*** Delete File:".length).trim() return filePath ? { filePath, nextIdx: startIdx + 1 } : null } if (line.startsWith("*** Update File:")) { - const filePath = line.split(":", 2)[1]?.trim() + const filePath = line.slice("*** Update File:".length).trim() let movePath: string | undefined let nextIdx = startIdx + 1 // Check for move directive if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) { - movePath = lines[nextIdx].split(":", 2)[1]?.trim() + movePath = lines[nextIdx].slice("*** Move to:".length).trim() nextIdx++ } diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index a1c2b57812e..c9a8f601c09 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -105,7 +105,7 @@ export namespace Snapshot { .split("\n") .map((x) => x.trim()) .filter(Boolean) - .map((x) => path.join(Instance.worktree, x)), + .map((x) => path.join(Instance.worktree, x).replaceAll("\\", "/")), } } diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index 1344467c719..06293b6eba6 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -161,7 +161,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", { // 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), + relativePath: path.relative(Instance.worktree, change.movePath ?? change.filePath).replaceAll("\\", "/"), type: change.type, diff: change.diff, before: change.oldContent, @@ -172,7 +172,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", { })) // Check permissions if needed - const relativePaths = fileChanges.map((c) => path.relative(Instance.worktree, c.filePath)) + const relativePaths = fileChanges.map((c) => path.relative(Instance.worktree, c.filePath).replaceAll("\\", "/")) await ctx.ask({ permission: "edit", patterns: relativePaths, @@ -242,13 +242,13 @@ export const ApplyPatchTool = Tool.define("apply_patch", { // Generate output summary const summaryLines = fileChanges.map((change) => { if (change.type === "add") { - return `A ${path.relative(Instance.worktree, change.filePath)}` + return `A ${path.relative(Instance.worktree, change.filePath).replaceAll("\\", "/")}` } if (change.type === "delete") { - return `D ${path.relative(Instance.worktree, change.filePath)}` + return `D ${path.relative(Instance.worktree, change.filePath).replaceAll("\\", "/")}` } const target = change.movePath ?? change.filePath - return `M ${path.relative(Instance.worktree, target)}` + return `M ${path.relative(Instance.worktree, target).replaceAll("\\", "/")}` }) let output = `Success. Updated the following files:\n${summaryLines.join("\n")}` @@ -264,7 +264,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", { const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE) const suffix = errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : "" - output += `\n\nLSP errors detected in ${path.relative(Instance.worktree, target)}, please fix:\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n` + output += `\n\nLSP errors detected in ${path.relative(Instance.worktree, target).replaceAll("\\", "/")}, please fix:\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n` } } diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 67559b78c08..6c9a7859d8d 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -145,7 +145,11 @@ export const BashTool = Tool.define("bash", async () => { } if (directories.size > 0) { - const globs = Array.from(directories).map((dir) => path.join(dir, "*")) + const globs = Array.from(directories).map((dir) => { + // Preserve POSIX-looking paths with /s, even on Windows + if (dir.startsWith("/")) return `${dir.replace(/[\\/]+$/, "")}/*` + return path.join(dir, "*") + }) await ctx.ask({ permission: "external_directory", patterns: globs, diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index c310256c5e7..2264723a090 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -50,7 +50,7 @@ Instructions here. const testSkill = skills.find((s) => s.name === "test-skill") expect(testSkill).toBeDefined() expect(testSkill!.description).toBe("A test skill for verification.") - expect(testSkill!.location).toContain("skill/test-skill/SKILL.md") + expect(testSkill!.location).toContain(path.join("skill", "test-skill", "SKILL.md")) }, }) }) @@ -180,7 +180,7 @@ description: A skill in the .claude/skills directory. expect(skills.length).toBe(1) const claudeSkill = skills.find((s) => s.name === "claude-skill") expect(claudeSkill).toBeDefined() - expect(claudeSkill!.location).toContain(".claude/skills/claude-skill/SKILL.md") + expect(claudeSkill!.location).toContain(path.join(".claude", "skills", "claude-skill", "SKILL.md")) }, }) }) @@ -200,7 +200,7 @@ test("discovers global skills from ~/.claude/skills/ directory", async () => { expect(skills.length).toBe(1) expect(skills[0].name).toBe("global-test-skill") expect(skills[0].description).toBe("A global skill from ~/.claude/skills for testing.") - expect(skills[0].location).toContain(".claude/skills/global-test-skill/SKILL.md") + expect(skills[0].location).toContain(path.join(".claude", "skills", "global-test-skill", "SKILL.md")) }, }) } finally { @@ -245,7 +245,7 @@ description: A skill in the .agents/skills directory. expect(skills.length).toBe(1) const agentSkill = skills.find((s) => s.name === "agent-skill") expect(agentSkill).toBeDefined() - expect(agentSkill!.location).toContain(".agents/skills/agent-skill/SKILL.md") + expect(agentSkill!.location).toContain(path.join(".agents", "skills", "agent-skill", "SKILL.md")) }, }) }) @@ -279,7 +279,7 @@ This skill is loaded from the global home directory. expect(skills.length).toBe(1) expect(skills[0].name).toBe("global-agent-skill") expect(skills[0].description).toBe("A global skill from ~/.agents/skills for testing.") - expect(skills[0].location).toContain(".agents/skills/global-agent-skill/SKILL.md") + expect(skills[0].location).toContain(path.join(".agents", "skills", "global-agent-skill", "SKILL.md")) }, }) } finally { diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts index a08e235885a..f81723fee09 100644 --- a/packages/opencode/test/tool/apply_patch.test.ts +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -93,6 +93,13 @@ describe("tool.apply_patch freeform", () => { expect(result.title).toContain("Success. Updated the following files") expect(result.output).toContain("Success. Updated the following files") + // Strict formatting assertions for slashes + expect(result.output).toMatch(/A nested\/new\.txt/) + expect(result.output).toMatch(/D delete\.txt/) + expect(result.output).toMatch(/M modify\.txt/) + if (process.platform === "win32") { + expect(result.output).not.toContain("\\") + } expect(result.metadata.diff).toContain("Index:") expect(calls.length).toBe(1)