From 01ea9e9d7df04dc771430e4c378202b8ef37b8db Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 25 Aug 2026 09:54:10 +0200 Subject: feat(compose): report autosave state in a status bar Autosave worked and said nothing on success. The only feedback was m_banner, which is the failure channel and whose persistence is load-bearing for the quit path, so success got its own channel rather than sharing one. The fix is a funnel, not a label. m_dirty had seven writers, four of which 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. setDirty() is the only writer now, and it refreshes the status cue and setWindowModified() together so neither display can drift from the flag. The age line needs a tick of its own, since it moves with no edit to drive it. Five seconds against a label that reads in tens of them. Two defects found by probing rather than by reading. The %n plural rendered as "2 minute(s) ago" for every English user, because Qt picks a plural form only when a translation supplies the forms and there is no English .ts; it uses %1 and "min" now, which Italian substitutes identically. And the status mark was inside the translatable string, where a translator could drop it; it is concatenated outside tr(). Presentation reworked after the user looked at it. The first version reused item 151's yellow ribbon treatment, which reads as a misplaced widget on a bare status label rather than as a warning, and put both labels in the permanent widget area, which is the right-hand tray. They are ordinary status text on the left now. onlyTheSetterWritesTheDirtyFlag() asserts the funnel structurally, by reading composewindow.cpp: the first test for the send path called markClean() directly and a mutation restoring a direct assignment left the whole suite green. Four mutations now fail. The suite still cannot see the presentation, which is why that half needed a hand test. lrelease reports 487 finished, 0 unfinished. Closes item 160, and unblocks 161. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UUQS6n3cmsFrsjCNmwNtf8 --- .../plans/2026-08-03-post-0.1.0-usability.md | 53 ++-------------------- 1 file changed, 5 insertions(+), 48 deletions(-) (limited to 'docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md') 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. -- cgit v1.2.3