diff options
Diffstat (limited to 'docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 182 |
1 files changed, 178 insertions, 4 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 1c36fee..e945aeb 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 @@ -49,13 +49,16 @@ taking that too literally. | 7 | HTML view should be default for HTML messages | behavior | XS | **verify first, may already be done** | | 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 | open | +| 10 | Reaching an account's inbox takes two steps | workflow | S | **postponed** (partly done) | | 11 | Icon, `.desktop` file, SlackBuild | packaging | M | **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 | open | | 17 | No completion for tags in the query bar | workflow | M | **done** | +| 18 | No visual cue that there are unsynced edits | feedback | S | open | +| 19 | No prompt to sync on exit when edits are pending | behavior | S | open | +| 20 | Thread view does not match the user's mental model | presentation | ? | open, unspecified | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -132,6 +135,16 @@ message struct, or does `src/types.h` / `MimeParser`'s output need extending first? If the fields are not parsed, that is the real first task and it is larger than the UI work. +**Answered, 2026-08-04: they are already parsed.** `MimeParser::parse()` fills +both (`src/mimeparser.cpp:344-345`, into `ParsedMessage::to` and `::cc`, +declared at `src/mimeparser.h:105`). The larger task the check warned about +does not exist. + +They are parsed and then **dropped at the renderer**: `HtmlBuilder` interpolates +only `from`, `subject` and `date` (`src/htmlbuilder.cpp:216`, `:244-245`), and +`to`/`cc` appear nowhere in it, in `messageview.cpp`, or in `mainwindow.cpp`. +So this is UI work only, as the item's own two-part approach assumes. + **Approach, two parts:** - Widen the persistent header at the top of the message pane to show the @@ -145,6 +158,40 @@ Both, not one: the header widget answers "who is this from" at a glance, the dialog answers "what actually happened to this message". They are different questions. +### Decided (user, 2026-08-04): the header adapts to the item count + +The pane shows a thread, not a message, so From/To/Cc are per-message while the +header is one strip. Rather than pick a message arbitrarily, the header shows +only what it can say honestly: + +- **One message in the thread:** From, To, Cc, Subject. The natural spot, and + every field is unambiguous. +- **N messages:** Subject and the thread count. Nothing more. +- **Everything else** lives in the popup, reached by a **button on the right of + the header** plus a keyboard shortcut. + +**No recipient line on a thread (user, 2026-08-04).** An earlier draft of this +decision put To on the thread header too, which forced a choice between the +union of recipients and their intersection: once the user has replied, message +1 is To: them and message 2 is To: the other party, so the intersection is +frequently empty and the union is really a participants list wearing the wrong +label. The user's call was that this is overcomplicating, and it is: the +per-message detail is what the popup is for. + +The thread header therefore keeps showing exactly what it shows today, subject +and count (`MessageView::updateHeader()`, `src/messageview.cpp:259`), and only +the single-message case gains fields. + +**Consequence: no address parsing is needed.** `ParsedMessage::to` and `::cc` +are raw header strings (`src/mimeparser.h:104-105`), and with no union or dedup +to compute they can be displayed as they stand. Splitting them into address +lists, which would have needed GMime's `internet_address_list_parse` to survive +a display name containing a comma, is not part of this item. + +**Noted for later, not now:** the user's mental model of the thread view +differs from what was built. That is a separate refactor and should not be +folded into this item. + **Constraint:** header values are untrusted input. The existing header label is `Qt::RichText` (`src/messageview.cpp:104`), so every value must be `toHtmlEscaped()` before interpolation, exactly as `updateHeader()` already @@ -412,9 +459,17 @@ matches nothing. Only a name the user wrote is worth a warning: the built-in default naming a query they never created is not something they got wrong. Neither half of item 10 proper is done: the account selection still resets on -restart, and reaching an account's inbox is still two steps. Persisting the -selection remains the next cheap step, and the reassessment the item calls for -should happen after that rather than now. +restart, and reaching an account's inbox is still two steps. + +### Postponed (user, 2026-08-04) + +**The user does not intend to go this route as of now.** Postponed rather than +dropped: the complaint was real, and the cheap fix the item proposes (persist +the account selection across restarts) is still the right first move if it is +picked up again. Nothing here is invalidated, it is simply not wanted yet. + +Only the startup-query half shipped, in 0.3.0. Do not propose the remaining +work unprompted. ## 11. Icon, `.desktop` file, SlackBuild @@ -650,6 +705,125 @@ pattern. `notmuch_database_get_all_tags()` is the underlying call. --- +## 18. No visual cue that there are unsynced edits + +**Observed (user, 2026-08-04):** tagging changes the notmuch index immediately, +but nothing in the UI says those changes have not reached the mail store. The +user cannot tell, at a glance, whether quitting now would leave work stranded. + +**Cause: nothing tracks it, and the obvious candidate cannot.** `MainWindow` +holds a `QUndoStack` (`src/mainwindow.h:142`) which looks like a record of +pending edits, but it is **cleared on every query** +(`src/mainwindow.cpp:805`, in the query-start path) because undo entries refer +to rows the new result set is about to discard. Tag a thread, then run any +query, and the stack is empty while the database change is still unsynced. The +same clear happens on a failed write (`:902`). + +So `QUndoStack::isClean()` is **not** usable as the signal here, and neither is +`canUndo()`. This needs its own counter, one that only a successful sync +resets. + +**Approach:** + +- Count confirmed mutations, incremented where `tagsApplied` is handled (the + same place that already clears `m_pendingChange`), and reset to zero on + `MailSync::finished(true, ...)`. A count, not a bool, so the indicator can + say how many. +- Show it in the status bar, next to the existing sync status rather than as a + new widget competing with it. Wording should name the unit the user thinks + in: threads or messages changed, not "mutations". +- Nothing modal, nothing blocking. This item is the passive cue only; the + question of interrupting the user belongs to item 19. + +**Constraints:** + +- **A failed sync must not clear the count.** `finished(false, ...)` means the + edits are still unsynced, and clearing there would assert the opposite. +- The count must survive a query, which is the whole reason it cannot ride on + the undo stack. Do not tie its lifetime to the model. +- An external `notmuch new` from the user's cron can sync changes without the + app knowing. The count is therefore a lower bound on confidence, not a + guarantee, and the wording should not promise more than it knows. + +**Verification:** tag a thread, run an unrelated query, confirm the cue +survives. Then sync and confirm it clears. Then make the sync fail and confirm +it does not. + +## 19. No prompt to sync on exit when edits are pending + +**Observed (user, 2026-08-04):** quitting with unsynced edits is silent. The +user asked for a blocking prompt offering to sync first, and suggested a +`sync_on_exit` config option. + +**Depends on item 18.** Both need the same "are there pending edits" counter, +and 18 establishes it. Build 18 first; this is the interruption layer on top. + +**Cause:** `MainWindow::closeEvent()` (`src/mainwindow.cpp:145`) saves UI state +and accepts unconditionally. It never consults sync state, and cannot today, +for the reason item 18 documents. + +**Approach:** + +- On close with a non-zero pending count, a modal question: sync now, quit + without syncing, or cancel. Three options, not two: a user who hit Quit by + mistake needs a way back that is not "sync". +- `[general] sync_on_exit`, as the user suggested. Sensible values are `ask` + (default, the prompt above), `always` (sync without asking), and `never` + (quit silently, today's behavior). A bare true/false cannot express all + three. +- Choosing to sync means the window must stay alive until `MailSync::finished` + arrives, since killing the process mid-sync is exactly the data loss the + prompt exists to prevent. Ignore the close event, show progress, and close on + the finished signal. + +**Constraints:** + +- **This is not a destructive-action confirmation** and does not contradict + `CLAUDE.md`'s rule against those. That rule is about tag mutations, which + keep undo instead of a dialog. This asks about *losing* work at a point where + undo no longer helps, which is the opposite situation. +- A sync that fails on exit must not silently discard the user's choice. Report + it and leave the window open rather than quitting as if it had worked. +- Quitting must remain possible when the sync command is not configured at all. + `MailSync::isAvailable()` is already false in that case, so the prompt should + degrade to a plain warning with no sync option rather than offering one that + cannot run. + +**Verification:** by hand. Tag, quit, take each of the three branches. Then set +each `sync_on_exit` value and confirm the behavior matches. Then quit with a +deliberately broken sync command and confirm the app neither hangs nor lies. + +## 20. Thread view does not match the user's mental model + +**Observed (user, 2026-08-04), in passing while deciding item 2:** "My view for +the thread visualization was different than what was built, but that's ground +for a small refactor later." + +**Unspecified on purpose.** What the user pictured has not been described yet, +so there is nothing here to design against. Recorded now only so the remark is +not lost, and because item 2 touches the same surface and might otherwise be +mistaken for having addressed it. + +**What exists today**, as the starting point for that conversation: a thread is +one HTML document in one web view, messages stacked in chronological order, +each with a small grey `.msg-header` carrying From and Date +(`src/htmlbuilder.cpp:241`). Messages that did not match the query render as +stubs (`MessageRef::matched`, `src/types.h:63`). It is deliberately flat: +`CLAUDE.md` records that reply structure is available from notmuch but is not +drawn as an indented tree. + +The single-document design is not incidental and constrains any redesign: a +`QWebEngineView` per message would spawn a Chromium render process each, which +is why the thread is one document and why `cid:` references are namespaced per +message. A refactor that splits messages into separate views has to answer that +cost first. + +**Next step: ask the user what they pictured** before proposing anything. Do +not design this item from the description above, which says only what is, +not what was wanted. + +--- + ## Deferred, unsized, or split out Items noted while triaging but not part of the original list. Same numbering |
