diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-08 11:49:02 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-08 11:49:02 +0200 |
| commit | 69281b218d70737f7a9ce7824f5c0a53d76456e4 (patch) | |
| tree | 2a28f3f243ebeece089a78e8ca1f87e90c9c4b13 /docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | |
| parent | 03f113dc0f6e8db13f600f2650e1536f4c09da21 (diff) | |
| download | qtmaildir-69281b218d70737f7a9ce7824f5c0a53d76456e4.tar.gz qtmaildir-69281b218d70737f7a9ce7824f5c0a53d76456e4.zip | |
docs: carry the item 20 and 53 backlog entries onto master
The work for item 20 lives on the branch item-20-message-rows, parked and not
merged while the user explores a layout other than the table. Without these
documents on master the session-start backlog read would miss item 53 entirely
and item 20 would still read as unspecified, so the next session would either
re-ask a question already answered or start designing against a rejected design.
Docs only. No code from the branch is merged here, and the entries say so in
three places: a note above the status table, item 20's own section, and a banner
on the plan document telling a future agent not to execute it again.
File and line references were checked against master rather than copied. Item
53's citation of the column-grid cause pointed at threadlistmodel.cpp:275-283,
which on master is unrelated font handling; it now names the branch and the
function instead of a line range that would send a reader to the wrong code.
CLAUDE.md and CHANGELOG.md were deliberately NOT brought across. Both describe a
QTreeView and features master does not have, and a CLAUDE.md that misdescribes
the architecture is worse than one that is merely out of date.
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 | 176 |
1 files changed, 166 insertions, 10 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 ffdc8d5..6beb35b 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 @@ -26,6 +26,15 @@ 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. +**One item's code is not on master.** Item 20 was built in full on the branch +`item-20-message-rows` (pushed, not merged) and parked: the user wants to +explore a layout other than the table before deciding. This document and the +plan beside it were brought onto master on purpose, so the backlog reads +correctly at session start, but master has none of that code. Any file and line +reference in items 20 and 53 points at the BRANCH. Item 53 is where the +reservation and the four possible directions live; read it before touching +either. + ## Theme 0.1.0 was built to a spec written by someone who lives in neomutt. The result @@ -68,7 +77,7 @@ taking that too literally. | 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 | ? | open, unspecified | +| 20 | Thread view does not match the user's mental model | presentation | L | built on branch `item-20-message-rows`, **not merged**; 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 | open | @@ -101,6 +110,7 @@ taking that too literally. | 50 | Esc blanks the pane but leaves the row selected | workflow | XS | **done** | | 51 | Clicking a subject scrolls the list sideways | presentation | XS | open | | 52 | `test_querycompleter` fails under Wayland, passes offscreen | testing | XS | **done** | +| 53 | Message rows still read as a table, not as a conversation | presentation | ? | open, unspecified | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -1149,12 +1159,43 @@ does fail it. 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 +**Specified 2026-08-08.** The user described the model with three Thunderbird +screenshots and four decisions. It is **not a message-pane redesign**: the +change is in the LEFT pane. A thread occupies one row carrying an "N replies" +affordance; expanding it reveals the replies as rows in the same list, indented +by reply depth; clicking one opens that single message in the reading pane. The +unit of selection stops being the thread and becomes the message, which is the +substance of the item and the source of its risk. + +**Decisions taken (user, 2026-08-08), each closing an option that the +screenshots alone did not settle:** + +1. **The root row IS the thread's first message**, not a thread-level summary + row. Expanding reveals only the REPLIES, so a thread of 7 shows 1 root and 6 + children, matching "6 risposte" in the screenshot. +2. **A true reply tree, indented by depth**, not a flat chronological list of + replies. notmuch supplies the structure via `notmuch_message_get_replies`. + Deep chains indenting off-screen and malformed `References` headers producing + misleading trees are accepted costs, to be capped in the view rather than + flattened in the model. +3. **Action scope follows the selected row.** A message row acts on that + message, a thread root acts on the whole thread. +4. **No confirmation dialog**, per this project's standing rule; the scope is + made visible instead, in the status bar, BEFORE the action ("1 thread + selected (7 messages)") and AFTER it ("Deleted 7 messages (whole thread)"). + The hazard this design introduces is not destruction, since `deleted` is a + tag and every mutation is already invertible on the undo stack. It is + **ambiguity**: with two kinds of row selectable, a keypress alone no longer + says whether it hit one message or seven. Naming the scope removes the + ambiguity where it exists, which a dialog on every correct action would not. + The user asked for a warning popup, was shown the rule in `CLAUDE.md`, and + chose this instead. + +**Improvement the user named but deferred:** moving from one message to the next +within a thread without returning to the list. An addition on top, not part of +this item. + +**What exists today**, as the starting point: 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 @@ -1168,9 +1209,58 @@ 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. +**What the codebase does not have yet**, verified 2026-08-08 rather than +assumed: + +| Needed | Status today | +|---|---| +| Per-message From / Subject / Date | Absent. `MessageRef` (`src/types.h:54`) carries only id, path, tags, matched | +| Reply parent, for indentation | Not carried at all. notmuch has it, the worker never asks | +| A tree-capable left pane | `ThreadListModel` is a `QAbstractTableModel`; `ThreadListView` is a `QTableView` | +| Loading ONE message into the pane | Absent. The worker exposes `loadThread` only | +| An expand affordance on the row | Nothing | + +**The two loads bearing the risk.** + +*The view port.* `ThreadListView` exists because a delegate cannot paint outside +its column, so the tag strip is painted across the full row width in +`paintEvent`. That code is written against a table. Mitigating fact, checked +rather than hoped: every geometry call it uses (`rowAt`, +`rowViewportPosition`, `rowHeight`, `columnViewportPosition`) exists on +`QTreeView` with the same semantics, so this is a port, not a rewrite. What does +NOT carry over is `isRowSelected(int)`, and the strip must not paint under +child rows the way it does under thread roots. + +*The selection semantics.* Every action path today assumes a selected row is a +thread: `applyTagsToThreads` resolves `thread:a or thread:b`, and the undo stack +and pending-edit map are thread-scoped throughout. Mitigating fact: they all +funnel through `m_model->threadAt(current.row())`, so the row-to-thread mapping +is centralised rather than smeared across call sites. + +**Size: L.** Larger than anything else in this backlog, and the first item to +warrant a feature branch (`item-20-message-rows`, 2026-08-08). It is a left-pane +model and view replacement plus a selection-scope change, not a refactor. + +**Built 2026-08-08 across ten tasks, on the branch `item-20-message-rows` and +NOT merged.** master carries these documents and none of the code, deliberately: +the user parked the branch to explore a different layout first. Read item 53 +before reviving it. All four decisions were implemented: message +rows in the left pane, the root row as the thread's first message, replies +indented by depth, action scope following the selected row kind and named in the +status bar. 15 test binaries green, 65 tests in `test_mainwindow` alone, ten +mutation checks. + +**The user's verdict on the result: not convinced it fits.** Recorded here +rather than in a commit message, because the item shipped working and the +reservation is about the DESIGN, not about a defect. See item 53, which carries +the diagnosis. Do not treat item 20 as a success story to build on without +reading it. + +**A cheaper alternative was offered and declined.** Collapsible `<details>` +blocks per message inside the existing single-document message pane would have +delivered per-message inspection at size S, touching only `htmlbuilder.cpp` and +neither fragile area, but gives no message rows in the list and no reply-tree +indentation. The user chose the full model deliberately. ## 21. Default shortcuts are not sensible enough @@ -3082,6 +3172,72 @@ popup how it likes. makes the width guard fail with its explanation, where before the change that case would have passed. +## 53. Message rows still read as a table, not as a conversation + +**Observed (user, 2026-08-08)**, on the finished item 20: *"I'm not very +convinced about this session's work. I don't think the table view fits our +use."* Said after the expander, the indent, the thread spine, the tint and the +dimmed text were all in and working, so it is not a report that a cue is +missing. It is a judgement on the result. + +**This is a design finding, not a defect.** Item 20 shipped exactly what its +four decisions specified and every one of them was the user's own choice. The +work is sound; what it produced is not what was wanted. Recording it as a defect +would misattribute the cause, and recording nothing would leave the next session +building on a design the user has already rejected. + +**Cause (verified in code, 2026-08-08).** A message row fills the SAME five +columns as a thread row: `ThreadListModel::data` answers `DateColumn`, +`AuthorsColumn` and `SubjectColumn` for message rows in the same switch that +answers them for threads. One model, one column grid, both row kinds. So every +reply lands on the same rigid column boundaries as the threads around it, and +the eye reads columns before it reads indentation or tint. + +**Line numbers deliberately omitted.** That code is on the branch +`item-20-message-rows`, not on master, where the same lines are unrelated. To +read it: `git show item-20-message-rows:src/threadlistmodel.cpp` and find the +`isMessageRow(index)` branch of `data()`. + +Compare the reference the user gave. In the Thunderbird screenshots the reply +rows carry sender and date only, laid out freely on a plain band, with no column +rules running through them. The structure comes from the ABSENCE of the grid, +which is the one thing three added cues cannot supply. + +Two aggravating details, both visible in the 2026-08-08 screenshots: + +- Every reply repeats `Re: <the thread's subject>`, near-identical down the + whole block, which is exactly the visual signature of a table of records. + Dropping the redundant prefix was offered and not chosen; it is worth + revisiting first because it is the cheapest of these by a wide margin. +- Reply rows keep the same row height as thread rows, since + `setUniformRowHeights(true)` is required for the tag strip's band arithmetic + (on the branch, in `MainWindow`'s view setup). A conversation view would want + tighter replies. + +**Approach: unspecified, and deliberately so.** Ask the user what to change +before proposing anything, exactly as item 20 required. The plausible directions +differ enormously in cost and are not interchangeable: + +1. **Span the columns for message rows.** Draw a reply as one free-form band + (sender, date, no grid) rather than as cells. `QTreeView::setFirstColumnSpanned` + does this per row without a second model. Cheapest real change, keeps + everything else built. +2. **Drop the `Re:` prefix and shrink what a reply shows.** XS on its own, and + worth trying before anything structural. +3. **Two different row shapes.** A delegate that paints a reply row entirely + itself, ignoring the columns. More control, and it fights `uniformRowHeights`. +4. **Abandon message rows in the list** and revisit the `<details>`-per-message + design in the message pane, which was offered at size S on 2026-08-08 and + declined in favour of this. Item 20's work would largely be reverted. + +**Constraint that shapes all of them.** The tag strip is why `ThreadListView` +exists, and its band arithmetic assumes a uniform row height and a known column +layout. Anything that varies row height or removes columns for one row kind has +to answer for the strip on thread rows, which must not change. + +**Do not start any of these from this description.** It says what is and why, +not what was wanted. + ## Deferred, unsized, or split out Items noted while triaging but not part of the original list. Same numbering |
