fix(vscode): reject stale diff reads and prune removed reviews

This commit is contained in:
marius-kilocode
2026-08-27 13:08:19 +02:00
parent 7270835321
commit bd4789f9dd
5 changed files with 95 additions and 8 deletions
@@ -155,11 +155,20 @@ export function createDiffCache(load: Loader) {
const existing = pending.get(latest.id)
if (existing && existing.item !== item) return existing.work
const result = await load.detail(item.dir, latest.state.anc, latest.meta)
if (current(item)?.id !== latest.id) return null
if (result.image?.before?.error === "unreadable" || result.image?.after?.error === "unreadable") return result
remember(latest.id, result)
return result
}
const settle = async (item: Item, value: Value | undefined) => {
if (current(item)?.id === item.id && value) {
item.resolve(value)
return
}
item.resolve(item.calls.size > 0 ? await fallback(item) : null)
}
const run = async (items: Item[]) => {
for (let index = 0; index < items.length; index += MAX_BATCH_FILES) {
const chunk = items.slice(index, index + MAX_BATCH_FILES).filter((item) => item.calls.size > 0)
@@ -171,7 +180,7 @@ export function createDiffCache(load: Loader) {
if (chunk.length === 1) {
const item = chunk[0]!
try {
item.resolve(await load.detail(item.dir, item.anc, item.meta))
await settle(item, await load.detail(item.dir, item.anc, item.meta))
} catch (error) {
item.reject(error)
}
@@ -190,13 +199,8 @@ export function createDiffCache(load: Loader) {
await Promise.all(
chunk.map(async (item) => {
try {
const latest = current(item)
const entry = value?.entries.get(item.meta.file)
if (latest?.id === item.id && entry && !value?.deferred.has(item.meta.file)) {
item.resolve(entry)
return
}
item.resolve(item.calls.size > 0 ? await fallback(item) : null)
const entry = value?.deferred.has(item.meta.file) ? undefined : value?.entries.get(item.meta.file)
await settle(item, entry)
} catch (error) {
item.reject(error)
}
@@ -100,6 +100,50 @@ describe("createWorktreeDiffs", () => {
})
})
it("prunes every scope of deleted worktrees without dropping local or other-project reviews", () => {
createRoot((dispose) => {
const store = createWorktreeDiffs(vscode([]), () => "project-a")
for (const id of ["gone#branch", "gone#staged", "live#branch", "local#session:s1"]) {
store.onWorktreeDiff({
type: "agentManager.worktreeDiff",
projectId: "project-a",
sessionId: id,
diffs: [diff("a.ts")],
})
}
store.onWorktreeDiff({
type: "agentManager.worktreeDiff",
projectId: "project-b",
sessionId: "gone#branch",
diffs: [diff("b.ts")],
})
store.onWorktreeDiffLoading({
type: "agentManager.worktreeDiffLoading",
projectId: "project-a",
sessionId: "gone#branch",
loading: true,
})
store.onWorktreeDiffNotice({
type: "agentManager.worktreeDiffNotice",
projectId: "project-a",
sessionId: "gone#branch",
notice: "deleted",
})
store.requestDiffFile("gone#branch", "a.ts")
store.prune(new Set(["live"]))
expect(Object.keys(store.diffDatas()).sort()).toEqual([
"project-a\0live#branch",
"project-a\0local#session:s1",
"project-b\0gone#branch",
])
expect(store.diffFileLoadingFor(() => "gone#branch").size).toBe(0)
expect(store.diffNotices()["project-a\0gone#branch"]).toBeUndefined()
expect(store.diffLoading()).toBe(false)
dispose()
})
})
it("does not replace state when an update produces an identical diff list", () => {
withDiffs((diffs) => {
diffs.onWorktreeDiff({ type: "agentManager.worktreeDiff", sessionId: "s1", diffs: [diff("a.ts")] })
@@ -732,6 +732,28 @@ describe("createLocalDiff concurrent details", () => {
})
})
it("does not return stale singleton details after a newer summary arrives", async () => {
await withRepo(async (dir, base) => {
await fs.writeFile(path.join(dir, "seed.txt"), "seed\nold\n")
const ops = new RecordingGitOps()
const local = createLocalDiff(ops)
await local.summary(dir, base)
const gate = deferred<void>()
const ready = deferred<void>()
ops.block(show, gate.promise, ready.resolve)
const pending = local.file(dir, base, "seed.txt")
await ready.promise
await fs.writeFile(path.join(dir, "seed.txt"), "seed\nnewer contents\n")
const summary = await local.summary(dir, base)
gate.resolve()
const value = await pending
expect(value?.after).toBe("seed\nnewer contents\n")
expect(value?.stamp).toBe(summary.find((entry) => entry.file === "seed.txt")?.stamp)
expect(await local.file(dir, base, "seed.txt")).toBe(value)
})
})
it("shares one result for duplicate concurrent callers in a batch", async () => {
await withRepo(async (dir, base) => {
await setup(dir, base)
@@ -724,6 +724,7 @@ const AgentManagerContent: Component = () => {
createEffect(() => {
const ids = new Set(worktrees().map((wt) => wt.id))
composers.prune(ids)
untrack(() => diffs.prune(ids))
setReviewOpenByContext((prev) => {
const next = pruneReviewState(prev, currentProjectId() ?? "single", ids)
if (Object.keys(next).length === Object.keys(prev).length) return prev
@@ -78,6 +78,21 @@ export function createWorktreeDiffs(
setDiffFileLoading(remove)
}
const prune = (ids: Set<string>) => {
const prefix = `${project() ?? "single"}\0`
const keys = new Set([
...Object.keys(diffDatas()),
...Object.keys(diffLoadings()),
...Object.keys(diffNotices()),
...Object.keys(diffFileLoading()),
])
for (const data of keys) {
if (!data.startsWith(prefix)) continue
const ctx = parseDiffId(data.slice(prefix.length)).ctx
if (ctx !== "local" && !ids.has(ctx)) drop(data)
}
}
const retain = (id: string) => {
const data = id.includes("\0") ? id : key(id)
const entries = diffDatas()[data]
@@ -232,6 +247,7 @@ export function createWorktreeDiffs(
diffDataKey,
retain,
drop,
prune,
reset,
onWorktreeDiff,
onWorktreeDiffFile,