fix(vscode): prevent draftKey effect from recreating deleted-session drafts

handleSessionDeleted cleared the draft Maps inside the cleanup batch,
but PromptInput's createEffect(on(draftKey, ...)) runs after the batch
ends. When the active session was deleted, the effect saw draftKey
transition from ...:session:<id> to ...:pending:<id> (draftSessionID
was left pointing at the deleted id) and called
saveDraft(prev, currentText, currentImages), which wrote the unsent
draft and attached image data URLs straight back into the just-cleared
...:session:<id> entry.

Two changes:

- Move deleteDraftsForSession(sessionID) out of the batch so it runs
  after the effect's recreate is also cleaned up.
- Clear draftSessionID alongside currentSessionID in the
  active-session delete branch so draftKey falls all the way to the
  'new' bucket instead of ...:pending:<deleted-id>.

Tests: regression in prompt-drafts.test.ts covering the recreate-then-
cleanup path, plus contract assertions in prompt-send-contract.test.ts
that setDraftSessionID is cleared and deleteDraftsForSession is called
outside the batch.
This commit is contained in:
Aarav Sharma
2026-06-18 21:43:50 -06:00
parent 5df7aa4f93
commit 494817da26
4 changed files with 42 additions and 2 deletions
@@ -30,6 +30,25 @@ describe("deleteDraftsForSession", () => {
deleteDraftsForSession("")
expect(drafts.get("prompt:default:session:a")).toBe("draft a")
})
it("clears drafts that PromptInput's draftKey effect recreates after the batch", () => {
const img = {
id: "i1",
filename: "x.png",
mime: "image/png",
dataUrl: "data:image/png;base64,AAAA",
}
drafts.set("prompt:default:session:a", "draft a")
imageDrafts.set("prompt:default:session:a", [img])
deleteDraftsForSession("a")
drafts.set("prompt:default:session:a", "draft a")
imageDrafts.set("prompt:default:session:a", [img])
deleteDraftsForSession("a")
expect(drafts.has("prompt:default:session:a")).toBe(false)
expect(imageDrafts.has("prompt:default:session:a")).toBe(false)
})
})
describe("sessionDraftKey", () => {
@@ -116,3 +116,23 @@ describe("isPromptBlocked signature contract", () => {
expect(params).toHaveLength(1)
})
})
describe("handleSessionDeleted draft cleanup contract", () => {
const source = readFile(SESSION_FILE)
it("clears draftSessionID alongside currentSessionID when deleting the active session", () => {
const body = extractFunctionBody(source, "handleSessionDeleted")
const activeBlock = body.match(/if \(currentSessionID\(\) === sessionID\) \{([\s\S]*?)\}/)
expect(activeBlock).not.toBeNull()
expect(activeBlock![1]).toContain("setDraftSessionID(undefined)")
})
it("calls deleteDraftsForSession outside the cleanup batch so PromptInput's recreate is also cleaned up", () => {
const body = extractFunctionBody(source, "handleSessionDeleted")
const batchMatch = body.match(/batch\(\(\) => \{([\s\S]*?)\}\)/)
expect(batchMatch).not.toBeNull()
expect(batchMatch![1]).not.toContain("deleteDraftsForSession(sessionID)")
const postBatch = body.slice((batchMatch!.index ?? 0) + batchMatch![0].length)
expect(postBatch).toContain("deleteDraftsForSession(sessionID)")
})
})
@@ -1911,12 +1911,13 @@ export const SessionProvider: ParentComponent = (props) => {
delete map[sessionID]
}),
)
deleteDraftsForSession(sessionID)
if (currentSessionID() === sessionID) {
setCurrentSessionID(undefined)
setDraftSessionID(undefined)
setLoading(false)
}
})
deleteDraftsForSession(sessionID)
}
// Splices the message from the store and deletes its parts.
@@ -18,4 +18,4 @@ export function deleteDraftsForSession(id: string) {
}
}
}
}
}