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.md59
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md1
2 files changed, 60 insertions, 0 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 55c5ee7..27b4e28 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
@@ -9315,3 +9315,62 @@ before proceeding. A summary left at the default is a MESSAGE row, so a test
meaning to exercise a conversation would quietly exercise the other branch and
pass for the wrong reason, which is the fixture trap the top-level document
records.
+
+## 181. The thread dashboard does not follow a write to the conversation it shows
+
+**Done 2026-08-29**, unreleased, on `thread-row-identity`, from the user's
+notes: "the thread dashboard doesn't update live with the modifications applied
+to the list pane. If I mark the thread as read, the dash still reports N
+unread".
+
+**Cause, verified in the code.** `ThreadDashboard` draws a `ThreadDigest`,
+which the worker builds from the INDEX and which reached the pane in exactly
+two places: the selection handler, and `onThreadDigestLoaded()`. A tag write
+updated the model optimistically and repainted the card beside it, and nothing
+touched the digest, so the pane went on reporting the unread count, the
+progress bar and the "Waiting for you" list the conversation had when it was
+opened.
+
+**Reachable from the dashboard's own button.** It carries Mark all read,
+Archive and Delete in its bottom strip (item 177), so the user presses Mark all
+read and the number directly above it does not move. That is the worst version
+of the defect and the one the note reports.
+
+**Fix.** `MainWindow::refreshDashboardDigest()` re-asks the worker for the
+digest of the conversation the pane is showing, and returns at once when the
+pane is showing anything else, which is the ordinary case. It bumps
+`m_digestGeneration` like any other request, so the three guards in
+`onThreadDigestLoaded()` discard a reply that arrives after the user has moved
+on. No placeholder digest, unlike the selection path: the pane already holds
+this conversation, and blanking it to re-fill it would flicker the whole
+dashboard for a change to one number.
+
+**Called from `onTagsApplied()`, where a write is CONFIRMED, and this is the
+part worth keeping.** The first attempt called it from both write funnels,
+beside the optimistic model update, by analogy with every other optimistic
+repaint. That is wrong here and the test caught it: the digest is rebuilt from
+the index, so a refresh queued beside the write reaches the worker BEFORE the
+write does and answers from the state before it. The reasoning that made it
+look right is the reasoning that applies to a model update, which needs no
+round trip; anything read back from the index has to wait for the write to
+land.
+
+**Every write, not a chosen subset.** The user's decision, and the cheaper rule
+to keep: narrowing it to the writes that change what the dashboard happens to
+draw today is a list the dashboard can outgrow silently, and this costs a round
+trip only while a conversation is on screen. It sits beside the unsynced-edits
+counter in the same handler for the same reason.
+
+**Re-requested rather than edited in place.** The digest is a derived summary,
+senders and buckets and timestamps and a capped unread list, so updating it
+here would be a second place that has to agree with the worker about what a
+write did, which is item 180's complaint exactly.
+
+**Test.** `theDashboardFollowsAWriteToTheConversationItShows`, worker-backed
+over a real two-message conversation, driven through the `mark_all_read` ACTION
+rather than the private funnel: that is the path the dashboard's own button
+takes. It asserts the pane carries the unread state BEFORE the write, so the
+assertion after it means something, and mutation-checked by removing the call.
+Note the failing runs take 45s and the passing one 295ms: the `QTRY` timeouts
+expire when the refresh never comes, which reads as a slow test rather than a
+missing feature.
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 1f5a45d..68c080c 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
@@ -254,6 +254,7 @@ taking that too literally.
| 175 | The send countdown says Undo, and cannot be skipped | presentation | XS | open, 2026-08-28, from the notes. Two changes in one control: the button reads Abort, and a second button sends immediately rather than waiting the countdown out |
| 179 | Undo is one level deep in practice, and there is no Redo | workflow | ? | open, 2026-08-29, from the notes. The `QUndoStack` is real and multi-level; what is missing is a `redo` action (absent from `knownActions()`, never called) and an answer to the stack being CLEARED on every new query (`mainwindow.cpp:3458`), which is what makes a deep stack behave like a shallow one. The clear has a correct reason and cannot simply be removed. Redo re-applies a write to real mail, so item 176's rule binds it too |
| 180 | The repaint rules are discovered one hole at a time | maintenance | S-L | open, 2026-08-29, from the notes, and a QUESTION rather than a defect. Items 105, 107, 109, 110 and 170 are each one hole in the same surface, all found by hand. Three mechanisms (optimistic repaint, `syncViewMembership()`, revert) agree by documentation rather than by code. Cheapest answer is one invariant test, not a rewrite; the user decides which, and that decides the size |
+| 181 | The thread dashboard does not follow a write to the conversation it shows | defect | XS | **done 2026-08-29**, unreleased, on `thread-row-identity`, from the notes. The dashboard draws a `ThreadDigest` built by the worker from the INDEX, which arrived only on selection, so a tag write moved the model and the card and left the pane reporting the count the conversation had when it was opened. Reachable from the dashboard's OWN Mark all read button. Re-requested from `onTagsApplied()`, where the write is confirmed: queued beside the write it races it and answers from the state before it, which is how the first fix passed review and failed the test. Section in the closed file |
Sizes are rough: XS under an hour, S a sitting, M a session.