summaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-04 11:16:52 +0200
committerDanilo M. <danix@danix.xyz>2026-08-04 12:54:48 +0200
commit3b0a52b8620d3cead2f527d108ba64bfec8ee273 (patch)
treef231a877f4805a7dbcde4145a7e8ce9443156dc5 /docs/superpowers
parent272cb9de98ea387b1d73c55333e4058d1af5da47 (diff)
downloadqtmaildir-3b0a52b8620d3cead2f527d108ba64bfec8ee273.tar.gz
qtmaildir-3b0a52b8620d3cead2f527d108ba64bfec8ee273.zip
feat(sync): show unsynced edits and offer to sync on exit
Tagging changes the notmuch index at once, but the mail store only hears about it on the next sync, and nothing said so. Quitting with tagging outstanding was silent. Items 18 and 19 of the usability backlog, built together because the second needs the first's counter. The counter cannot be QUndoStack::isClean(), which is the obvious candidate and the wrong one: the undo stack is cleared on every query, since its entries refer to rows the new result set discards. Tag a thread, run any query, and the stack is empty while the change is still unsynced. m_pendingEdits is its own count, incremented where a write is CONFIRMED rather than where one is sent, so an optimistic update the worker later rejects cannot leave the indicator claiming an edit that never landed. Only a successful sync resets it: clearing on failure would assert the changes had reached the mail store when the sync is exactly what failed to put them there. It is shown in the status bar, hidden entirely at zero, and described as a lower bound rather than a guarantee, since an external notmuch run can carry changes over without this application noticing. On exit, sync_on_exit in [general] takes ask, always or never. Three values rather than a bool because "prompt me", "just do it" and "do nothing" are three behaviours and true/false expresses two; an unknown value warns by name, since a typo there silently changes what happens to unsynced work. The prompt offers three buttons for the same reason: a user who hit Quit by mistake needs a way back that is not "sync". A sync started at exit holds the window open until it finishes rather than being killed mid-run, and a sync that FAILS does not quit, because quitting there would discard the user's choice silently. With no sync command configured the prompt degrades to a plain warning instead of offering a sync that cannot run. This is not a destructive-action confirmation of the kind CLAUDE.md forbids. Those cover tag mutations, which keep undo instead of a dialog. This asks about losing work at the one point where undo cannot help. The tagsApplied lambda became a named slot, which is better structure and also what lets a test drive it: the worker is deliberately parentless because it moves to its own thread, so reaching it with findChild to emit the real signal cannot work, and contorting the test to try was the wrong instinct. Testing a modal needed its own care. A test that sends a close event hangs forever if an unexpected dialog opens, because the modal spins its own event loop; CloseProbe polls for activeModalWidget, closes it and records that one appeared, turning "a dialog opened" into an assertion rather than a hang. Also removes a stray qDebug left in the open_thread action by the earlier Enter-key investigation, which had reached two commits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md55
1 files changed, 53 insertions, 2 deletions
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 7fedaf3..8f19d6b 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
@@ -56,8 +56,8 @@ taking that too literally.
| 15 | Attachments are parsed but unreachable from the UI | information | M | **done** |
| 16 | Delete on an already-deleted thread should undelete | behavior | S | open |
| 17 | No completion for tags in the query bar | workflow | M | **done** |
-| 18 | No visual cue that there are unsynced edits | feedback | S | open |
-| 19 | No prompt to sync on exit when edits are pending | behavior | S | open |
+| 18 | No visual cue that there are unsynced edits | feedback | S | **done** |
+| 19 | No prompt to sync on exit when edits are pending | behavior | S | **done** |
| 20 | Thread view does not match the user's mental model | presentation | ? | open, unspecified |
| 21 | Default shortcuts are not sensible enough | discoverability | S | open |
| 22 | Translatability audit and i18n wiring | correctness | M | open |
@@ -847,6 +847,27 @@ resets.
survives. Then sync and confirm it clears. Then make the sync fail and confirm
it does not.
+### Outcome (done)
+
+Built as specced. `m_pendingEdits` counts confirmed mutations, incremented in
+`onTagsApplied()` and reset only by `onSyncFinished(true, ...)`. The count is
+shown as a permanent status-bar widget, hidden entirely at zero.
+
+**Counted where a write is confirmed, not where one is sent.** An optimistic
+update the worker later rejects must not leave the indicator claiming an edit
+that never landed, so the increment sits in the `tagsApplied` handler.
+
+That handler was a lambda; it is now a named slot, which is both better
+structure and what lets a test drive it. An earlier draft tried to reach the
+worker with `findChild` to emit the real signal: the worker is deliberately
+parentless because it moves to its own thread, so that cannot work, and
+contorting the test to reach it was the wrong instinct. What matters is the
+counter's behaviour, not the signal's origin.
+
+**Both tests were verified by breaking the code.** Clearing the count on any
+sync outcome is caught, and so is clearing it where the undo stack is cleared,
+which is the naive design this item exists to avoid.
+
## 19. No prompt to sync on exit when edits are pending
**Observed (user, 2026-08-04):** quitting with unsynced edits is silent. The
@@ -891,6 +912,36 @@ for the reason item 18 documents.
each `sync_on_exit` value and confirm the behavior matches. Then quit with a
deliberately broken sync command and confirm the app neither hangs nor lies.
+### Outcome (done)
+
+Built as specced, with the user confirming the three-value key over their
+original on/off idea. `closeEvent()` consults the count, and a sync started for
+exit holds the window open until `finished` arrives rather than being killed
+mid-run.
+
+**A failed exit-sync does not quit.** It restores the window, shows the log and
+says what happened. Quitting there would discard the user's choice silently,
+which is the exact failure the prompt exists to prevent.
+
+**With no sync command configured the prompt degrades** to a warning offering
+Discard or Cancel, rather than offering a sync that cannot run.
+`MailSync::isAvailable()` is already false in that case.
+
+**Testing a modal needs care, and the first attempt was wrong.** A test that
+sends a close event hangs forever if an unexpected dialog opens, because the
+modal spins its own event loop. The first version appeared to pass in 19
+seconds; it had actually opened a real dialog on the user's screen, and the
+user dismissed it. `CloseProbe` now polls for `activeModalWidget()`, closes it,
+and records that one appeared, turning "a dialog opened" into an assertion
+instead of a hang or a prompt for whoever is watching.
+
+**One mutation test was a dud and is worth recording.** Removing the `Never`
+guard from `closeEvent` did NOT make the test fail, because the branches below
+match only `Ask` and `Always`, so `Never` fell through to closing anyway. The
+guard is redundant belt-and-braces rather than load-bearing. Confirming the
+probe really detects a prompt needed the config mutated to `ask` instead, which
+does fail it.
+
## 20. Thread view does not match the user's mental model
**Observed (user, 2026-08-04), in passing while deciding item 2:** "My view for