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.
The webview kept module-level Maps of unsent prompt text, pending
review comments, and pending image attachments keyed by session ID.
Deleting a session removed its transcript and todo state via
handleSessionDeleted, but left the draft entries orphaned in those
Maps forever. Because image attachments carry base64 data URLs, the
leak was unbounded for any user who attached images and then deleted
sessions.
This extracts the three Maps into webview-ui/src/utils/draft-store.ts
and adds deleteDraftsForSession(id), which drops every entry ending in
:session:<id> or :pending:<id>. PromptInput now imports the shared
Maps so there is a single source of truth, and handleSessionDeleted
calls the helper as part of its existing cleanup batch.
Session switching is intentionally unchanged: the save/restore effect
in PromptInput still preserves drafts across A -> B -> A. Only real
deletion via the sessionDeleted message frees the entries.