summaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md100
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md68
2 files changed, 101 insertions, 67 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
index ef5c68f..30271d5 100644
--- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
+++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
@@ -5524,3 +5524,103 @@ name their tags, so the UI can answer from `~/.config/mailrules/rules.json` at
the moment it is asked, retroactively and with nothing stored. Reopen from
there, not from the provenance design, unless real use shows the per-message
distinction is what is wanted.
+
+## 91. Double-clicking a thread should open it on its own
+
+**Observed (user, notes):** "double clicking a message/thread should open it in
+its own view (?) UX not sure."
+
+**Specified by the user, 2026-08-15**, in three cases. The view is always the
+whole THREAD; what changes is which message the pane shows:
+
+- Double-click a single message: that message in a view by itself, loaded in
+ the right pane.
+- Double-click a thread: the whole thread, expanded, in a view by itself, with
+ the FIRST message in the right pane.
+- Double-click a reply inside a thread: the whole thread, expanded, in a view by
+ itself, with THAT REPLY in the right pane.
+
+So a reply does not drill down to itself alone. It drills to its thread, with
+itself selected. That is the part an implementation is most likely to get wrong,
+because the obvious reading of "open it by itself" is an `id:` query on the
+reply, and it is not what was asked for.
+
+**It is NOT a second window.** The user ruled that out explicitly: "I didn't
+mean open in a new window." No second `QWebEngineView`, no extra Chromium render
+process, nothing reparented.
+
+**Cause (verified in code):** nothing is connected to `doubleClicked` on
+`ThreadListView`, so a double-click today is two single-clicks and selects the
+row twice.
+
+**Approach: this is `recoverStaleThread()` triggered by a gesture.** That
+function already does the entire job for the stale-thread notice: it sets
+`m_recoverThreadId` and `m_recoverMessageId`, runs a `thread:` query, expands
+the thread when the row arrives, selects the target reply once the replies land,
+and falls back to selecting the thread when the message has gone. Every case
+above maps onto it, with the recovery target set to the double-clicked row's own
+message. Reuse it rather than writing a second selection-after-query path.
+
+Two properties of that code are exactly what this item needs and are already
+commented in place: the root card IS the thread's first message and is not among
+the children, so a thread double-click is the empty-target case; and a reply
+cannot be selected until the replies exist, so the first pass selects the thread
+provisionally and refines on the next.
+
+**Expansion is not automatic and must be asked for.** Nothing in the tree
+auto-expands: `expandAll` and `setExpanded` appear nowhere in `mainwindow.cpp`,
+so a plain `thread:` query lands on ONE COLLAPSED CARD and the user would still
+have to click the expander. `recoverStaleThread()` calls `expand()` on the
+thread, which is also what asks the worker for the replies.
+
+**How the user gets back: nothing is built for it.** Answered by the user:
+"I didn't think of a back action, usually I'd go back to a known list like
+unread or inbox at that point." The filter buttons already are that and are one
+click away in every view. A Back action, a history stack and a
+restore-the-previous-query scheme were all drafted and are all unnecessary.
+
+**The undo-stack cost is inherited and acceptable.** `runQuery()` clears the
+undo stack because its rows are about to be discarded, and a drill-down discards
+them exactly as a typed query does. The user already expects that from the query
+bar, so the gesture inherits consistent behaviour rather than a surprise. No
+filter-over-the-model alternative is needed, which is what would have made this
+item 40's machinery and M rather than S.
+
+**Constraint.** A double-click also delivers a single click first, which selects
+the row and arms the mark-read timer. The drill-down must not mark a message
+read that the user only passed through, and the existing single-click path is
+what arms it, so the double-click handler has to cancel it the way the
+multi-row branch of `onThreadSelected()` does.
+
+**Outcome, 2026-08-15. Shipped, hand-confirmed green by the user.**
+
+`onRowDoubleClicked()` resolves the row to a thread id and a message id and
+hands both to `recoverStaleThread()`, which already did the entire job for the
+stale-thread notice: run `thread:<id>`, expand the thread when the row arrives,
+select the target message once the replies land, fall back to the root when the
+message has gone. All three of the user's cases are existing paths through it,
+so the feature is a connection and a resolver rather than new machinery.
+
+Two details that were nearly built wrong:
+
+- **A reply drills to its THREAD, not to itself.** The obvious reading of "open
+ it by itself" is `id:<reply>`, and one of the three tests exists specifically
+ to fail against that.
+- **The row is reached through the INDEX, never `index.row()`.** A tree numbers
+ rows per parent, so `threadAt(row)` on a reply answers about an unrelated
+ thread. This is item 88's trap, avoided here by construction.
+
+**The first click of a double-click arms the mark-read timer**, and the handler
+cancels it: a gesture that navigates must not mutate mail. The timer is armed
+again for whichever row the recovery lands on, so this cancels only the arming
+for the row being left. Its test asserts the timer WAS active before the
+double-click, so it cannot pass by the timer never having been armed.
+
+**The expander keeps its own double-click.** `ThreadListView::mousePressEvent`
+accepts a press inside the expander rect and returns, so Qt never pairs one into
+a double-click there: double-clicking the reply count toggles expansion twice,
+double-clicking anywhere else on the card drills in. That is the right split and
+it falls out of the existing code rather than needing a guard.
+
+**Nothing was built for getting back**, per the user: the filter buttons already
+are that.
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 aefa6a5..7b44e7e 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
@@ -155,7 +155,7 @@ taking that too literally.
| 86 | A right-click search can replace or narrow, but never exclude | workflow | S | **done** 2026-08-14, unreleased; see `specs/2026-08-14-exclude-from-search-design.md`. Follows 85. The `extend` bool became a `SearchMode` enum across four signatures |
| 89 | A sync moves the list under the user's hands, and the auto-sync skips rather than retries | workflow | XS | **done** 2026-08-15, unreleased. The timer half only: a skipped auto-sync re-arms instead of giving up. The list-churn half is **dropped**, not built: the user resolved it as a mental-model question, an Unread view is SUPPOSED to be volatile |
| 90 | A saved-query button clears the account selection | workflow | S | **folded into 93** 2026-08-15. Not fixed in place: the button that misbehaves stops being a saved query at all. See `specs/2026-08-15-builtin-filters-design.md` |
-| 91 | Double-clicking a thread should open it on its own | workflow | S | open, fully specified 2026-08-15 and ready to build. The view is always the whole thread, EXPANDED; the pane shows whichever row was double-clicked, so a reply drills to its thread and not to itself. It is `recoverStaleThread()` triggered by a gesture |
+| 91 | Double-clicking a thread should open it on its own | workflow | S | **done** 2026-08-15, unreleased. The view is always the whole thread, EXPANDED; the pane shows whichever row was double-clicked, so a reply drills to its thread and not to itself. Reuses `recoverStaleThread()` outright |
| 92 | Nothing distinguishes a tag written by a rule from one the user applied | information | M | **postponed** 2026-08-15 at the user's request: "I don't see the utility, so I don't really know how to answer." Needs per-MESSAGE provenance nothing records, a two-repo format change blank on all existing mail. Reopen only if the need appears in use |
| 93 | The query buttons are whatever the user pinned, not a designed set of filters | workflow | M | **done** 2026-08-15, unreleased; see `specs/2026-08-15-builtin-filters-design.md`. Absorbs item 90. Four built-in filters composing with the account dropdown; the user's own queries unpinned, never deleted |
| 95 | A query in the overflow menu cannot be run | defect | XS | **done** 2026-08-15, unreleased. Pre-existing and not caused by 93: the entry's action owned a submenu, and Qt emits no `triggered` for those, so the connection had never fired. Surfaced because 93 moved every query into the menu |
@@ -493,72 +493,6 @@ bump either way: an ignored optional field is not a breaking change.
**Size: S.** Removing a field, two UI affordances and their tests.
-## 91. Double-clicking a thread should open it on its own
-
-**Observed (user, notes):** "double clicking a message/thread should open it in
-its own view (?) UX not sure."
-
-**Specified by the user, 2026-08-15**, in three cases. The view is always the
-whole THREAD; what changes is which message the pane shows:
-
-- Double-click a single message: that message in a view by itself, loaded in
- the right pane.
-- Double-click a thread: the whole thread, expanded, in a view by itself, with
- the FIRST message in the right pane.
-- Double-click a reply inside a thread: the whole thread, expanded, in a view by
- itself, with THAT REPLY in the right pane.
-
-So a reply does not drill down to itself alone. It drills to its thread, with
-itself selected. That is the part an implementation is most likely to get wrong,
-because the obvious reading of "open it by itself" is an `id:` query on the
-reply, and it is not what was asked for.
-
-**It is NOT a second window.** The user ruled that out explicitly: "I didn't
-mean open in a new window." No second `QWebEngineView`, no extra Chromium render
-process, nothing reparented.
-
-**Cause (verified in code):** nothing is connected to `doubleClicked` on
-`ThreadListView`, so a double-click today is two single-clicks and selects the
-row twice.
-
-**Approach: this is `recoverStaleThread()` triggered by a gesture.** That
-function already does the entire job for the stale-thread notice: it sets
-`m_recoverThreadId` and `m_recoverMessageId`, runs a `thread:` query, expands
-the thread when the row arrives, selects the target reply once the replies land,
-and falls back to selecting the thread when the message has gone. Every case
-above maps onto it, with the recovery target set to the double-clicked row's own
-message. Reuse it rather than writing a second selection-after-query path.
-
-Two properties of that code are exactly what this item needs and are already
-commented in place: the root card IS the thread's first message and is not among
-the children, so a thread double-click is the empty-target case; and a reply
-cannot be selected until the replies exist, so the first pass selects the thread
-provisionally and refines on the next.
-
-**Expansion is not automatic and must be asked for.** Nothing in the tree
-auto-expands: `expandAll` and `setExpanded` appear nowhere in `mainwindow.cpp`,
-so a plain `thread:` query lands on ONE COLLAPSED CARD and the user would still
-have to click the expander. `recoverStaleThread()` calls `expand()` on the
-thread, which is also what asks the worker for the replies.
-
-**How the user gets back: nothing is built for it.** Answered by the user:
-"I didn't think of a back action, usually I'd go back to a known list like
-unread or inbox at that point." The filter buttons already are that and are one
-click away in every view. A Back action, a history stack and a
-restore-the-previous-query scheme were all drafted and are all unnecessary.
-
-**The undo-stack cost is inherited and acceptable.** `runQuery()` clears the
-undo stack because its rows are about to be discarded, and a drill-down discards
-them exactly as a typed query does. The user already expects that from the query
-bar, so the gesture inherits consistent behaviour rather than a surprise. No
-filter-over-the-model alternative is needed, which is what would have made this
-item 40's machinery and M rather than S.
-
-**Constraint.** A double-click also delivers a single click first, which selects
-the row and arms the mark-read timer. The drill-down must not mark a message
-read that the user only passed through, and the existing single-click path is
-what arms it, so the double-click handler has to cancel it the way the
-multi-row branch of `onThreadSelected()` does.
## Deferred, unsized, or split out