summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-05 15:11:55 -0600
committerAdam <[email protected]>2026-01-05 15:12:02 -0600
commit2ca0ae77557ae759f8463f82f67df4a132f5f749 (patch)
tree3dbf26803d41c8140eeb2ce1f7d7076429fe59e6 /packages
parent19123b6803d1720d83dfd2388b0396971ddb179e (diff)
downloadopencode-2ca0ae77557ae759f8463f82f67df4a132f5f749.tar.gz
opencode-2ca0ae77557ae759f8463f82f67df4a132f5f749.zip
fix(app): more defensive, handle no git
Diffstat (limited to 'packages')
-rw-r--r--packages/app/src/pages/session.tsx2
-rw-r--r--packages/app/src/utils/prompt.ts41
-rw-r--r--packages/opencode/src/file/watcher.ts1
-rw-r--r--packages/opencode/src/project/project.ts59
-rw-r--r--packages/opencode/src/snapshot/index.ts6
-rw-r--r--packages/opencode/src/util/filesystem.ts4
-rw-r--r--packages/ui/src/components/session-review.tsx4
7 files changed, 94 insertions, 23 deletions
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 1a967759d..a0de9021c 100644
--- a/packages/app/src/pages/session.tsx
+++ b/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)
diff --git a/packages/app/src/utils/prompt.ts b/packages/app/src/utils/prompt.ts
index 29a774c2a..5d9edfed1 100644
--- a/packages/app/src/utils/prompt.ts
+++ b/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))
diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts
index 334b2d264..44f8a0a3a 100644
--- a/packages/opencode/src/file/watcher.ts
+++ b/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")
diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts
index 8b78553bf..ea59f991e 100644
--- a/packages/opencode/src/project/project.ts
+++ b/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,
diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts
index 0bbb1115e..69f2abc79 100644
--- a/packages/opencode/src/snapshot/index.ts
+++ b/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
diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts
index 98fbe533d..472bff83d 100644
--- a/packages/opencode/src/util/filesystem.ts
+++ b/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)
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index 275077a58..e11df6c9f 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/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>