summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-02 10:55:15 -0600
committerAdam <[email protected]>2026-02-02 14:24:23 -0600
commit3b93e8d95cfc30d1a85fbb76694bdb7f49dff1e9 (patch)
tree1d3c5b40c80f64de6bb5fc26870dd1f2f6367e35
parent23631a93935a33fb8e44272ba1572e3475a223c2 (diff)
downloadopencode-3b93e8d95cfc30d1a85fbb76694bdb7f49dff1e9.tar.gz
opencode-3b93e8d95cfc30d1a85fbb76694bdb7f49dff1e9.zip
fix(app): added/deleted file status now correctly calculated
-rw-r--r--packages/app/src/components/file-tree.tsx49
-rw-r--r--packages/app/src/pages/session.tsx4
-rw-r--r--packages/opencode/src/snapshot/index.ts19
-rw-r--r--packages/opencode/test/snapshot/snapshot.test.ts46
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts1
5 files changed, 115 insertions, 4 deletions
diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx
index d43310b19..19f5e9a3b 100644
--- a/packages/app/src/components/file-tree.tsx
+++ b/packages/app/src/components/file-tree.tsx
@@ -130,10 +130,57 @@ export default function FileTree(props: {
const nodes = file.tree.children(props.path)
const current = filter()
if (!current) return nodes
- return nodes.filter((node) => {
+
+ const parent = (path: string) => {
+ const idx = path.lastIndexOf("/")
+ if (idx === -1) return ""
+ return path.slice(0, idx)
+ }
+
+ const leaf = (path: string) => {
+ const idx = path.lastIndexOf("/")
+ return idx === -1 ? path : path.slice(idx + 1)
+ }
+
+ const out = nodes.filter((node) => {
if (node.type === "file") return current.files.has(node.path)
return current.dirs.has(node.path)
})
+
+ const seen = new Set(out.map((node) => node.path))
+
+ for (const dir of current.dirs) {
+ if (parent(dir) !== props.path) continue
+ if (seen.has(dir)) continue
+ out.push({
+ name: leaf(dir),
+ path: dir,
+ absolute: dir,
+ type: "directory",
+ ignored: false,
+ })
+ seen.add(dir)
+ }
+
+ for (const item of current.files) {
+ if (parent(item) !== props.path) continue
+ if (seen.has(item)) continue
+ out.push({
+ name: leaf(item),
+ path: item,
+ absolute: item,
+ type: "file",
+ ignored: false,
+ })
+ seen.add(item)
+ }
+
+ return out.toSorted((a, b) => {
+ if (a.type !== b.type) {
+ return a.type === "directory" ? -1 : 1
+ }
+ return a.name.localeCompare(b.name)
+ })
})
const Node = (
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 772ad063b..540046c09 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -500,9 +500,7 @@ export default function Page() {
const out = new Map<string, "add" | "del" | "mix">()
for (const diff of diffs()) {
const file = normalize(diff.file)
- const add = diff.additions > 0
- const del = diff.deletions > 0
- const kind = add && del ? "mix" : add ? "add" : del ? "del" : "mix"
+ const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
out.set(file, kind)
diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts
index 1c1539090..b3c8a905c 100644
--- a/packages/opencode/src/snapshot/index.ts
+++ b/packages/opencode/src/snapshot/index.ts
@@ -188,6 +188,7 @@ export namespace Snapshot {
after: z.string(),
additions: z.number(),
deletions: z.number(),
+ status: z.enum(["added", "deleted", "modified"]).optional(),
})
.meta({
ref: "FileDiff",
@@ -196,6 +197,23 @@ export namespace Snapshot {
export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
const git = gitdir()
const result: FileDiff[] = []
+ const status = new Map<string, "added" | "deleted" | "modified">()
+
+ const statuses =
+ await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-status --no-renames ${from} ${to} -- .`
+ .quiet()
+ .cwd(Instance.directory)
+ .nothrow()
+ .text()
+
+ for (const line of statuses.trim().split("\n")) {
+ if (!line) continue
+ const [code, file] = line.split("\t")
+ if (!code || !file) continue
+ const kind = code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified"
+ status.set(file, kind)
+ }
+
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)
@@ -224,6 +242,7 @@ export namespace Snapshot {
after,
additions: Number.isFinite(added) ? added : 0,
deletions: Number.isFinite(deleted) ? deleted : 0,
+ status: status.get(file) ?? "modified",
})
}
return result
diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts
index ef6271ed5..091469ec7 100644
--- a/packages/opencode/test/snapshot/snapshot.test.ts
+++ b/packages/opencode/test/snapshot/snapshot.test.ts
@@ -749,6 +749,52 @@ test("revert preserves file that existed in snapshot when deleted then recreated
})
})
+test("diffFull sets status based on git change type", async () => {
+ await using tmp = await bootstrap()
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ await Bun.write(`${tmp.path}/grow.txt`, "one\n")
+ await Bun.write(`${tmp.path}/trim.txt`, "line1\nline2\n")
+ await Bun.write(`${tmp.path}/delete.txt`, "gone")
+
+ const before = await Snapshot.track()
+ expect(before).toBeTruthy()
+
+ await Bun.write(`${tmp.path}/grow.txt`, "one\ntwo\n")
+ await Bun.write(`${tmp.path}/trim.txt`, "line1\n")
+ await $`rm ${tmp.path}/delete.txt`.quiet()
+ await Bun.write(`${tmp.path}/added.txt`, "new")
+
+ const after = await Snapshot.track()
+ expect(after).toBeTruthy()
+
+ const diffs = await Snapshot.diffFull(before!, after!)
+ expect(diffs.length).toBe(4)
+
+ const added = diffs.find((d) => d.file === "added.txt")
+ expect(added).toBeDefined()
+ expect(added!.status).toBe("added")
+
+ const deleted = diffs.find((d) => d.file === "delete.txt")
+ expect(deleted).toBeDefined()
+ expect(deleted!.status).toBe("deleted")
+
+ const grow = diffs.find((d) => d.file === "grow.txt")
+ expect(grow).toBeDefined()
+ expect(grow!.status).toBe("modified")
+ expect(grow!.additions).toBeGreaterThan(0)
+ expect(grow!.deletions).toBe(0)
+
+ const trim = diffs.find((d) => d.file === "trim.txt")
+ expect(trim).toBeDefined()
+ expect(trim!.status).toBe("modified")
+ expect(trim!.additions).toBe(0)
+ expect(trim!.deletions).toBeGreaterThan(0)
+ },
+ })
+})
+
test("diffFull with new file additions", async () => {
await using tmp = await bootstrap()
await Instance.provide({
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 0556e1ad9..085c9d9c7 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -96,6 +96,7 @@ export type FileDiff = {
after: string
additions: number
deletions: number
+ status?: "added" | "deleted" | "modified"
}
export type UserMessage = {