aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
diff options
context:
space:
mode:
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.