diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-25 10:13:46 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-25 10:13:46 +0200 |
| commit | f4568c0591184823d9f37ab8eb86691ad3057ac4 (patch) | |
| tree | 1bda36513dcd30d78aa22e6bba0b478d76b166d0 /docs | |
| parent | 07c36ce617512bb9514e604b614e95a15c390b04 (diff) | |
| download | qtmaildir-f4568c0591184823d9f37ab8eb86691ad3057ac4.tar.gz qtmaildir-f4568c0591184823d9f37ab8eb86691ad3057ac4.zip | |
docs(backlog): record item 162, Delete races a sync's rename
Found by hand while deleting a draft: "Cannot move <file> to <folder>".
Neither Delete nor item 158 is at fault. mbsync uploads a saved draft and
RENAMES it to add its ,U=<uid> infix, and notmuch keeps the pre-U= name
until that sync's notmuch new runs, so moveMessages() calls QFile::rename
on a path that no longer exists.
Verified against the live Maildir rather than read: notmuch named a file
that was not on disk while a sync was running, and the same query was
clean afterwards with the file present under its new name. That is why it
reads as intermittent and why it heals itself.
Truthful and lossless, but the action silently does nothing and the
message blames a folder for a timing problem, which sent the user looking
at a configuration that was correct.
Records both candidate approaches and notes the likelier one: refuse the
move while a sync holds the lock, joining the held-edit machinery items
97 and 106 already built for exactly this shape, rather than re-resolving
the filename and racing the same window. Also notes that this is the ,U=
trap CLAUDE.md records for MaildirName::fresh(), seen from the other
side.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UUQS6n3cmsFrsjCNmwNtf8
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 93 |
1 files changed, 55 insertions, 38 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md index ab11c4f..e3ede0b 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md @@ -233,8 +233,9 @@ taking that too literally. | 159 | The Drafts view lists threads, so a draft is unreachable by double-click | defect | S | **done** 2026-08-25, unreleased. Reverses item 138's own decision, confirmed with the user. `generatorIsFlat()` in `config.cpp` is now the single closed set of flat generators, replacing three hardcoded comparisons against `"sent"`: the built-in filter, the reader that reapplies the mode, and the writer that skips storing what the generator implies. Those three had to agree and nothing made them; a `drafts` entry saved and reloaded would otherwise have come back THREADED while the button was flat. `builtinFilter()` sets `flat` once from the helper rather than in a branch, so the set cannot drift from the labels | | 160 | The composer never says a draft was autosaved | feedback | S | **done** 2026-08-25, unreleased. A status bar on the composer: the age line left, the `○ unsaved content` cue beside it. **The fix is a funnel, not a label.** `m_dirty` had SEVEN writers and four of them clear it, only two of which are a save, so a cue hung off the save path silently missed the constructor and the send; `setDirty()` is the one writer now and refreshes both cues plus `setWindowModified()`. Presentation was **reworked after the user looked at it**: it first reused item 151's yellow ribbon treatment, which reads as a misplaced widget on a bare status label, and the cue sat in the permanent (right-hand) tray. Two defects found by probing rather than by reading, see the section | -| 161 | The composer has no menu bar | discoverability | S | open, 2026-08-25, from the notes. Save draft on `Ctrl+S` is asked for and does not exist at all: `saveDraftNow()` is reachable only from the timer, Send and `closeEvent`. The composer's actions are ad-hoc `QAction`s parented to the window (Send, Close, and the formatting toolbar's), none registered in `KeyMap`, so item 132's menu-reachability rule does not currently reach them. **Unblocked** 2026-08-25: 160 shipped the status bar, so a manual save now has somewhere to report. Still needs the user to say WHICH main-window actions belong on a composer menu, since most message actions are meaningless over a message being written | +| 161 | The composer has no menu bar | discoverability | S | **done** 2026-08-25, unreleased. File / Edit / Format, to the user's own chosen scope. **Save draft (`Ctrl+S`) is the only NEW action**; everything else is gathered, and the menus show the toolbar's own `QAction` objects rather than copies, per item 140's rule. Two needed hand-building: the HTML toggle is a `QToolButton` and cannot go in a menu, so a checkable twin mirrors it BOTH ways; and the signature entry takes the switch's own `QMenu` pointer, since that menu is rebuilt when the signatures change and copied entries would go stale. Edit's entries follow the editor's own `undoAvailable`/`copyAvailable`. `theMenuBarReachesEveryComposerAction()` is item 132's rule applied to the composer, walking the real menu bar and finding actions by `findChildren`, so a future action added to the toolbar and forgotten in the menus fails without touching the test | +| 162 | Delete fails while a sync is renaming the file underneath it | defect | S | open, 2026-08-25, found by hand. `Cannot move <file> to <folder>`. NOT a Delete defect and not item 158's: mbsync renames an uploaded file to add its `,U=<uid>` infix, and notmuch keeps the pre-`U=` name until that sync's `notmuch new` runs, so `moveMessages` renames a path that no longer exists. Truthful, harmless and SELF-HEALING, which is why it reads as intermittent: verified a ghost present mid-sync and gone after. The message names a folder as though the folder were the problem. Delete reaches the real mail server, so read `CLAUDE.md` before touching it | Sizes are rough: XS under an hour, S a sitting, M a session. --- @@ -1337,43 +1338,59 @@ The 70-second duration recorded above fits a `QTRY_*` waiting for a file that is never going to appear, which is consistent with a wrong destination rather than a slow one. -## 161. The composer has no menu bar - -**Observed (user, 2026-08-25):** "the compose window should have a menu bar on -top", with "save draft (ctrl+s)" and "duplicate the other actions like we do -on the main window". - -**Cause (verified in code):** the composer has no `QMenuBar`, and **Save draft -does not exist as an action at all.** `saveDraftNow()` is reachable only from -the autosave timer, from the send path, and from `closeEvent`; there is no way -for the user to ask for a save, and no `Ctrl+S` anywhere in the composer or in -`KeyMap`. - -The actions the composer does have are ad-hoc `QAction`s parented to the -window: `m_sendAction` and a Close action (`composewindow.cpp:583-608`), plus -the formatting toolbar's own. **None is registered in `KeyMap`**, which is -deliberate and documented for `Ctrl+W` in item 148: they are WindowShortcuts -dispatched to the active composer only, so they never touch the main window's -namespace. A consequence worth stating before this item is built: -`everyActionIsReachableFromAMenu()` walks the MAIN window's menu bar, so it -does not currently constrain these, and adding a composer menu bar does not -automatically bring them under item 132's rules. - -**Approach.** A `QMenuBar` on the composer, with Save draft (`Ctrl+S`) as the -new action and the existing ones gathered under it rather than duplicated. +## 162. Delete fails while a sync is renaming the file underneath it + +**Observed (user, 2026-08-25):** deleting a draft reported `Cannot move +<file> to <account>/Trash`. + +**Cause (verified against the live Maildir, not read):** a stale path, and +neither Delete nor item 158 is at fault. + +1. The composer autosaves a draft as `<name>:2,D` and item 158 indexes it + under exactly that filename. +2. **mbsync uploads it and RENAMES it** to `<name>,U=<uid>:2,D`, recording the + server UID in the filename. +3. notmuch still holds the pre-`U=` name until that sync's `notmuch new` runs. +4. `moveMessages()` reads the filename from notmuch and calls + `QFile::rename()` on a path that no longer exists. It fails, the error is + emitted, and the message is skipped. + +Measured, in this order: `notmuch search --output=files` named a file that was +not on disk while `Background sync running...` was up, and the same query was +clean once the sync finished, with the file present under its new `,U=4` name. +That is why it reads as intermittent, and why it heals itself. + +**It is truthful and it loses nothing.** The move is skipped, no wrong folder +is created, no file is destroyed, and the next sync reconciles. The defect is +that the message blames a folder for a timing problem, and that the action +silently does nothing when the user asked for something. + +**This is `CLAUDE.md`'s `,U=` trap from the other side.** `MaildirName::fresh()` +exists because CARRYING that infix across a folder boundary produced +`Maildir error: duplicate UID` on real mail. Here mbsync is ADDING it and the +index lags; the same infix, the opposite direction. + +**Approach, and it needs a decision.** Two candidates: + +- **Refuse the move while a sync holds the lock.** `SyncMonitor` already + reports this, and the held-edit machinery from items 97 and 106 already + exists for exactly this shape: a tag edit made during a sync is held and + flushed when it ends. Delete would join it rather than inventing anything. + This is the likelier right answer, since it matches what every other + mutation already does. +- **Re-resolve the filename** from notmuch immediately before the rename and + re-query the message if the path is gone. Smaller, but it races the same + window it is trying to close, and a second lookup can be stale by the time + it is used. **Constraints.** -- **"Duplicate the other actions" means the main window's MESSAGE actions, and - most of them are meaningless here.** A composer has no thread, no selection - and no tag surface. Ask which the user actually wants before building a menu - that offers Archive or Mark all read over a message being written. -- Keep the composer's actions out of `KeyMap`, per item 148's reasoning. A - composer menu bar is not a reason to move them. -- Save draft must go through `saveDraftNow()`, which already handles the - failure banner and emits `draftSaved` for the indexing item 158 added. A - second write path would reintroduce the ghost-file problem that fixed. -- **Item 160 unblocked this** on 2026-08-25: the status bar exists, so a - manual save reports through `refreshDraftStatus()` like an autosave. Route - `Ctrl+S` through `saveDraftNow()` and the reporting is already done. - +- **Delete reaches the real mail server.** Read `CLAUDE.md`'s item 103 notes + before touching `moveMessages()`: a wrong folder name is created, adopted by + mbsync, and propagated to every other client. +- Whatever is built, **the message must say a sync is running**, not name a + folder. The current wording sent the user looking for a broken folder + configuration, which was correct and configured. +- A test cannot see this in the ordinary fixture layout, where nothing renames + a file underneath the index. Driving it means renaming the file between the + index write and the move, which is what the reproducer has to do. |
