Просмотр исходного кода

fix(app): query selector with non-latin chars

adamelmore 3 недель назад
Родитель
Сommit
7c34319b19

+ 57 - 1
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)

+ 1 - 1
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]

+ 7 - 3
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()

+ 68 - 1
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<Snapshot.FileDiff[]>(["session_diff", input.sessionID]).catch(() => [])
+      const diffs = await Storage.read<Snapshot.FileDiff[]>(["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
     },
   )
 

+ 2 - 2
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<FileDiff[]> {
     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()