Invalidate canceled hierarchy delete-preview requests - #2992
Merged
Conversation
Opening Delete, cancelling, and reopening before the first preview resolved reused the same card-session identity, so the stale response could populate the reopened dialog, clear its loading state, or replace a newer preview. Track a per-attempt generation - the idiom already used by BoardCardArchive, CardParentField and BoardProposalPreview - and bump it on cancel, reopen, card switch, modal close and unmount. Success, failure and finally effects now run only while their attempt still owns the open dialog, so confirmation keeps applying to the child list the user is actually shown.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Owner
Author
|
Fresh-context independent review at exact head Verified by the reviewer from source (read-only):
Non-blocking notes, recorded here rather than as commits (law 2c):
Checks at this head: hosted rollup green (0 failed, 0 pending). Merging with a merge commit. |
This was referenced Sep 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2968
What
Give every hierarchy delete-preview attempt its own ownership token in
useCardModal.ts, and reject every late effect from a superseded attempt.handleDeleteClickpreviously guarded its success/failure/finally effects withisCurrentCardSession(cardId, session)plusshowDeleteConfirm.value. Both are identical across a cancel-then-reopen on the same card, so a response from the canceled attempt could populate the reopened dialog, clear its loading state, or replace a newer preview that had already landed.Why this shape
A per-attempt generation counter is the idiom already in use in this codebase for exactly this problem —
components/board/BoardCardArchive.vue,components/board/CardParentField.vue,components/board/BoardProposalPreview.vue,components/chat/ChatContextPicker.vue. Reused rather than introducing anAbortController, socardsApi.previewDetachkeeps its signature and no other file changes.deletePreviewGenerationis bumped byinvalidateDeletePreview(), which resetsdetachPreview,deletePreviewErroranddeletePreviewLoadingtogether with the token, so a bump can never leave the dialog spinning on an abandoned request.handleDeleteCancel, a card-identity switch, the card modal closing, and unmount.ownsDeletePreview(generation, cardId, session)gates all three effect sites (success, failure,finally) — thefinallyandcatcharms were previously weaker than the success arm.handleDeleteConfirmis unchanged: it still readsdetachPreview.value, i.e. the fingerprint/version the user is actually shown, so explicit confirmation stays wired to the displayed child list. A test now pins that a stale reply landing after a fresh one cannot change what gets confirmed.Deliberately not changed: the card watcher's unconditional
detachPreview = null/deletePreviewError = nullfor a same-card object refresh does not bump the generation. A refresh of the same card is not a card change, and #2965's post-commit refresh repair relies on the in-flight preview for that card still being allowed to land. Bumping there would strand the dialog with no preview and no way to retry short of cancel/reopen.Also out of scope:
handleDeleteConfirm's own failure path still writesdeletePreviewErrorwithout an ownership check. That is the delete mutation, not the preview request, and it is guarded server-side by the fingerprint; flagging rather than expanding the slice.Lane / file-ownership note
PR #2977 (
issue-2240/card-assignments) is concurrently editingCardModal.vue,CardAssignmentField.vueandPaperBoardCard.vue. This PR touches neither — no template change was needed,CardModal.vuealready rendersdetachPreview/deletePreviewLoading/deletePreviewErrorunchanged. The only source edit isuseCardModal.ts, confined to the delete region (delete state declarations,handleDeleteClick/handleDeleteCancel, and one added line each in the two watchers andonBeforeUnmount). Textual conflict with #2977 is possible, semantic overlap is not. Claimed on #2968 with[Claude lane claim v2]before writing.Tests
Seven new cases in
src/tests/composables/useCardModal.spec.tsunderdelete preview ownership, driven by adefer()helper so two attempts overlap deterministically:All seven were confirmed red against the pre-fix
useCardModal.ts(source reverted, tests run, 7 failed) and green with the fix.Checks run
npx vitest --run --maxWorkers=2 src/tests/composables/useCardModal.spec.tsnpx vitest --run --maxWorkers=2 src/tests/components/CardModal.spec.ts src/tests/guards/nativeBrowserDialogs.spec.tsnpm run typecheck(vue-tsc -b)npm run buildINEFFECTIVE_DYNAMIC_IMPORTwarning fortoastStore.tsnpx eslint src/composables/useCardModal.ts src/tests/composables/useCardModal.spec.tsNOT verified
docs/STATUS.md/ masterplan / ADR are untouched.