diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 20fc980f6..afde451ff 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -57,6 +57,62 @@ function stripQueryAndHash(input: string) { return input } +function unquoteGitPath(input: string) { + if (!input.startsWith('"')) return input + if (!input.endsWith('"')) return input + const body = input.slice(1, -1) + const bytes: number[] = [] + + for (let i = 0; i < body.length; i++) { + const char = body[i]! + if (char !== "\\") { + bytes.push(char.charCodeAt(0)) + continue + } + + const next = body[i + 1] + if (!next) { + bytes.push("\\".charCodeAt(0)) + continue + } + + if (next >= "0" && next <= "7") { + const chunk = body.slice(i + 1, i + 4) + const match = chunk.match(/^[0-7]{1,3}/) + if (!match) { + bytes.push(next.charCodeAt(0)) + i++ + continue + } + bytes.push(parseInt(match[0], 8)) + i += match[0].length + continue + } + + const escaped = + next === "n" + ? "\n" + : next === "r" + ? "\r" + : next === "t" + ? "\t" + : next === "b" + ? "\b" + : next === "f" + ? "\f" + : next === "v" + ? "\v" + : next === "\\" || next === '"' + ? next + : undefined + + bytes.push((escaped ?? next).charCodeAt(0)) + i++ + } + + return new TextDecoder().decode(new Uint8Array(bytes)) +} + export function selectionFromLines(range: SelectedLineRange): FileSelection { const startLine = Math.min(range.start, range.end) const endLine = Math.max(range.start, range.end) @@ -197,7 +253,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ const root = directory() const prefix = root.endsWith("/") ? root : root + "/" - let path = stripQueryAndHash(stripFileProtocol(input)) + let path = unquoteGitPath(stripQueryAndHash(stripFileProtocol(input))) if (path.startsWith(prefix)) { path = path.slice(prefix.length) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index bc89b2a48..5e5cba69c 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1016,7 +1016,7 @@ export default function Page() { const activeTab = createMemo(() => { const active = tabs().active() - if (active) return active + if (active) return normalizeTab(active) if (hasReview()) return "review" const first = openedTabs()[0] diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 76b7be4b7..dfa6356a2 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -206,7 +206,11 @@ export namespace File { const project = Instance.project if (project.vcs !== "git") return [] - const diffOutput = await $`git diff --numstat HEAD`.cwd(Instance.directory).quiet().nothrow().text() + const diffOutput = await $`git -c core.quotepath=false diff --numstat HEAD` + .cwd(Instance.directory) + .quiet() + .nothrow() + .text() const changedFiles: Info[] = [] @@ -223,7 +227,7 @@ export namespace File { } } - const untrackedOutput = await $`git ls-files --others --exclude-standard` + const untrackedOutput = await $`git -c core.quotepath=false ls-files --others --exclude-standard` .cwd(Instance.directory) .quiet() .nothrow() @@ -248,7 +252,7 @@ export namespace File { } // Get deleted files - const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD` + const deletedOutput = await $`git -c core.quotepath=false diff --name-only --diff-filter=D HEAD` .cwd(Instance.directory) .quiet() .nothrow() diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 611d5f1c6..91a520a9b 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -20,6 +20,62 @@ import { Agent } from "@/agent/agent" export namespace SessionSummary { const log = Log.create({ service: "session.summary" }) + function unquoteGitPath(input: string) { + if (!input.startsWith('"')) return input + if (!input.endsWith('"')) return input + const body = input.slice(1, -1) + const bytes: number[] = [] + + for (let i = 0; i < body.length; i++) { + const char = body[i]! + if (char !== "\\") { + bytes.push(char.charCodeAt(0)) + continue + } + + const next = body[i + 1] + if (!next) { + bytes.push("\\".charCodeAt(0)) + continue + } + + if (next >= "0" && next <= "7") { + const chunk = body.slice(i + 1, i + 4) + const match = chunk.match(/^[0-7]{1,3}/) + if (!match) { + bytes.push(next.charCodeAt(0)) + i++ + continue + } + bytes.push(parseInt(match[0], 8)) + i += match[0].length + continue + } + + const escaped = + next === "n" + ? "\n" + : next === "r" + ? "\r" + : next === "t" + ? "\t" + : next === "b" + ? "\b" + : next === "f" + ? "\f" + : next === "v" + ? "\v" + : next === "\\" || next === '"' + ? next + : undefined + + bytes.push((escaped ?? next).charCodeAt(0)) + i++ + } + + return Buffer.from(bytes).toString() + } + export const summarize = fn( z.object({ sessionID: z.string(), @@ -116,7 +172,18 @@ export namespace SessionSummary { messageID: Identifier.schema("message").optional(), }), async (input) => { - return Storage.read(["session_diff", input.sessionID]).catch(() => []) + const diffs = await Storage.read(["session_diff", input.sessionID]).catch(() => []) + const next = diffs.map((item) => { + const file = unquoteGitPath(item.file) + if (file === item.file) return item + return { + ...item, + file, + } + }) + const changed = next.some((item, i) => item.file !== diffs[i]?.file) + if (changed) Storage.write(["session_diff", input.sessionID], next).catch(() => {}) + return next }, ) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 135bd0944..1c1539090 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -163,7 +163,7 @@ export namespace Snapshot { const git = gitdir() await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() const result = - await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .` + await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .` .quiet() .cwd(Instance.worktree) .nothrow() @@ -196,7 +196,7 @@ export namespace Snapshot { export async function diffFull(from: string, to: string): Promise { const git = gitdir() const result: FileDiff[] = [] - for await (const line of $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .` + for await (const line of $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .` .quiet() .cwd(Instance.directory) .nothrow()