diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 5ea499387..d7630509a 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -195,7 +195,20 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ const root = directory() const prefix = root.endsWith("/") ? root : root + "/" - let path = stripQueryAndHash(stripFileProtocol(input)) + let path = input + + // Only strip protocol and decode if it's a file URI + if (path.startsWith("file://")) { + const raw = stripQueryAndHash(stripFileProtocol(path)) + try { + // Attempt to treat as a standard URI + path = decodeURIComponent(raw) + } catch { + // Fallback for legacy paths that might contain invalid URI sequences (e.g. "100%") + // In this case, we treat the path as raw, but still strip the protocol + path = raw + } + } if (path.startsWith(prefix)) { path = path.slice(prefix.length) @@ -218,7 +231,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ function tab(input: string) { const path = normalize(input) - return `file://${path}` + const encoded = path.split("/").map(encodeURIComponent).join("/") + return `file://${encoded}` } function pathFromTab(tabValue: string) { diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 46c97cf8d..5a198434b 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -104,6 +104,7 @@ export namespace Snapshot { .split("\n") .map((x) => x.trim()) .filter(Boolean) + .map((x) => unquote(x)) .map((x) => path.join(Instance.worktree, x)), } } @@ -151,7 +152,7 @@ export namespace Snapshot { }) } else { log.info("file did not exist in snapshot, deleting", { file }) - await fs.unlink(file).catch(() => {}) + await fs.unlink(file).catch(() => { }) } } files.add(file) @@ -202,20 +203,22 @@ export namespace Snapshot { .nothrow() .lines()) { if (!line) continue - const [additions, deletions, file] = line.split("\t") + const [additions, deletions, rawFile] = line.split("\t") + const file = unquote(rawFile) const isBinaryFile = additions === "-" && deletions === "-" - const before = isBinaryFile - ? "" + const beforeResult = isBinaryFile + ? { exitCode: 0, text: () => "", stderr: Buffer.from("") } : await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}` - .quiet() - .nothrow() - .text() - const after = isBinaryFile - ? "" + .quiet() + .nothrow() + const before = beforeResult.exitCode === 0 ? beforeResult.text() : `[DEBUG ERROR] git show ${from}:${file} failed: ${beforeResult.stderr.toString()}` + + const afterResult = isBinaryFile + ? { exitCode: 0, text: () => "", stderr: Buffer.from("") } : await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}` - .quiet() - .nothrow() - .text() + .quiet() + .nothrow() + const after = afterResult.exitCode === 0 ? afterResult.text() : `[DEBUG ERROR] git show ${to}:${file} failed: ${afterResult.stderr.toString()}` const added = isBinaryFile ? 0 : parseInt(additions) const deleted = isBinaryFile ? 0 : parseInt(deletions) result.push({ @@ -229,6 +232,69 @@ export namespace Snapshot { return result } + export function unquote(path: string): string { + // If the path is wrapped in quotes, it might contain octal escapes + if (path.startsWith('"') && path.endsWith('"')) { + const quoted = path.slice(1, -1) + // Decode escaped characters + const buffer: number[] = [] + for (let i = 0; i < quoted.length; i++) { + if (quoted[i] === "\\") { + i++ + // Check for octal escape (e.g. \344) + if (i + 2 < quoted.length && /^[0-7]{3}$/.test(quoted.slice(i, i + 3))) { + const octal = quoted.slice(i, i + 3) + buffer.push(parseInt(octal, 8)) + i += 2 + } else { + // Handle standard escapes + switch (quoted[i]) { + case "b": + buffer.push(8) + break + case "t": + buffer.push(9) + break + case "n": + buffer.push(10) + break + case "v": + buffer.push(11) + break + case "f": + buffer.push(12) + break + case "r": + buffer.push(13) + break + case '"': + buffer.push(34) + break + case "\\": + buffer.push(92) + break + default: + // If unknown escape, keep original (or char code of escaped char) + buffer.push(quoted.charCodeAt(i)) + } + } + } else { + const charCode = quoted.charCodeAt(i) + if (charCode < 128) { + buffer.push(charCode) + } else { + const charBuffer = Buffer.from(quoted[i]) + for (const byte of charBuffer) { + buffer.push(byte) + } + } + } + } + return Buffer.from(buffer).toString("utf8") + } + return path + } + function gitdir() { const project = Instance.project return path.join(Global.Path.data, "snapshot", project.id)