# Post-0.1.0 usability backlog Status: **open, expandable by design.** This document is not a fixed release plan. It collects items found by actually using qtmaildir after 0.1.0, and it grows as more turn up. Nothing here is scheduled; picking what ships in a given release is a separate decision. Source: usage notes taken while running the app, 2026-08-03. **Numbers here are this document's own.** The user's own notes were numbered independently and the two sequences drifted apart once items were split: what those notes called 12 is item 13 here, and item 14 here (the tag column) was never in them at all. Items 15 to 17 come from a later pass over the same notes. Cite these numbers, not the notes', and do not renumber to reconcile. **The notes are the upstream source and they keep growing.** The user adds to them while using the app, so this document goes stale on its own. Items 28 to 35 came from one such pass on 2026-08-04 and included two defects that had gone unrecorded here for a while. Items 39 to 45 came from the 2026-08-05 pass, which found one more defect (41, a message body silently dropped by the MIME walk) and one item that cannot be planned at all until the user says where the thing it manages lives (44). Compare the two at the start of a session; the procedure is in `CLAUDE.md`. Numbering is stable. New items append with the next free number and never renumber, so a note referring to "item 7" keeps meaning the same thing. An item that is dropped stays in the table marked `dropped` with a one-line reason. **The status table below is the index of every item; the sections are only the open ones.** Item 73 moved the done, dropped and postponed sections out to `2026-08-03-post-0.1.0-usability-closed.md`, which took this file from just over five thousand lines to under six hundred. Nothing was deleted and nothing was renumbered: a closed item keeps its row here, with its date and outcome, and its full Observed/Cause/Approach section is in that file under the same number. Look there when a row cites evidence you need. **Items 20 and 53 are both on master since 2026-08-10**, as the card list. Item 20's original presentation, the one the user rejected on sight, is preserved on the branch `item-20-message-rows` at 029a50e and was never merged; the branch `card-list` carries the work that was. Any file or line reference in item 20's entry, now in the closed-items file, points at that PARKED branch, not at master, where the same lines are unrelated. Item 53 records why the first attempt was rejected and is worth reading before changing the thread pane again. ## Theme 0.1.0 was built to a spec written by someone who lives in neomutt. The result is a keyboard-driven reader with almost no visible affordances. The notes below are, with few exceptions, one complaint restated in several forms: **the app does not tell the user what it can do, and it does not remember what the user told it.** Two clusters follow from that: - **Persistence.** Splitter position, font size, window geometry, and the account selection all reset on restart. Each is small on its own and aggravating every single launch. - **Discoverability.** Shortcuts are the only route to most actions, and there is no menu bar, no toolbar, and no way to see the key bindings from inside the app. Both clusters are cheap to fix. Neither was an oversight in design so much as a consequence of specifying the app as "a GUI counterpart to neomutt" and then taking that too literally. ## Status table | # | Item | Cluster | Size | Status | |---|------|---------|------|--------| | 1 | Splitter/column widths do not survive restart | persistence | S | **done** | | 2 | No way to see full message details (From/To/Cc/Subject) | information | M | **done** | | 3 | Too few clickable affordances, shortcuts are the only route | discoverability | M | **done** | | 4 | Message-pane font size does not survive restart | persistence | S | **done** | | 5 | Thread list is cramped, poor readability | presentation | S | **done** | | 6 | Opened message stays unread | behavior | S | **done** | | 7 | HTML view should be default for HTML messages | behavior | XS | **done** (already worked) | | 8 | No buttons or menu entries for archive, undo, etc | discoverability | M | **done** | | 9 | No in-app view of configured shortcuts | discoverability | S | **done** | | 10 | Reaching an account's inbox takes two steps | workflow | S | **postponed** (partly done) | | 11 | Icon, `.desktop` file, SlackBuild | packaging | M | **done** | | 12 | Message pane is light-theme only | presentation | S | **done** | | 13 | No visual feedback that an action stuck | feedback | S | **done** | | 14 | Tag column unreadable, tags need another home | presentation | M | **done** | | 15 | Attachments are parsed but unreachable from the UI | information | M | **done** | | 16 | Delete on an already-deleted thread should undelete | behavior | S | **done** | | 17 | No completion for tags in the query bar | workflow | M | **done** | | 18 | No visual cue that there are unsynced edits | feedback | S | **done** | | 19 | No prompt to sync on exit when edits are pending | behavior | S | **done** | | 20 | Thread view does not match the user's mental model | presentation | L | **done** 2026-08-10, as the card list; see 53 | | 21 | Default shortcuts are not sensible enough | discoverability | S | open | | 22 | Translatability audit and i18n wiring | correctness | M | open | | 23 | No way to save a search query from the UI | workflow | M | **done** 2026-08-13, shipped in 0.18.0; see `specs/2026-08-13-saved-queries-design.md` | | 24 | No right-click actions on the thread list | discoverability | S | **done** | | 25 | No select-all, and bulk actions are undiscoverable | workflow | S | **done** | | 26 | No way to add or remove an arbitrary tag from the UI | workflow | S | **done** | | 27 | The UI cannot see a sync it did not start | feedback | S | **done** | | 28 | Re-adding `unread` counts 2 unsynced changes, not 0 | correctness | S | **done** | | 29 | Sync button stays enabled during a background sync | feedback | XS | **done** | | 30 | The blank right pane is wasted space | presentation | M | **done** | | 31 | The quit prompt has no highlighted default button | discoverability | XS | **done** | | 32 | Esc does not blank the right pane | workflow | XS | **done** | | 33 | Status bar messages never expire | feedback | S | **done** | | 34 | No overview of the Maildir itself | information | M | **done** | | 35 | No refresh of the thread list after a sync | workflow | M | **done** 2026-08-10; the list now follows a sync on its own | | 36 | `test_mainwindow` cannot reach the worker | testing | S | open, on demand | | 37 | The worker stalls on a tag edit made during a background sync | correctness | S | **done** | | 38 | `test_mainwindow` fails when a real sync holds the lock | testing | XS | **done** | | 39 | Thread list cannot be sorted by clicking a column header | workflow | S | **dropped** 2026-08-10; the card list has no column headers to click, and 0.13.0 shipped a sort dropdown instead | | 40 | No live filter over the current view | workflow | M | open | | 41 | A message whose HTML body carries a `Content-Id` renders blank | correctness | S | **done** | | 42 | "Syncing..." says nothing about what is being synced | feedback | S | **done** | | 43 | No "Mark all read" for the current view | workflow | S | **done** | | 44 | No way to manage the filters applied at sync time | workflow | M | **done** 2026-08-13; see `specs/2026-08-12-tagging-rules-design.md`. Spans this repo and `mailctl` | | 45 | Two Sync buttons, and only one of them works properly | correctness | S | **done** | | 46 | `uiStateSurvivesARestart` fails under the offscreen platform | testing | XS | **done** | | 47 | The query bar looks unfinished, and cannot be cleared by mouse | presentation | XS | **done** | | 48 | Removing a tag suggests every tag, not the thread's own | workflow | XS | **done** | | 49 | Sync runs every account regardless of what changed | workflow | M | **done** | | 50 | Esc blanks the pane but leaves the row selected | workflow | XS | **done** | | 51 | Clicking a subject scrolls the list sideways | presentation | XS | **done** 2026-08-10; a card is viewport width, so there is nowhere to scroll | | 52 | `test_querycompleter` fails under Wayland, passes offscreen | testing | XS | **done** | | 53 | Message rows still read as a table, not as a conversation | presentation | M | **done** 2026-08-10, merged to master as the card list | | 54 | A cron sync carries the edits but the count still says pending | correctness | S | **done** | | 55 | In a narrow window the message pane is invisible | presentation | XS | **done** | | 56 | No action carries an icon, so the toolbar reserves space for nothing | presentation | S | **done** | | 57 | "Flag" would read better as "Important" or "Starred" | presentation | XS | **done** | | 58 | `message_zoom` documents a 0.5 to 3.0 range and enforces none of it | correctness | XS | **done** | | 59 | Archive and Mark all read shipped with the same icon | presentation | XS | **done** | | 60 | Next thread dead-ends on the last reply of an expanded thread | defect | XS | **done**; already fixed by 5487d58, see the closed-items file | | 61 | `test_mainwindow` fails intermittently, about 1 run in 20 | testing | S | **done** 2026-08-13; an `init()` fixture points every test at its own lock table | | 62 | No config option for the date format on a card | presentation | XS | **done** 2026-08-11 | | 63 | No way to see sent mail, and no filter for it | workflow | M | **done** 2026-08-11; see `specs/2026-08-11-sent-mail-design.md` | | 64 | The Sync button carries a mailbox icon, not a refresh one | presentation | XS | **done** 2026-08-11 | | 65 | No full code review and optimization pass | correctness | ? | open, unspecified | | 66 | Selecting a thread root leaves the message pane blank until a reply has been selected | defect | S | open; needs a reproduction before a fix | | 67 | The placeholder pane counts unread, flagged and inbox, but not sent or drafts | information | XS | **done** 2026-08-11, shipped in 0.15.0 | | 68 | A forwarded subject gets no `passed` tag | workflow | S | open; no subject rule exists, measured 2026-08-11. Decision needed: display mark (XS) or write the flag (S, syncs out) | | 69 | `passed` and `replied` read as words where every other state is a glyph | presentation | S | **done** 2026-08-11, inside item 70 | | 70 | Pane icons are a private set where the main window uses the system theme | presentation | M | **done** 2026-08-11; six shipped SVGs | | 71 | A toolbar action does not sync, so the edit sits until the next cron run | workflow | S | **done** 2026-08-11; 2s default, `auto_sync_delay_ms` | | 72 | No khard/khal integration | workflow | ? | open, unspecified; the user places it after send, so v2 at the earliest | | 73 | This backlog is past four thousand lines | maintenance | S | **done** 2026-08-13; 5056 lines to 578, closed sections moved to `2026-08-03-post-0.1.0-usability-closed.md` | | 74 | "Searching..." keeps claiming a query is running while rows are already arriving | feedback | XS | open; cause measured 2026-08-11, the delay itself is the cold page cache and is not fixable here | | 75 | The tagging rules window forgets its size and its column widths | persistence | S | **done** 2026-08-13, shipped in 0.17.0. The window-kind question is left open, see the closed-items file | | 76 | Every field in the rules dialog is free text, so a rule is easy to get wrong | workflow | M | **done** 2026-08-13, shipped in 0.17.0. See `specs/2026-08-13-rule-builder-design.md` | | 77 | No way to see what a rule would collect, in the thread list | workflow | S | **done** 2026-08-13, shipped in 0.17.0 | | 78 | No way to build a rule from something visible in a message | workflow | S | open, narrowed 2026-08-14; the search half shipped as item 85, which is the road a rule is made from. Now a shortcut across that road: the menus and the seeded-dialog path both exist. Use 85 first and see which values are worth promoting | | 80 | A rule with many conditions squeezes the rule list to one visible row | defect | XS | **done** 2026-08-13, shipped in 0.17.0. Follows item 76 | | 79 | Opening the rules dialog and saving destroys the first rule | defect | XS | **fixed on `rule-builder`** 2026-08-13, unreleased. Shipped in 0.16.0; damaged one real rule, repaired by hand | | 81 | No way to turn a saved query into a tagging rule | workflow | S | **done** 2026-08-14, unreleased; see `specs/2026-08-14-query-to-rule-design.md` | | 82 | A saved query cannot be edited, unpinned or deleted from the UI | defect | S | **done** 2026-08-13, shipped in 0.18.0. Right-click offers Edit, Pin/Unpin and Delete | | 83 | A rule named with spaces is written to the file and dropped by every reader | defect | S | **done** 2026-08-14, unreleased. The name is sanitised into an id, save validates, a bad id loads for repair | | 84 | A config problem blocks `test_mainwindow` on a modal nobody can dismiss | testing | S | open; measured 2026-08-14, `showWarnings()` calls `QMessageBox::warning` from the constructor | | 85 | Nothing on screen can be searched for by right-clicking it | workflow | M | **done** 2026-08-14, unreleased; see `specs/2026-08-14-search-from-message-design.md`. Split from 78; rebuilt the details dialog as rows | Sizes are rough: XS under an hour, S a sitting, M a session. --- ## 21. Default shortcuts are not sensible enough **Observed (user, 2026-08-04):** "improve the default shortcuts to some sensed defaults." **Unspecified in detail**, so ask which bindings feel wrong before proposing a table. What is worth recording is the history, because the defaults have already moved once and the reasons still constrain any second pass. **Where the current defaults came from.** 0.1.0 used bare letters. They were replaced in the 0.2.0 menu work for two reasons that have not gone away: a single letter cannot be a menu accelerator without claiming that letter window-wide, and a bare capital such as `N` parses to an unshifted `Key_N`, which no keystroke emits, so `toggle_unread`, `flag` and `sync` were dead keys that appeared to be bound. See `KeyMap::defaultBindings()` and `normalizeSequence()`. **Constraints on any new default.** - **Do not test reachability with synthetic input.** `QTest::keyClick()` does not reproduce a keyboard layout: it reported `Ctrl++` as dead when it is exactly what the `+` key emits on the user's Italian layout. Verify against the real keyboard, as `CLAUDE.md` records. - Every binding is overridable in `[keys]`, so this is about what a fresh install feels like, not about what is possible. - `Return` is a special case already resolved: it belongs to `open_thread` but the query bar claims it back while focused, so a proposal that moves it must not resurrect that bug. ## 22. Translatability audit and i18n wiring **Observed (user, 2026-08-04):** "a full check of the codebase and wiring up of the i18n system." **This is a debt `CLAUDE.md` already records.** The rule that every user-facing string must be wrapped in `tr()` was added while building query completion, and that file states plainly that "pre-existing code has not been audited against this rule". This item is that audit, plus the loading machinery which does not exist at all. **Two halves, and they are different sizes.** - *The audit.* Every user-visible string in `src/` checked for `tr()`, with the translation context correct: a string in a free function needs `Q_DECLARE_TR_FUNCTIONS`, since calling `QObject::tr()` compiles but files it under the wrong context. `lupdate` output is the evidence here, not reading. - *The wiring.* Nothing loads a `.qm` file today: there is no `QTranslator` in `main.cpp`, no `.ts` files in the tree, and no CMake rule to build or install them. Until that exists, a translated string has nowhere to come from. **Constraint:** query syntax is not user-facing text. notmuch keywords such as `tag:` and `date:` are wire format and must never be translated, only the prose describing them. The completion vocabulary is exactly this trap: the values are literal, the descriptions are prose. **Verification:** run `lupdate` and read the generated `.ts`. A string that does not appear there is not translatable, whatever the source looks like. ## 36. `test_mainwindow` cannot reach the worker **Observed:** twice in one session (0.8.0), a defect could not be given a regression test because `MainWindow` in tests has no notmuch database, so its `NotmuchWorker` never emits `threadLoaded`. Both were verified by hand and by a standalone model instead: - A queued `threadLoaded` repainting a pane that a multi-row selection had just blanked. The fix guards `onThreadLoaded()`, and that guard is untested: the test passes with it reverted. - A local sync reporting itself as a background one. Staging it needs `MailSync::isRunning()` true at one transition and false at the next, which needs a configured sync command and a live child process. That was tried and abandoned: it left a process running for the length of the suite and popped a dialog on the user's screen. **The machinery already exists and is not the gap.** `tests/notmuchfixture.h` builds a throwaway Maildir in a `QTemporaryDir`, runs `notmuch new` with `NOTMUCH_CONFIG` scoped to the test process, and `addMessage()` takes message-id, subject, from, date, body, unread and in-reply-to, so threads come from real `In-Reply-To` chains. `test_notmuchworker` uses it. `test_mainwindow` does not. **Explicitly rejected: a corpus built from the user's real mail.** Considered and declined 2026-08-04. The global rule against personal data in fixtures forbids it, git hooks enforce it, and this repository is public. Scrubbing is also the fragile path: one surviving `In-Reply-To` or `References` header names a real correspondent, and a scrubbed message no longer demonstrates what the real one did. Where a real message exposes a parser bug, hand-write a minimal `.eml` reproducing its **shape**, as `truncated.eml` and `hostile_filename.eml` already do. **Approach.** Give `test_mainwindow` the fixture and point the window's worker at it, so a test can select a thread and let a real `threadLoaded` arrive. **Do this when a defect needs it, not before** (user, 2026-08-04). Wiring it with nothing to test proves nothing. The two defects above are already fixed; this item is the note that the next one of its kind should be tested properly rather than modelled. **Constraint:** every existing `test_mainwindow` case constructs a bare `MainWindow` and must keep working. The fixture is per-test, not a suite-wide `initTestCase`, or every case pays for a `notmuch new`. ## 40. No live filter over the current view **Observed (user, 2026-08-05):** "search in current view", spelled out as two things: "a light filter applied live on the current view", and "a search bar appearing as soon as we type while no entry box is focused". **Cause (verified in code):** the only search is the query bar, which runs a notmuch query and replaces the result set. There is no client-side filtering of an existing result: no `QSortFilterProxyModel` anywhere in `src/`, and `ThreadListModel` has no filter of its own. Narrowing the current view therefore means writing a new notmuch query and losing the view. **Approach.** Distinct from the query bar, and the distinction is the point: this filters rows already fetched, without touching notmuch. - A filter over the model's loaded rows, matching subject and from, case insensitively. No worker round trip. - A filter strip that appears on the first keystroke while no entry box has focus, and disappears on Escape, restoring the full result set. **Constraints.** - **Type-to-filter competes with the plain-letter shortcuts.** Item 3's outcome records that a plain-letter `QAction` shortcut is suppressed only while an editable widget has focus, which is exactly the state this feature does not start in. Any binding that is a bare letter would be swallowed by the filter strip or would swallow it. Check the current defaults before choosing the trigger, and prefer appearing only for characters no action claims. - Escape already blanks the message pane (item 32). If Escape also closes the filter, decide the precedence explicitly rather than letting whichever handler runs first win. - The filter is presentation only: it must not clear the selection, the undo stack, or the query, and the pending-edit count must not move. - Interaction with item 39: a filter and a sort over the same rows want the same proxy. Whichever is built first should leave room for the other. ## 65. No full code review and optimization pass **Observed (user, from the notes):** "full code review and optimization." **Cause:** not a defect. The codebase has grown from the 0.1.0 spec through sixty-odd backlog items, and nothing has gone back over it as a whole. **Why this cannot be planned from the backlog.** "Review and optimize" names no symptom, no measurement and no target. There is no reported slowness to chase, and the one performance property the design does commit to (threads emitted in batches of 200 so a 10k-thread query paints immediately) already holds. An optimization pass with no measurement behind it is the kind of work that produces a large diff and no change a user can notice. **What it needs before it can be sized.** The user saying which of these they meant: a correctness/security review of a named area, a specific operation that feels slow with the query that makes it slow, a dead-code and duplication sweep, or the translatability audit that is already item 22. The first three are different pieces of work with different sizes, and the fourth is already recorded. **Size: `?`, unspecified.** Do not propose a design for this; ask. ## 66. Selecting a thread root leaves the message pane blank until a reply has been selected **Observed (user, from the notes):** clicking the main message of a thread shows nothing in the right pane. After expanding the thread and selecting a reply, clicking the main message again renders it correctly. The user filed a second observation as a possible duplicate: clicking the main message shows the whole thread with stubs and the last two replies expanded, and after clicking a reply and going back to the root, only the root's own message is shown. **Cause (partly verified, and the two halves are not the same fault).** The second half is not a defect at all. A thread row renders the conversation and a message row renders one message; `onThreadSelected` (`src/mainwindow.cpp:1847`) branches on `isMessageRow` for exactly that. What breaks the user's expectation is that going back to the root should return to the conversation view, and it does not always: `m_currentMessageId` is set by the message branch (`src/mainwindow.cpp:1860`) and `onMessageLoaded` refuses to render when it is empty (`src/mainwindow.cpp:1924`), so the two paths are ordered by which reply lands first, not by which row is current. This is the "a queued load can outlive the state that started it" class already recorded in CLAUDE.md. The first half, a blank pane on the very first click, has a candidate and only a candidate: `onThreadSelected` returns without loading anything when the index is not yet selected (`src/mainwindow.cpp:1804`), a guard added so that a view taking focus does not open and mark-read a message the user never looked at. If a click can deliver `currentRowChanged` before the selection model is updated, which CLAUDE.md records as this signal's documented behaviour, that guard drops the load and `onSelectionChanged` only reloads when the row CHANGED (`src/mainwindow.cpp:1721`), which it did not. **Approach:** do not fix from this reading. The candidate above is exactly the kind of plausible cause the project has been burned by twice; instrument the running application and watch the order of `currentRowChanged`, `selectionChanged` and the worker reply for one first click on a cold view. `test_mainwindow` cannot reproduce it: it has no worker and never fires `threadLoaded`. **Constraints:** the mark-read guard at 1804 must survive whatever is done, it is item 35b's fix and removing it re-opens marking mail read without the user having seen it. A fix that only re-orders the two guards will look correct and will still be timing-dependent. **Size: S**, and a defect rather than an enhancement. ## 68. A forwarded subject gets no `passed` tag **Observed (user, from the notes):** "passed tag should appear when subject is `Fwd:` and `Fw:`." Refined in session on 2026-08-11: the user had noticed `passed` appearing on messages whose subject carried `Fwd:` and not on `Fw:`, and asked to expand the rule to both. **Cause:** there is no rule to expand. `passed` is the Maildir `P` flag in the message filename, translated into a tag by notmuch because `maildir.synchronize_flags=true`. The flag is written by whichever client forwarded the message, or by the server over IMAP; nothing reads a subject line anywhere in the chain. qtmaildir only ever colours the tag (`src/tagcolors.cpp:36-37`) and the database's `post-new` hook does not mention it either. **Measured against the real database (2026-08-11):** | Query | Count | |---|---| | `tag:passed` | 6 | | `tag:passed and subject:"Fwd:"` | 1 | | `tag:passed and subject:"Fw:"` | 0 | | `subject:"Fwd:" and not tag:passed` | 194 | | `subject:"Fw:" and not tag:passed` | 28 | Six tagged messages in the whole database, and every one of them carries `P` in its filename flags. The single overlap with `Fwd:` is a message that was forwarded and whose subject was already a forward, not evidence of a rule: 194 `Fwd:` subjects carry no tag at all. The correlation the observation rests on does not exist. **Approach and the decision it needs first.** Two different features, and the measurements above decide how far apart they are. *Display only.* The card shows a forwarded mark when the subject matches. Touches no mail, changes no flag, reversible by deleting the rule. XS. *Write the tag.* qtmaildir sets `P` from a subject heuristic. With `maildir.synchronize_flags=true` that flag is a filename change that mbsync carries out to the server, on 222 existing messages, on a guess about a string. Not cleanly undoable, and it asserts a meaning for a flag this application did not define. Recommended against; recorded so the choice is deliberate rather than forgotten. **Constraints:** localised clients use their own prefixes, and `Fwd:` can appear inside a subject rather than at its head, so whatever matches must be anchored. If the tag is ever written, it must not be re-applied on every sync in a way that produces pending edits the user never made, item 28 is the record of a count going wrong. The display-only route avoids that entirely, since it derives the mark at paint time and stores nothing. **Size: S** as written, XS if it is display only. Most of it is the decision, not the code. **Status:** left open deliberately on 2026-08-11. The cause is settled and the options are costed; the user has not chosen, and no code was written. ## 72. No khard/khal integration **Observed (user, from the notes):** "investigate khard/khal integration (light PIM, probably worthy after we add send capabilities)." **Cause:** not a defect. v1 is read-and-organize; there is no address book and no calendar anywhere in the codebase. **Why this cannot be planned.** The user's own note places it after send, and send is v2. What "integration" means is undecided: completing recipients from khard when composing, showing a sender's card, or acting on an invitation. Those are three different features. **Size: `?`, unspecified**, and out of scope until v2 exists. Ask before designing anything. ## 74. The first query after boot sits on "Searching..." for seconds **Observed (user, 2026-08-11):** the first start of the day takes noticeably longer to show its default view, and the delay happens while the status bar reads "Searching...". **Cause: the notmuch index paging in from disk, not this application's code.** Measured by instrumenting `main()`, the `MainWindow` constructor and `NotmuchWorker::runQuery` behind an environment variable, then running the same `tag:inbox` query (4444 threads) warm and again after evicting the index from the page cache with `posix_fadvise(POSIX_FADV_DONTNEED)`: | phase | warm | cold | |---|---|---| | `notmuch_query_search_threads` returns | 0 ms | 411 ms | | first batch of 200 reaches the model | 10 ms | 642 ms | | walk complete, all 4444 threads | 154 ms | 5714 ms | A 37x difference over the identical code path. The index measured 1.1 GB. The in-process startup costs nothing by comparison: `QApplication` in 20 ms, the whole `MainWindow` constructor in ~120 ms, and the window is shown and interactive at ~196 ms in both the warm and the cold run. **There is nothing to fix in the query path**, and the measurement exists mainly so this is not re-investigated. Two things it did establish that are worth keeping. The default startup view is whichever saved query `startup_query` names, defaulting to `Unread`, so a user with an empty unread view never sees this at all and a user whose default is Inbox always does. And batching already works: rows land from 642 ms cold, long before the 5714 ms finish. **The one real defect it exposed is the status bar.** "Searching..." is set once in `runQuery` and cleared only on `queryFinished`, so it keeps claiming the query is running for the full 5.7 s while rows are visibly arriving behind it. That makes a slow query read as a frozen one. The fix is to update the text per batch with the count so far rather than holding one string, which changes no timing and only stops the bar from lying. **The user declined this on 2026-08-11**, having asked for the explanation rather than a change. Recorded as open because the status bar is still inaccurate, not because anything is expected to happen. **Size: XS** for the status bar. The cold-cache cost itself is not addressable here and should not be attempted: prefaulting 1.1 GB at startup to make one query look fast is a worse trade than the wait. ## 78. No way to build a rule from something visible in a message **Observed.** The user would like to select an address or another piece of a message in the main window, right-click, and be offered a rule built from it. **Cause.** Not a defect, unbuilt. **Narrowed 2026-08-14, and most of the work is already done.** The search half shipped as item 85, which is the road a rule is made from: search for a value, save the query, create a rule from the saved query. What remains here is a SHORTCUT across that road, and both of its halves now exist. - The menus are built and every surface already extracts its value as a finished query (`SearchOffer`, `src/searchterm.h`). A rule entry is another action beside the two search ones, not new plumbing. - The seeded-dialog path exists from item 81: `MainWindow::showTagRulesDialog(const TagRule &seed)`. A rule from a message becomes a second caller of it, with a different seed, which is what item 81's spec anticipated when it made the seed a whole `TagRule` rather than a query string. **Approach.** Decide it after using item 85 for a while. Which values are worth promoting straight to a rule is a usage question, and the earlier answer to it was wrong (see below), so it is worth having the evidence first. **Constraints.** The original approach here said to start from the thread list's context menu "where the sender is already a value the model holds". **That is false and item 85 verified it.** `ThreadSummary::authors` comes from `notmuch_thread_get_authors` and is a DISPLAY SUMMARY, reading `Alice, Bob` or `Alice| Bob`, so a `from:` built from it matches nothing. A real address comes from `MessageNode::from` or `ParsedMessage::from`, neither of which the thread list carries. Any thread-list entry needs an address resolved from a message first. JavaScript is disabled in the profile and must stay disabled. Item 85 reads a body selection with `QWebEnginePage::selectedText()`, which injects no script; reuse that rather than adding anything. The rules file is shared with mailctl, so a rule created here must go through `TagRules` and preserve unknown fields; see "Changing the shared rule format" in CLAUDE.md. **Size: S**, down from M now that item 85 has built the menus and item 81 the seeded dialog. ## 84. A config problem blocks `test_mainwindow` on a modal nobody can dismiss **Observed (2026-08-14):** a new test in `test_mainwindow` hung with no output and was killed at the two-minute timeout. It had configured an account section carrying only `sent=`, with no `maildir=`. **Cause (verified by attaching gdb to the hung process, not inferred).** ``` #7 QDialog::exec() #9 MainWindow::showWarnings ... src/mainwindow.cpp:1670 #10 MainWindow::MainWindow ... src/mainwindow.cpp:381 ``` `Config::load` handles the malformed account exactly as it should: it records "Account 'one' has no maildir; ignoring it" and carries on (`src/config.cpp:413-418`). `showWarnings()` then puts every collected problem in a `QMessageBox::warning`, which is modal, and it is called from the `MainWindow` CONSTRUCTOR. Under the offscreen platform nothing can dismiss it, so the constructor never returns. **This is not a defect in the application.** The modal is deliberate and is right for a person: a config problem should interrupt startup rather than scroll past, and the code comment at `src/mainwindow.cpp:1661` explains which problems qualify. A user sees the dialog and clicks OK. The defect is that a TEST cannot, and the failure it produces is a silent hang rather than an error naming the cause, which cost a debugging detour to identify. **Constraint on any fix: the modal must survive for real use.** Suppressing it whenever `QTEST_MAIN` is linked would be the obvious move and is wrong, since that is exactly the path `test_mainwindow` exercises and a suppressed dialog means the startup warning ships untested. Two candidates, neither yet chosen: - A `MainWindow` flag, defaulting to showing the modal, that the tests set. It makes the behaviour explicit and testable in both states. - Collecting the problems and emitting them, with the modal raised by a caller outside the constructor. Larger, and it separates "what is wrong" from "how the user is told", which is the better shape if anything else ever needs the list. **Related: every test that configures an account is one typo away from this.** The suite has several, all of them currently well-formed. A malformed one does not fail, it hangs, and a hang in CI reads as an infrastructure problem rather than a test problem. **A second trap sits on top of the first and wasted as much time.** A hang leaves the test binary running, and a later `ctest` then runs a STALE binary while the source on disk has moved on, so the failure appears to persist after it has been fixed and to vanish for reasons unconnected to the change. Kill any surviving `test_mainwindow` and rebuild before concluding anything about a hang here. **Size: S.** The diagnosis is the expensive part and it is already done. ## Deferred, unsized, or split out Items noted while triaging but not part of the original list. Same numbering sequence, appended as they arise. | # | Item | Why here | |---|------|----------| | 12 | `HtmlBuilder` CSS is light-theme only | **Done 2026-08-07**, and moved to the main status table. Kept listed here so the split from item 5 stays traceable. | ## Adding to this document Append a row to the status table with the next free number, then a section using the same shape: **Observed** (what the user saw), **Cause** (the code, with file and line, verified not assumed), **Approach**, **Constraints**, and **Verification** where it is not obvious. Do not renumber. Do not delete: mark `dropped` with a reason. **When an item closes, move its section to `2026-08-03-post-0.1.0-usability-closed.md`** and leave the status table row here with its date and outcome. This is what keeps the file readable, and it is the step that was missing for seventy items: doing it only once, as item 73 did, buys a few months and then the problem returns. Move the section on the commit that closes the item, not in a later cleanup pass. Where the closed section records a trap that is still true of the code, that trap belongs in `CLAUDE.md`, which is where it will actually be read. **A fully specified item goes in its own file under `docs/superpowers/specs/`, not inline here.** This document is a backlog: its job is to say what is open, how big it is, and what decides whether it can be picked up. A design that runs to a hundred lines buries that under itself. The split is by depth, not by size on the day. An entry stays here while it records an observation, a cause and an approach. It moves out once it carries decisions the user made, measured evidence, and constraints that have to be read before writing code. Items 53 and 63 are the pattern: the entry keeps the finding and the size, and points at the spec with one line saying to read that instead. Carry the two or three constraints a reader needs in order to decide whether to open the spec at all, and leave the rest there. Name the spec `--design.md`, and state in its header which backlog items it resolves, so the numbering stays traceable in both directions.