aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/plans')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md182
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