diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | 96 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 53 |
2 files changed, 101 insertions, 48 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 7a52984..41a7a1b 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 @@ -7613,3 +7613,99 @@ second test that would have restated it. Mutation-checked: reverting Suite 37 of 38; the failure is `undoMovesTheMessageBack`, item 136, pre-existing and on an unrelated path. + + + +## 160. The composer never says a draft was autosaved + +**Observed (user, 2026-08-25):** "we should add a status bar to the compose +window, to report every time a draft is autosaved. With a timer like 'last +autosave 20s ago' progressing into 'Draft autosaved' and next to it a visually +highlighted notification 'unsaved content' (blinking, color yellow, something +eye catching) whenever there's new content since the last autosave." + +**Cause (verified in code):** autosave is built and works. `m_autosaveTimer` +is a single-shot timer restarted on every edit (`composewindow.cpp:960-963`), +so it fires once the user pauses rather than once per character, and +`saveDraftNow()` writes the Maildir file. It reports **nothing on success**. + +The only feedback that exists is `m_banner`, and it is deliberately a FAILURE +channel: the comment at `composewindow.cpp:1026` records the reasoning, "a +PERSISTENT banner, not a modal and not a status-bar line that fades", because +a failed save must survive until it is dealt with. Success is the opposite +case and wants the fading line that comment rejected for failure. + +**Both states already exist as data**, which is what makes this small: + +- `m_dirty` is true exactly when there is content newer than the last save. +- `m_savedFingerprint` and the `draftSaved` signal mark a successful write. + +Nothing displays either. There is no `QStatusBar` on the composer at all. + +**Built 2026-08-25.** A `QStatusBar` on the composer, ticking every five +seconds against a label that reads in tens of them. + +**The fix is a funnel, not a label, and that is the whole item.** `m_dirty` +had SEVEN writers. Four of them CLEAR it and only two of those are a save: the +constructor clears it because seeding is not an edit, and the send handler +clears it because the message is gone. A cue hung off `saveDraftNow()` would +have been silently wrong in both, which is the shape of every defect items 105 +to 109 recorded. `setDirty()` is the only writer now; it refreshes the status +cue and calls `setWindowModified()`, so neither display can drift from the +flag. `markClean()` is the send path's entry to it. + +**Two cues, deliberately.** The status label is what the user reads while +typing; the title marker is what they see when the composer is behind another +window. Qt substitutes the `[*]` placeholder with the platform's own +convention, so the title half is native rather than invented. + +**The presentation was wrong first, and only looking found it.** It shipped +reusing item 151's yellow ground, border and text, on the reasoning that a +warning should look like the message pane's warnings. It should not: those are +bars spanning the pane and have something to be a ground OF, while the same +treatment on a bare status label reads as a misplaced widget, which is exactly +what the user reported. The cue is ordinary status text with a `○` mark now. +The first version also put both labels in the PERMANENT widget area, which is +the right-hand tray; `addWidget` is the left, which is where they belong. + +**Two defects found by probing, neither visible by reading.** + +- **The `%n` plural rendered as `2 minute(s) ago` for every English user.** + Qt picks a plural form only when a TRANSLATION supplies the forms, and there + is no English `.ts`, so an untranslated `%n` string falls back to its source + text with the `(s)` intact. Replaced with `%1` and "min", which Italian + substitutes identically. Same family as the `tr()` traps in `CLAUDE.md`: the + source reads correctly and the runtime does not. +- **The `○` was inside the translatable string** at first, so a translator + could drop or mangle it. Concatenated outside `tr()` now. + +**Testing, and one probe that measured nothing.** + +`aSentMessageLeavesNoUnsavedCue` first called `markClean()` directly. A +mutation putting `m_dirty = false` back into the send handler left all fifteen +tests GREEN, measured: the test proved what the setter does and nothing about +whether the send path calls it, which is `CLAUDE.md`'s "a probe pointed at the +wrong object". The property is structural and no runtime probe can see it, so +`onlyTheSetterWritesTheDirtyFlag()` reads `composewindow.cpp` and fails naming +the offending line. It carries a guard asserting `m_dirty` still exists, so a +rename makes it fail rather than quietly verify nothing. + +Four mutations now fail: dropping `setWindowModified`, bypassing the setter in +the send path, not recording the save time, and never starting the age tick. + +**The suite cannot see the presentation.** The tests assert visibility and +text, not styling, so the restyle left them green. That is correct and it is +also the limit: the yellow-chip version passed everything. It was fixed +because the user looked at it. + +**Constraints.** + +- **Do not reuse `m_banner`.** Its persistence is load-bearing for the quit + path's honesty, and a success message that shares it would either fade the + failure away or make success sticky. +- The "blinking" the note asks for should be treated as "eye-catching", not + literally: a blinking widget is an accessibility problem and Qt has no + blink facility to reach for. A yellow ground matching item 151's warning + severity is the established vocabulary here. +- `saveDraftNow()` returns false on failure and the banner takes over, so the + status line must not claim a save the write did not make. 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 755caa5..ab11c4f 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 @@ -232,8 +232,8 @@ taking that too literally. | 159 | The Drafts view lists threads, so a draft is unreachable by double-click | defect | S | **done** 2026-08-25, unreleased. Reverses item 138's own decision, confirmed with the user. `generatorIsFlat()` in `config.cpp` is now the single closed set of flat generators, replacing three hardcoded comparisons against `"sent"`: the built-in filter, the reader that reapplies the mode, and the writer that skips storing what the generator implies. Those three had to agree and nothing made them; a `drafts` entry saved and reloaded would otherwise have come back THREADED while the button was flat. `builtinFilter()` sets `flat` once from the helper rather than in a branch, so the set cannot drift from the labels | -| 160 | The composer never says a draft was autosaved | feedback | S | open, 2026-08-25, from the notes. Autosave works (`m_autosaveTimer`, `saveDraftNow()`) and is SILENT on success: the only feedback is `m_banner`, which appears on FAILURE. The user asks for a status bar reporting "last autosave 20s ago" progressing to "Draft autosaved", plus an eye-catching "unsaved content" cue whenever `m_dirty` is true. Both states already exist as data; nothing displays them | -| 161 | The composer has no menu bar | discoverability | S | open, 2026-08-25, from the notes. Save draft on `Ctrl+S` is asked for and does not exist at all: `saveDraftNow()` is reachable only from the timer, Send and `closeEvent`. The composer's actions are ad-hoc `QAction`s parented to the window (Send, Close, and the formatting toolbar's), none registered in `KeyMap`, so item 132's menu-reachability rule does not currently reach them. Depends on 160 for what "saved" then reports | +| 160 | The composer never says a draft was autosaved | feedback | S | **done** 2026-08-25, unreleased. A status bar on the composer: the age line left, the `○ unsaved content` cue beside it. **The fix is a funnel, not a label.** `m_dirty` had SEVEN writers and four of them clear it, only two of which are a save, so a cue hung off the save path silently missed the constructor and the send; `setDirty()` is the one writer now and refreshes both cues plus `setWindowModified()`. Presentation was **reworked after the user looked at it**: it first reused item 151's yellow ribbon treatment, which reads as a misplaced widget on a bare status label, and the cue sat in the permanent (right-hand) tray. Two defects found by probing rather than by reading, see the section | +| 161 | The composer has no menu bar | discoverability | S | open, 2026-08-25, from the notes. Save draft on `Ctrl+S` is asked for and does not exist at all: `saveDraftNow()` is reachable only from the timer, Send and `closeEvent`. The composer's actions are ad-hoc `QAction`s parented to the window (Send, Close, and the formatting toolbar's), none registered in `KeyMap`, so item 132's menu-reachability rule does not currently reach them. **Unblocked** 2026-08-25: 160 shipped the status bar, so a manual save now has somewhere to report. Still needs the user to say WHICH main-window actions belong on a composer menu, since most message actions are meaningless over a message being written | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -1337,50 +1337,6 @@ The 70-second duration recorded above fits a `QTRY_*` waiting for a file that is never going to appear, which is consistent with a wrong destination rather than a slow one. -## 160. The composer never says a draft was autosaved - -**Observed (user, 2026-08-25):** "we should add a status bar to the compose -window, to report every time a draft is autosaved. With a timer like 'last -autosave 20s ago' progressing into 'Draft autosaved' and next to it a visually -highlighted notification 'unsaved content' (blinking, color yellow, something -eye catching) whenever there's new content since the last autosave." - -**Cause (verified in code):** autosave is built and works. `m_autosaveTimer` -is a single-shot timer restarted on every edit (`composewindow.cpp:960-963`), -so it fires once the user pauses rather than once per character, and -`saveDraftNow()` writes the Maildir file. It reports **nothing on success**. - -The only feedback that exists is `m_banner`, and it is deliberately a FAILURE -channel: the comment at `composewindow.cpp:1026` records the reasoning, "a -PERSISTENT banner, not a modal and not a status-bar line that fades", because -a failed save must survive until it is dealt with. Success is the opposite -case and wants the fading line that comment rejected for failure. - -**Both states already exist as data**, which is what makes this small: - -- `m_dirty` is true exactly when there is content newer than the last save. -- `m_savedFingerprint` and the `draftSaved` signal mark a successful write. - -Nothing displays either. There is no `QStatusBar` on the composer at all. - -**Approach.** A `QStatusBar` on the composer, with the age line as a permanent -widget and the unsaved cue beside it. The age needs a second timer of its own, -since "20s ago" changes with no edit to drive it; a one-second tick is wasteful -for a label that reads in tens of seconds, so tick slower and accept the -granularity. - -**Constraints.** - -- **Do not reuse `m_banner`.** Its persistence is load-bearing for the quit - path's honesty, and a success message that shares it would either fade the - failure away or make success sticky. -- The "blinking" the note asks for should be treated as "eye-catching", not - literally: a blinking widget is an accessibility problem and Qt has no - blink facility to reach for. A yellow ground matching item 151's warning - severity is the established vocabulary here. -- `saveDraftNow()` returns false on failure and the banner takes over, so the - status line must not claim a save the write did not make. - ## 161. The composer has no menu bar **Observed (user, 2026-08-25):** "the compose window should have a menu bar on @@ -1417,6 +1373,7 @@ new action and the existing ones gathered under it rather than duplicated. - Save draft must go through `saveDraftNow()`, which already handles the failure banner and emits `draftSaved` for the indexing item 158 added. A second write path would reintroduce the ghost-file problem that fixed. -- **Depends on item 160** for what a manual save then reports: with no status - line, a successful `Ctrl+S` would be as silent as an autosave is now. +- **Item 160 unblocked this** on 2026-08-25: the status bar exists, so a + manual save reports through `refreshDraftStatus()` like an autosave. Route + `Ctrl+S` through `saveDraftNow()` and the reporting is already done. |
