aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-25 09:54:10 +0200
committerDanilo M. <danix@danix.xyz>2026-08-25 09:54:10 +0200
commit01ea9e9d7df04dc771430e4c378202b8ef37b8db (patch)
treefdca0a7328acf86f7999c89bcfd3638fe0f67ef9 /docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
parent671e76f691b4027c3c9707fdae6ea6d4b4690832 (diff)
downloadqtmaildir-01ea9e9d7df04dc771430e4c378202b8ef37b8db.tar.gz
qtmaildir-01ea9e9d7df04dc771430e4c378202b8ef37b8db.zip
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UUQS6n3cmsFrsjCNmwNtf8
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.md96
1 files changed, 96 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 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.