Răsfoiți Sursa

fix(app): more defensive, handle no git

Adam 1 lună în urmă
părinte
comite
2ca0ae7755

+ 1 - 1
packages/app/src/pages/session.tsx

@@ -502,7 +502,7 @@ export default function Page() {
         // Restore the prompt from the reverted message
         const parts = sync.data.part[message.id]
         if (parts) {
-          const restored = extractPromptFromParts(parts)
+          const restored = extractPromptFromParts(parts, { directory: sdk.directory })
           prompt.set(restored)
         }
         // Navigate to the message before the reverted one (which will be the new last visible message)

+ 29 - 12
packages/app/src/utils/prompt.ts

@@ -53,9 +53,25 @@ function textPartValue(parts: Part[]) {
  * Extract prompt content from message parts for restoring into the prompt input.
  * This is used by undo to restore the original user prompt.
  */
-export function extractPromptFromParts(parts: Part[]): Prompt {
+export function extractPromptFromParts(parts: Part[], opts?: { directory?: string }): Prompt {
   const textPart = textPartValue(parts)
   const text = textPart?.text ?? ""
+  const directory = opts?.directory
+
+  const toRelative = (path: string) => {
+    if (!directory) return path
+
+    const prefix = directory.endsWith("/") ? directory : directory + "/"
+    if (path.startsWith(prefix)) return path.slice(prefix.length)
+
+    if (path.startsWith(directory)) {
+      const next = path.slice(directory.length)
+      if (next.startsWith("/")) return next.slice(1)
+      return next
+    }
+
+    return path
+  }
 
   const inline: Inline[] = []
   const images: ImageAttachmentPart[] = []
@@ -78,7 +94,7 @@ export function extractPromptFromParts(parts: Part[]): Prompt {
           start,
           end,
           value,
-          path,
+          path: toRelative(path),
           selection: selectionFromFileUrl(filePart.url),
         })
         continue
@@ -158,20 +174,21 @@ export function extractPromptFromParts(parts: Part[]): Prompt {
 
   for (const item of inline) {
     if (item.start < 0 || item.end < item.start) continue
-    if (item.end > text.length) continue
-    if (item.start < cursor) continue
 
-    pushText(text.slice(cursor, item.start))
+    const expected = item.value
+    if (!expected) continue
 
-    if (item.type === "file") {
-      pushFile(item)
-    }
+    const mismatch = item.end > text.length || item.start < cursor || text.slice(item.start, item.end) !== expected
+    const start = mismatch ? text.indexOf(expected, cursor) : item.start
+    if (start === -1) continue
+    const end = mismatch ? start + expected.length : item.end
 
-    if (item.type === "agent") {
-      pushAgent(item)
-    }
+    pushText(text.slice(cursor, start))
+
+    if (item.type === "file") pushFile(item)
+    if (item.type === "agent") pushAgent(item)
 
-    cursor = item.end
+    cursor = end
   }
 
   pushText(text.slice(cursor))

+ 1 - 0
packages/opencode/src/file/watcher.ts

@@ -85,6 +85,7 @@ export namespace FileWatcher {
         .cwd(Instance.worktree)
         .text()
         .then((x) => path.resolve(Instance.worktree, x.trim()))
+        .catch(() => undefined)
       if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
         const gitDirContents = await readdir(vcsDir).catch(() => [])
         const ignoreList = gitDirContents.filter((entry) => entry !== "HEAD")

+ 55 - 4
packages/opencode/src/project/project.ts

@@ -53,11 +53,22 @@ export namespace Project {
       if (git) {
         let sandbox = path.dirname(git)
 
+        const gitBinary = Bun.which("git")
+
         // cached id calculation
         let id = await Bun.file(path.join(git, "opencode"))
           .text()
           .then((x) => x.trim())
-          .catch(() => {})
+          .catch(() => undefined)
+
+        if (!gitBinary) {
+          return {
+            id: id ?? "global",
+            worktree: sandbox,
+            sandbox: sandbox,
+            vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
+          }
+        }
 
         // generate id from root commit
         if (!id) {
@@ -73,24 +84,53 @@ export namespace Project {
                 .map((x) => x.trim())
                 .toSorted(),
             )
+            .catch(() => undefined)
+
+          if (!roots) {
+            return {
+              id: "global",
+              worktree: sandbox,
+              sandbox: sandbox,
+              vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
+            }
+          }
+
           id = roots[0]
-          if (id) Bun.file(path.join(git, "opencode")).write(id)
+          if (id) {
+            void Bun.file(path.join(git, "opencode"))
+              .write(id)
+              .catch(() => undefined)
+          }
         }
 
-        if (!id)
+        if (!id) {
           return {
             id: "global",
             worktree: sandbox,
             sandbox: sandbox,
             vcs: "git",
           }
+        }
 
-        sandbox = await $`git rev-parse --show-toplevel`
+        const top = await $`git rev-parse --show-toplevel`
           .quiet()
           .nothrow()
           .cwd(sandbox)
           .text()
           .then((x) => path.resolve(sandbox, x.trim()))
+          .catch(() => undefined)
+
+        if (!top) {
+          return {
+            id,
+            sandbox,
+            worktree: sandbox,
+            vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
+          }
+        }
+
+        sandbox = top
+
         const worktree = await $`git rev-parse --git-common-dir`
           .quiet()
           .nothrow()
@@ -101,6 +141,17 @@ export namespace Project {
             if (dirname === ".") return sandbox
             return dirname
           })
+          .catch(() => undefined)
+
+        if (!worktree) {
+          return {
+            id,
+            sandbox,
+            worktree: sandbox,
+            vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
+          }
+        }
+
         return {
           id,
           sandbox,

+ 4 - 2
packages/opencode/src/snapshot/index.ts

@@ -179,12 +179,14 @@ export namespace Snapshot {
             .quiet()
             .nothrow()
             .text()
+      const added = isBinaryFile ? 0 : parseInt(additions)
+      const deleted = isBinaryFile ? 0 : parseInt(deletions)
       result.push({
         file,
         before,
         after,
-        additions: parseInt(additions),
-        deletions: parseInt(deletions),
+        additions: Number.isFinite(added) ? added : 0,
+        deletions: Number.isFinite(deleted) ? deleted : 0,
       })
     }
     return result

+ 2 - 2
packages/opencode/src/util/filesystem.ts

@@ -31,7 +31,7 @@ export namespace Filesystem {
     const result = []
     while (true) {
       const search = join(current, target)
-      if (await exists(search)) result.push(search)
+      if (await exists(search).catch(() => false)) result.push(search)
       if (stop === current) break
       const parent = dirname(current)
       if (parent === current) break
@@ -46,7 +46,7 @@ export namespace Filesystem {
     while (true) {
       for (const target of targets) {
         const search = join(current, target)
-        if (await exists(search)) yield search
+        if (await exists(search).catch(() => false)) yield search
       }
       if (stop === current) break
       const parent = dirname(current)

+ 2 - 2
packages/ui/src/components/session-review.tsx

@@ -123,11 +123,11 @@ export const SessionReview = (props: SessionReviewProps) => {
                     diffStyle={diffStyle()}
                     before={{
                       name: diff.file!,
-                      contents: diff.before!,
+                      contents: typeof diff.before === "string" ? diff.before : "",
                     }}
                     after={{
                       name: diff.file!,
-                      contents: diff.after!,
+                      contents: typeof diff.after === "string" ? diff.after : "",
                     }}
                   />
                 </Accordion.Content>