summaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-04 10:32:42 +0200
committerDanilo M. <danix@danix.xyz>2026-08-04 12:54:39 +0200
commit1a8fdb48140cb40243bd46bff94d39f610d70ef3 (patch)
treea53c9064dcffd6ca66f9eb4cb3447b0b11e79ee1 /docs
parent7ecdd3fe942a02b80cf4e7cf351aa15d44b48934 (diff)
downloadqtmaildir-1a8fdb48140cb40243bd46bff94d39f610d70ef3.tar.gz
qtmaildir-1a8fdb48140cb40243bd46bff94d39f610d70ef3.zip
docs: record the item 2 decision, postpone 10, add items 18-20
Item 2 (message details) gains the user's decision on where the details go. The header adapts to what it can say honestly: a single-message thread shows From, To, Cc and Subject, while a multi-message thread keeps showing only subject and count. Everything else moves to a popup behind a button on the right of the header. An earlier draft also put a recipient line on the thread header, which forced a choice between the union of recipients and their intersection and would have needed real address parsing to compute either. The user called that overcomplicating and dropped it, so no address parsing is needed and the item is UI work over strings MimeParser already fills. The item's own "check before building" question is answered in place: To and Cc are parsed at mimeparser.cpp:344-345 and then dropped at the renderer, which never interpolates them. The larger task it warned about does not exist. Item 10 is postponed at the user's request rather than dropped: the complaint was real and the cheap first fix it proposes still stands, it is simply not wanted now. Only its startup-query half ever shipped. Items 18 and 19 come from the user's own notes and were missing here: a visual cue for unsynced edits, and a sync-on-exit prompt with a config option. 18 records a finding that shapes both: the QUndoStack looks like a record of pending edits but is cleared on every query, so it cannot drive the indicator and a separate counter is needed. Item 20 records, unspecified, that the user's mental model of the thread view differs from what was built. Nothing is designed there yet; the next step is asking what they pictured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs')
-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