diff options
Diffstat (limited to 'docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | 59 |
1 files changed, 59 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. |
