aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-26 20:17:50 +0200
committerDanilo M. <danix@danix.xyz>2026-08-26 20:17:50 +0200
commit9c782e0c96c19525babc6c7ceb4f1a184a400169 (patch)
treec8bb73d446287bedff1fb13660e147cec5bc42ee /docs/superpowers/plans
parent494e2f263831614f3ea4f023ba93ccc7d740e0c1 (diff)
downloadqtmaildir-9c782e0c96c19525babc6c7ceb4f1a184a400169.tar.gz
qtmaildir-9c782e0c96c19525babc6c7ceb4f1a184a400169.zip
docs: record item 119 and close it in the backlog
The status rows for 119 and its duplicate 146, 119's section moved to the closed file on this commit rather than left for a later cleanup, and the README and changelog entries for the feature. CLAUDE.md gains three findings, all of which cost time to learn here: A defensive counter for an unreachable case is worse than nothing, because it blocks the feature that needs the data. Reading the code said that branch was reachable and the reading was wrong; instrumenting it and running the suite is what settled it, and the tests that appeared to exercise it were driving it from outside the production path. PendingChangesDialog groups by a run rather than a map, which is why the snapshot is stable-sorted, and startsMessage is carried rather than inferred so a stale row still opens its own run. A queued call carrying a container deserves the same suspicion as a Q_ENUM, with the measurement: both containers cross intact on Qt 6.11, but a standalone probe found QMetaType::fromName("QList<int>") invalid while QList<bool> resolved, so the property does not follow from the type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P88Q3MCSCSQxKDy7pmXh9F
Diffstat (limited to 'docs/superpowers/plans')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md101
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md58
2 files changed, 103 insertions, 56 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 39026cb..7261bd9 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
@@ -8607,3 +8607,104 @@ that instead.
**Not built, and left as the item's own recommendation:** writing `P` from a
subject heuristic. Rejected on the same grounds the entry gave before the
work started.
+
+
+## 119. The unsynced-changes count cannot be opened to see what it counts
+
+**Observed (user, from the notes):** "the bottom left statusbar message needs to
+be clickable and show what 'N unsynced changes' are in a modal window".
+
+**Cause (verified in the code).** `m_pendingLabel` is a plain `QLabel` added to
+the status bar with `addPermanentWidget` (`src/mainwindow.cpp:502-505`). A
+`QLabel` has no clicked signal and none is installed, so there is nothing to
+click and no route to a list. It carries a tooltip and nothing else.
+
+**The count is a SUM OVER FOUR SOURCES, and that is what makes this bigger than
+it looks.** `pendingEditCount()` returns
+`m_pendingTagEdits.size() + m_unnettablePendingEdits + held + heldMoves`.
+Three of those can name what they hold: `m_pendingTagEdits` is a
+`QHash<QString, bool>` keyed by message id, `m_heldEdits` and `m_heldMoves` are
+queues of edits waiting for a sync to end. **`m_unnettablePendingEdits` is a
+bare `int`** (`src/mainwindow.h:1248`), deliberately so: it counts confirmed
+changes that carry no message ids and therefore cannot be netted against
+anything.
+
+So a dialog built from what is currently kept would list three of the four
+groups and then have to account for a remainder it cannot describe. Showing "and
+3 more" is worse than the tooltip, because the user opened the window
+specifically to find out what those were.
+
+**Approach.** Two halves, and the second is the real work.
+
+- The clickable half is small: a label that emits on click (an event filter, or
+ a flat `QToolButton` styled as a label), plus a dialog listing what the three
+ describable groups hold. The message pane already resolves an id to a subject.
+- The complete half needs `m_unnettablePendingEdits` to become something that
+ can name its entries. Its comment says why it is an int: understating the
+ indicator is the direction that costs the user work, so it counts what it
+ cannot identify rather than dropping it. Making it describable means finding
+ out what those changes actually are and whether they can carry an id.
+
+**Constraints.**
+
+- **The count is deliberately conservative and must stay so.** Item 28 and item
+ 54 both landed on this indicator being wrong in the direction that made the
+ user think their work was safe. A dialog that lists fewer changes than the
+ count claims is the same failure in a new place: reconcile the two, or state
+ the remainder honestly rather than hiding it.
+- **An external `notmuch` run can clear pending changes without this count
+ noticing**, which the tooltip already admits. A dialog makes that staleness
+ much more visible, since a listed change may no longer exist. Worth deciding
+ whether the dialog re-verifies against the database before showing.
+- Read-only. This is an information window, not a place to retry or discard a
+ change; either would be a new mutation path with its own undo question.
+
+**Size: S** for the clickable half over the three describable groups. **Unknown**
+for the fourth, and the item is not complete without it.
+
+**Closed 2026-08-26.** The blocker above was investigated first and did not
+survive: `m_unnettablePendingEdits` counted confirmed changes carrying no
+message ids, and `NotmuchWorker::applyTags()` (the only emitter of
+`tagsApplied`) returns early on an empty id list, which is that exact
+condition. `applyTagsToThreads()` resolves through a query and errors out on
+an empty result, so it cannot hand `applyTags()` an empty list either.
+
+**Measured rather than read**, twice, because reading is what produced the
+wrong answer the first time: a `qFatal` in the branch fired in 4 of 70
+`test_mainwindow` cases, all four building a `TagChange` by hand and invoking
+the slot directly with no worker, and a `Q_ASSERT` before the worker's own
+emit never fired across the whole suite. The counter was deleted and the
+guard it shadowed is pinned where it lives, by
+`applyTagsWithNoIdsDoesNothing()` in `test_notmuchworker`.
+
+Built in four commits: the snapshot, the subject resolve, the dialog and the
+click, then a sizing fix after a hand test.
+
+Three decisions the user made, each of which shapes the code:
+
+- **Scope follows the ACTION, not the storage.** A thread action shows one
+ thread row with the count of messages it covered; a message action shows
+ its message. The three queues already encoded this, so nothing is expanded
+ and nothing is escalated: `HeldEdit` is thread-scoped because a `*_thread`
+ action made it, and everything else carries message ids.
+- **A snapshot, frozen.** Taken at the click and never refreshed under the
+ user, who asked for exactly this: "if I keep the popup open for 20 minutes,
+ I don't want the popup to keep updating the info it's showing me."
+- **Subjects resolved, stale rows kept.** An id the index no longer holds
+ still gets a row saying its subject is unknown, because the count the user
+ clicked has to equal the list they are shown.
+
+The re-verify question this item worried about mostly dissolved: item 54
+already clears the count when an external sync carries the edits, so a change
+applied by cron does not survive to be clicked on.
+
+`resolvePendingSubjects()` answers POSITIONALLY, one subject per input row,
+because one id can legitimately appear on several rows and a combined query
+returns a set. `PendingChangeRow::startsMessage` is carried rather than
+inferred from a non-empty subject, so an unresolved id still opens a run of
+its own instead of folding its actions under the message above it.
+
+Read-only, per the constraint above. The dialog's height is sized to its
+content; that is a hand test, since the offscreen platform returns an
+identical frame either way.
+
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 5e95804..69aac44 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
@@ -185,7 +185,7 @@ taking that too literally.
| 116 | Copy image copies markup instead of the image | defect | XS | **dropped** 2026-08-17, same day. NOT A DEFECT: `wl-paste --list-types` run immediately after a copy reports `image/png`, `application/x-qt-image` and 30 more image flavours. The clipboard is correct and Chromium is behaving. The earlier "text only" reading was taken minutes late off a clipboard that had been overwritten, and a whole cause was theorised on it |
| 117 | The message pane offers no Select all | workflow | XS | **done** 2026-08-19, unreleased. `addPaneActions()` supplies it. The call site is NOT covered by a test and cannot be: the production menu needs a real context-menu event. Stated in the test rather than faked |
| 118 | No way to empty the trash from inside the app | workflow | S | **done 2026-08-25**, unreleased. Unblocked by 103. `Message > Empty trash...`, scoped to the account selector, no shortcut. The one confirmation in this application, and CLAUDE.md now records it as the single exception rather than leaving it to be discovered. Found a defect while testing: the count claimed messages whose files were already gone |
-| 119 | The unsynced-changes count cannot be opened to see what it counts | information | S | open, 2026-08-19, from the notes. One of the four things it sums carries no message ids at all, so a list cannot be complete without a change to how the count is kept |
+| 119 | The unsynced-changes count cannot be opened to see what it counts | information | S | **done** 2026-08-26, unreleased. **The stated blocker was not real**: the fourth term counted confirmed changes with no message ids, and `applyTags()` returns early on exactly that condition, so it could never fire. Measured before removing it, not read. The label opens a read-only list, grouped as the user asked: subject once, actions beneath. Scope follows the ACTION, so a held thread edit stays one thread row and reports its message count. A snapshot, frozen once open |
| 121 | The thread list shows nothing while a query is running | feedback | S | open, 2026-08-20, from the notes. Follows item 74, which fixed the status-bar half and left the list itself blank |
| 122 | The README documents a version of the app that no longer exists | documentation | M | **done** 2026-08-23, unreleased, inside item 123 task 13. `trash`, `send_command` and the whole `[compose]` section were undocumented; a Composing section is added and "sending is not implemented" removed. Every default was read from `config.h` rather than from the prose, which caught `send_html` documented as false when it defaults to true |
@@ -215,7 +215,7 @@ taking that too literally.
| 143 | The formatting buttons are text, where every editor uses icons | presentation | XS | **done** 2026-08-24, unreleased, inside 142. `QIcon::fromTheme` per CLAUDE.md's chrome rule, the words kept as the tooltip, and an action whose theme lacks the name keeps its text rather than rendering an empty button |
| 144 | "Also send a formatted copy" is prominent and does not say what it does | presentation | XS | **done** 2026-08-24, unreleased, inside 142. "Send as HTML", icon and text, alone at the right end of the editor bar where it reads as a control of the editor rather than as a formatting button. The Italian entry was refreshed with it, and `lrelease` reports 477 finished, 0 unfinished |
| 145 | Cc and Bcc are permanent rows on every composer | presentation | S | **done** 2026-08-24, unreleased, inside 142. A `QToolButton` disclosure beside To:. `revealCcBccIfUsed()` is the load-bearing half the entry called for: it only ever SHOWS, never hides, so nothing but the user's own click can make a field holding an address invisible. `ComposeContext` carries no `bcc` at all, so the seeded-Bcc case can only arrive from a reopened draft, which is what its test drives. The LABEL is hidden with each field: a `QFormLayout` holds the two as separate items, so hiding the line edit alone strands a `Cc:` over empty space |
-| 146 | The unsynced-changes count cannot be opened to see what it counts | information | S | **duplicate of 119**, recorded 2026-08-23 from the notes. Same request, and 119 already carries the blocker: one of the four things the count sums holds no message ids, so a list cannot be complete without changing how the count is kept |
+| 146 | The unsynced-changes count cannot be opened to see what it counts | information | S | **done as 119** 2026-08-26. Duplicate, recorded 2026-08-23 from the notes; closed by the same work |
| 147 | Toggle unread reads the same whichever way it will go | presentation | S | **duplicate of 99**, recorded 2026-08-23 from the notes, and closed with it on 2026-08-25 |
| 148 | Ctrl+W does not close the composer | discoverability | XS | **done** 2026-08-24, unreleased. A `QAction` parented to the composer, so it is a WindowShortcut dispatched to the active composer only and the main window's namespace is untouched, exactly like the formatting shortcuts. It calls `close()` rather than doing anything of its own: `closeEvent()` already decides whether the draft is saved, and a second route out that skipped it would lose the message. Not registered in `KeyMap`, so item 132's rules do not apply |
| 149 | A reply's cursor lands on the attribution line, not on blank space | defect | XS | **done** 2026-08-24, unreleased, in TWO passes. The first fixed the cursor within each branch (`End` under Above, `Start` under Below) and the user still saw the old layout, because the branches were already right and the DEFAULT was wrong: `above` shipped, and the layout asked for is what `below` produces. Default flipped, and the composer now focuses the body whenever To: is already filled, which a Reply and a Forward always are. Both halves were invisible to the existing `theQuotePositionDecidesWhereTheQuoteLands`, which asserts the quote's position and never the cursor's |
@@ -640,60 +640,6 @@ make Save image work must not make Save link reachable again. The test fails if
it does, which is the point: the handler is per-profile, so the natural
implementation would light up both entries at once.
-## 119. The unsynced-changes count cannot be opened to see what it counts
-
-**Observed (user, from the notes):** "the bottom left statusbar message needs to
-be clickable and show what 'N unsynced changes' are in a modal window".
-
-**Cause (verified in the code).** `m_pendingLabel` is a plain `QLabel` added to
-the status bar with `addPermanentWidget` (`src/mainwindow.cpp:502-505`). A
-`QLabel` has no clicked signal and none is installed, so there is nothing to
-click and no route to a list. It carries a tooltip and nothing else.
-
-**The count is a SUM OVER FOUR SOURCES, and that is what makes this bigger than
-it looks.** `pendingEditCount()` returns
-`m_pendingTagEdits.size() + m_unnettablePendingEdits + held + heldMoves`.
-Three of those can name what they hold: `m_pendingTagEdits` is a
-`QHash<QString, bool>` keyed by message id, `m_heldEdits` and `m_heldMoves` are
-queues of edits waiting for a sync to end. **`m_unnettablePendingEdits` is a
-bare `int`** (`src/mainwindow.h:1248`), deliberately so: it counts confirmed
-changes that carry no message ids and therefore cannot be netted against
-anything.
-
-So a dialog built from what is currently kept would list three of the four
-groups and then have to account for a remainder it cannot describe. Showing "and
-3 more" is worse than the tooltip, because the user opened the window
-specifically to find out what those were.
-
-**Approach.** Two halves, and the second is the real work.
-
-- The clickable half is small: a label that emits on click (an event filter, or
- a flat `QToolButton` styled as a label), plus a dialog listing what the three
- describable groups hold. The message pane already resolves an id to a subject.
-- The complete half needs `m_unnettablePendingEdits` to become something that
- can name its entries. Its comment says why it is an int: understating the
- indicator is the direction that costs the user work, so it counts what it
- cannot identify rather than dropping it. Making it describable means finding
- out what those changes actually are and whether they can carry an id.
-
-**Constraints.**
-
-- **The count is deliberately conservative and must stay so.** Item 28 and item
- 54 both landed on this indicator being wrong in the direction that made the
- user think their work was safe. A dialog that lists fewer changes than the
- count claims is the same failure in a new place: reconcile the two, or state
- the remainder honestly rather than hiding it.
-- **An external `notmuch` run can clear pending changes without this count
- noticing**, which the tooltip already admits. A dialog makes that staleness
- much more visible, since a listed change may no longer exist. Worth deciding
- whether the dialog re-verifies against the database before showing.
-- Read-only. This is an information window, not a place to retry or discard a
- change; either would be a new mutation path with its own undo question.
-
-**Size: S** for the clickable half over the three describable groups. **Unknown**
-for the fourth, and the item is not complete without it.
-
-
## 121. The thread list shows nothing while a query is running
**Observed (user, from the notes):** "can we show a spinner in the left panel