diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-11 19:41:16 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-11 19:41:16 +0200 |
| commit | 64d3138ba923071069da6c9bc458a25a9cc7d27f (patch) | |
| tree | 0c35032240b7dcd2f92f613aec02147cc56ee8eb /docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | |
| parent | 2c3a3d4da8ceb21bd1e2f7be16fcaa7473b6bbe6 (diff) | |
| download | qtmaildir-64d3138ba923071069da6c9bc458a25a9cc7d27f.tar.gz qtmaildir-64d3138ba923071069da6c9bc458a25a9cc7d27f.zip | |
feat(sync): sync a tag change automatically after a short delay
Item 71. A tag edit reached the notmuch index at edit time and then sat there
until the user clicked Sync or their cron job fired, so "mark all read" updated
the view while the change itself waited, sometimes for ten minutes.
A confirmed edit now arms a debounce that runs the existing sync path. The delay
is auto_sync_delay_ms in [general], defaulting to 2000, and follows
mark_read_delay_ms exactly, including that zero and negative are not errors:
zero syncs on the next trip through the event loop, and any negative value
disables the behaviour, which is the switch for a user who wants only their cron
job.
It is armed from onTagsApplied, where a write is confirmed and the pending count
is already current, rather than where one is sent: a sync scheduled for a write
the worker went on to reject would run for nothing. A debounce rather than a
schedule, restarted by each edit, because "mark all read" confirms one write per
thread in the view and an arm-per-edit timer would be the storm of syncs the
debounce exists to prevent. Nothing is armed when no sync command is configured
or when the pending count is zero, the case where an edit was netted against its
own inverse. When the timer fires with a sync already running, local or cron, it
skips rather than queues: mbsync's own answer to a second run is to fail on it,
and the edits stay pending rather than being lost.
Also fixes a pane blanked out from under the reader, found by hand testing this
feature. onSyncFinished called runCurrentQuery() where the cron path calls
refreshCurrentQuery(), and a re-run clears the model, the undo stack and the
message pane. The stale-thread notice handles a thread that stops matching the
query and has since item 35, but a re-run left nothing for it to describe. The
two paths had no reason to differ; before this item a local sync only followed a
click on Sync, so the difference went unnoticed. Reading a message in the Unread
view, having it marked read, and watching the pane go blank two seconds later is
what surfaced it.
Its test asserts on the undo stack rather than the pane: both paths issue a
queued query test_mainwindow has no worker to answer, so the pane ends up blank
either way and an assertion on it would pass against both, while the undo stack
is cleared by one and kept by the other.
Nine tests, four in test_config and five in test_mainwindow, each
mutation-checked: removing the schedule call, honouring a negative delay,
dropping the nothing-pending guard, dropping the already-running guard, and
restoring runCurrentQuery() each fail a test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 48 |
1 files changed, 47 insertions, 1 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 9957ba8..2663b44 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 @@ -127,7 +127,7 @@ taking that too literally. | 68 | A forwarded subject gets no `passed` tag | workflow | S | open; no subject rule exists, measured 2026-08-11. Decision needed: display mark (XS) or write the flag (S, syncs out) | | 69 | `passed` and `replied` read as words where every other state is a glyph | presentation | S | open; depends on 68 for what `passed` means | | 70 | Pane icons are a private set where the main window uses the system theme | presentation | M | open | -| 71 | A toolbar action does not sync, so the edit sits until the next cron run | workflow | S | open; user decision on the delay | +| 71 | A toolbar action does not sync, so the edit sits until the next cron run | workflow | S | **done** 2026-08-11; 2s default, `auto_sync_delay_ms` | | 72 | No khard/khal integration | workflow | ? | open, unspecified; the user places it after send, so v2 at the earliest | | 73 | This backlog is past four thousand lines | maintenance | S | open | @@ -4447,6 +4447,52 @@ lock is held. The undo stack has to survive it, which is what item 35 built. **Size: S** once the delay is chosen. +**Done 2026-08-11.** The delay is 2000ms by default, chosen by the user, and +configurable as `auto_sync_delay_ms` in `[general]`. It follows +`mark_read_delay_ms` exactly, including that zero and negative are not errors: +zero syncs on the next trip through the event loop and any negative value +restores the pre-0.16.0 behaviour, which is the switch for a user who wants only +their cron job. + +Armed from `onTagsApplied`, where a write is CONFIRMED and the pending count is +already current, rather than where one is sent: a sync scheduled for a write the +worker went on to reject would run for nothing. It is a debounce and not a +schedule, restarted by each confirmed edit, because "mark all read" confirms one +write per thread in the view and an arm-per-edit timer would be the storm of +syncs the debounce exists to prevent. Nothing is armed when the delay is +negative, when no sync command is configured, or when the pending count is zero, +which is the case where an edit was netted against its own inverse (item 28). + +The constraints held: `runAutoSync` skips rather than queues when +`m_externalSyncBusy` or a local sync is running, and the edits stay pending +rather than being lost. Item 35's refresh keeps the undo stack. + +Four tests in `test_mainwindow` and four in `test_config`, each mutation-checked: +removing the `scheduleAutoSync()` call, honouring a negative delay, dropping the +nothing-pending guard and dropping the already-running guard each failed a test. +**Follow-up, found by hand testing the same day.** Reading a message in the +Unread view, the automatic mark-read tagged it, the automatic sync fired two +seconds later, and the message pane went blank. The stale-thread notice (item 35) +exists for exactly this and was not the problem: `onSyncFinished` called +`runCurrentQuery()` where the cron path calls `refreshCurrentQuery()`, and a +re-run clears the model, the undo stack and the pane, so there was nothing left +for the notice to describe. The two paths had no reason to differ; before item 71 +a local sync followed only a click on Sync, where blanking was at least +explicable, so the difference went unnoticed. `onSyncFinished` now refreshes. + +Its test asserts on the UNDO STACK, not on the pane. Both paths issue a queued +query that `test_mainwindow` has no worker to answer, so the pane ends up blank +either way and an assertion on it passes against both; the undo stack is cleared +by one and kept by the other, so it names which path ran. Mutation-checked by +restoring `runCurrentQuery()`. + +Two traps met while writing them and worth keeping. The helper first wrote +`general/auto_sync_delay_ms` and the key silently matched nothing, leaving the +default in place, exactly the QSettings `[general]` behaviour recorded in +CLAUDE.md. And a debounce assertion comparing `remainingTime()` before and after +with `>` is FLAKY, since both reads can land in the same millisecond; assert the +remaining time went back up near the full interval instead. + ## 72. No khard/khal integration **Observed (user, from the notes):** "investigate khard/khal integration (light |
