Closes#71 — both halves, which turn out to be one cause.
Deleting from the keyboard left focusId pointing at a row no longer in the list. afterAction cleared the selection and advanced the open thread, but never touched list focus.
The confirmation "every other message".targetIds() falls back to the focused id, so the second # re-targeted the message the first had just deleted. The optimistic update had already moved that message into Deleted Items — so it read as a permanent delete, and a permanent delete always confirms, whatever "Confirm before deleting" says. The dialog was correct about the message it was asked about; it was asked about the wrong one.
k jumping to the top.moveFocus reads ids.indexOf(focusId), which is -1 for a departed row, and -1 is treated as "before the first row". Add -1 and it clamps to 0.
Both explain the detail that made this diagnosable: "if using the mouse to click on the next message, then no confirmation is requested" — clicking sets focus to a row that exists.
The fix
Focus moves to whatever slid into the deleted row's place, honouring After archiving or deleting (the row below by default, the one above when set to newer), and clears when the folder empties. moveFocus also stops reading a missing row as index 0.
Verified in the browser, not just in arithmetic
Unit tests pin the index maths, but they can't prove the wiring — so this was driven against the running app with confirmDelete: false, the reported condition:
start focus e4 (index 2)
# e4 gone, focus → e5 no dialog
# e5 gone, focus → e6 no dialog ← the reported bug
# e6 gone, focus → e9 index still 2
k focus → e74, index 1 ← up by one, not to the top (e3)
7 new tests covering both directions, the ends of the list, the newer setting, and the empty-folder case. 272 web + 77 server tests, typecheck and build clean.
Closes #71 — both halves, which turn out to be one cause.
Deleting from the keyboard left `focusId` pointing at a row no longer in the list. `afterAction` cleared the selection and advanced the open *thread*, but never touched list focus.
**The confirmation "every other message".** `targetIds()` falls back to the focused id, so the second `#` re-targeted the message the first had just deleted. The optimistic update had already moved that message into Deleted Items — so it read as a *permanent* delete, and a permanent delete always confirms, whatever "Confirm before deleting" says. The dialog was correct about the message it was asked about; it was asked about the wrong one.
**`k` jumping to the top.** `moveFocus` reads `ids.indexOf(focusId)`, which is `-1` for a departed row, and `-1` is treated as "before the first row". Add `-1` and it clamps to `0`.
Both explain the detail that made this diagnosable: *"if using the mouse to click on the next message, then no confirmation is requested"* — clicking sets focus to a row that exists.
## The fix
Focus moves to whatever slid into the deleted row's place, honouring **After archiving or deleting** (the row below by default, the one above when set to *newer*), and clears when the folder empties. `moveFocus` also stops reading a missing row as index 0.
## Verified in the browser, not just in arithmetic
Unit tests pin the index maths, but they can't prove the wiring — so this was driven against the running app with `confirmDelete: false`, the reported condition:
```
start focus e4 (index 2)
# e4 gone, focus → e5 no dialog
# e5 gone, focus → e6 no dialog ← the reported bug
# e6 gone, focus → e9 index still 2
k focus → e74, index 1 ← up by one, not to the top (e3)
```
7 new tests covering both directions, the ends of the list, the *newer* setting, and the empty-folder case. 272 web + 77 server tests, typecheck and build clean.
**Merged** 2026-08-26 as coffey-labs/ihasmail@6e58c2280758
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #71 — both halves, which turn out to be one cause.
Deleting from the keyboard left
focusIdpointing at a row no longer in the list.afterActioncleared the selection and advanced the open thread, but never touched list focus.The confirmation "every other message".
targetIds()falls back to the focused id, so the second#re-targeted the message the first had just deleted. The optimistic update had already moved that message into Deleted Items — so it read as a permanent delete, and a permanent delete always confirms, whatever "Confirm before deleting" says. The dialog was correct about the message it was asked about; it was asked about the wrong one.kjumping to the top.moveFocusreadsids.indexOf(focusId), which is-1for a departed row, and-1is treated as "before the first row". Add-1and it clamps to0.Both explain the detail that made this diagnosable: "if using the mouse to click on the next message, then no confirmation is requested" — clicking sets focus to a row that exists.
The fix
Focus moves to whatever slid into the deleted row's place, honouring After archiving or deleting (the row below by default, the one above when set to newer), and clears when the folder empties.
moveFocusalso stops reading a missing row as index 0.Verified in the browser, not just in arithmetic
Unit tests pin the index maths, but they can't prove the wiring — so this was driven against the running app with
confirmDelete: false, the reported condition:7 new tests covering both directions, the ends of the list, the newer setting, and the empty-folder case. 272 web + 77 server tests, typecheck and build clean.
Merged 2026-08-26 as coffey-labs/ihasmail@6e58c22807
Rebuilt from: git history, session transcript.