From 67870d209efafd303b34ec9996c21fed9f165d1a Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 24 Aug 2026 21:32:46 +0200 Subject: docs(backlog): record item 158, a saved draft is invisible until a sync indexes it Found by hand: autosave writes the draft to the Maildir drafts folder but never indexes it, and the Drafts view is a notmuch path: query, so the draft cannot be reopened until notmuch new runs. Approach reuses the single-file index moveMessages already performs. --- .../plans/2026-08-03-post-0.1.0-usability.md | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) (limited to 'docs/superpowers/plans') 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 4153deb..c2c82ea 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 @@ -228,6 +228,8 @@ taking that too literally. | 156 | No delivery confirmation | v2 | ? | open, 2026-08-24, from the notes. Distinct from 154: this is a DSN (`Return-Receipt-To`, or the ESMTP NOTIFY parameter), which is the sending server's to honour rather than the reader's client. Whether it can be requested at all depends on the `send_command`, so this may not be this application's to offer | | 157 | A draft on display offers Reply and Forward, not Edit | workflow | XS | **done** 2026-08-24, unreleased, and the half item 153 did not close. `populateMessageBar()` swaps the reply pair for `edit_draft`, refilled from `updateComposeActions()` so it follows the message. **Took three hand-test rounds, each finding a defect the tests could not see.** First version shipped item 150's trap one level up: it keyed on `currentIndex()`, which a query leaves VALID on a row of the discarded result, so the bar kept the draft button after clicking Inbox and the reply pair after clicking Drafts. It answers from `m_currentMessageId`/`m_currentThreadId` now, which every blanking route clears, refilled from `showPlaceholderPane()` — the one site all five of those routes share. That exposed a THIRD defect nobody had reported and which predates the bar: enablement ran only from the two selection handlers, so Reply and Forward stayed **enabled over a blank pane**, invisible while they sat on the main toolbar among always-on actions. The bar is then HIDDEN over an empty pane (`!m_items.isEmpty()` in `MessageView::setBarActions`): the user first chose a greyed-out bar, then reversed it on sight for a better reason, that the subject and details button already vanish and a persisting bar was the only piece of header furniture that did not. **The hiding half broke the showing half**, found by hand again: `setBarActions` is called from `updateComposeActions()`, which runs BEFORE `showThread()` fills `m_items`, so the first message opened after any blanking left the bar hidden and the second showed it, reading `m_items` still holding the first — one selection behind for the life of the view. `updateHeader()` shows it, beside the details button it rides with. The test missed it by asserting before the render landed, measuring the placeholder; it waits on `showingPlaceholder()` now. Several guards and `hide()` calls were written across the three rounds and then measured dead, and removed | +| 158 | A freshly saved draft is invisible until a sync indexes it | defect | S | open, 2026-08-24, found by hand. Autosave writes the draft to the Maildir drafts folder and stops there, while the Drafts view is a notmuch `path:` query, so a draft cannot be reopened after closing the composer until `notmuch new` runs (sync or cron). Refresh only re-queries. See the section | + Sizes are rough: XS under an hour, S a sitting, M a session. --- @@ -1402,3 +1404,42 @@ edited with the user's own editor, and a text editor inside a mail client is not this project's to build. And a resumed draft seeds nothing, because the saved body already carries whatever signature it was written with, and seeding again would put a second one on a message written once. + +## 158. A freshly saved draft is invisible until a sync indexes it + +**Observed (user, 2026-08-24, by hand).** Composing a new message or resuming +a draft, then closing the composer, the draft cannot be found again in the +Drafts view until a sync runs or the cron job fires. "Refresh the search" +does not bring it back. + +**Cause (verified in the code).** Autosave writes the draft to the Maildir +drafts folder and stops there: `saveDraftNow()` → `DraftStore::write()` at +`composewindow.cpp:1021-1022`, with no indexing step. The Drafts view is a +notmuch query, `Config::allDraftsQuery()` → `Account::draftsQuery()` → +`path:".../Drafts/**"` (`config.cpp:139,166`), and "refresh" re-runs that +query against the existing index. Only `assets/mailsync.sh` runs +`notmuch new`. So the file exists on disk and is invisible to the view, which +is why the user cannot reopen it: the draft is safe but unreachable. + +**Approach.** After a successful `saveDraftNow()`, index the one file the same +way `moveMessages` already does: `notmuch_database_index_file()` at +`notmuchworker.cpp:795-798`, opening read-write, indexing the single path, +closing. This is a single-file index, not a `notmuch new`, so it does not scan +the tree or contend with the cron sync the way a full index would. The draft +then appears on the next refresh, or immediately if the worker also emits the +refresh it already emits for a move. + +**Constraints.** + +- **Thread boundary.** `ComposeWindow` lives on the UI thread and does not + talk to `NotmuchWorker`; `MainWindow` owns that connection. The request + needs a route (a `ComposeWindow` signal that `MainWindow` forwards, or a new + worker operation called from the save path). +- **Tags.** `index_file` on a brand-new message may apply notmuch's `new.tags` + (typically `unread;inbox`), which would make the draft ALSO appear in the + inbox. The drafts view is path-based and needs no tag, so the fix must strip + whatever `index_file` assigns. Verify what it assigns before assuming it is + empty. +- **Read-write burst.** This is another short read-write open on the notmuch + database, so it must follow the same close-first ordering `applyTags` uses + (`CLAUDE.md`), never holding the write lock. -- cgit v1.2.3