aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-24 21:58:38 +0200
committerDanilo M. <danix@danix.xyz>2026-08-24 21:58:38 +0200
commit8399a2652584e348ba73f7059d9e178958855897 (patch)
tree54262c802538c48b7d21ed9ad59e60ed84039611
parentb7f2a4e0f8858b1aa0d86755ebab6826306f3eff (diff)
downloadqtmaildir-8399a2652584e348ba73f7059d9e178958855897.tar.gz
qtmaildir-8399a2652584e348ba73f7059d9e178958855897.zip
docs(backlog): close item 158, drafts are indexed on save
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md35
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md43
2 files changed, 36 insertions, 42 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
index 94a02d2..d6adc98 100644
--- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
+++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
@@ -7513,3 +7513,38 @@ bump either way: an ignored optional field is not a breaking change.
**Size: S.** Removing a field, two UI affordances and their tests.
**Closed 2026-08-24** (unreleased). See the status table row for the outcome.
+
+---
+
+## 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`, with no indexing step. The Drafts view is a notmuch
+query, `Config::allDraftsQuery()` -> `Account::draftsQuery()` ->
+`path:".../Drafts/**"`, 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.
+
+**Outcome.** `saveDraftNow()` emits `draftSaved(path, previousPath)`;
+`MainWindow::openComposer()` connects it to a new
+`NotmuchWorker::indexDraftFile()`, which indexes the one file the way
+`moveMessages()` does and removes the previous revision so a rewrite leaves no
+ghost. The send path unlinks a draft it had indexed while composing, so
+`draftRemoved(path)` -> `removeIndexedFile()` drops that entry.
+
+**Measured, and it makes the fix smaller than the item guessed.**
+`notmuch_database_index_file` assigns NO tags at all, unlike `notmuch new`,
+which would add `draft inbox unread` from `new.tags` and the `:2,D` flag. So
+the "strip whatever index_file assigns" concern is moot: a draft indexed this
+way cannot leak into a `tag:inbox` or `tag:unread` view, and no stripping is
+needed. The drafts view is path-based, so zero tags is exactly enough.
+
+**Size: S.** One worker slot, one signal, and their tests.
+
+**Closed 2026-08-24** (unreleased). See the status table row for the outcome.
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 b86740e..2d99704 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,7 +228,7 @@ 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 |
+| 158 | A freshly saved draft is invisible until a sync indexes it | defect | S | **done** 2026-08-24, unreleased. `saveDraftNow()` emits `draftSaved`, which `MainWindow` connects to a new `NotmuchWorker::indexDraftFile()` that indexes the one file (previous revision removed, so a rewrite leaves no ghost), and `draftRemoved` drops the entry when a sent draft is unlinked. Measured: `index_file` assigns NO tags, so no stripping and no tag:inbox leak. See the section |
Sizes are rough: XS under an hour, S a sitting, M a session.
@@ -1331,44 +1331,3 @@ half of the fork above, and it would look exactly like this.
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.
-
----
-
-## 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.