diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-15 09:37:54 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-15 09:37:54 +0200 |
| commit | deec4e1bbf2a6af6f3f86043080fd5b4df535e03 (patch) | |
| tree | 7c0e13f02d2f99eba0bb6fd55be7e10f46ac79b9 /docs | |
| parent | 38b9466aea7daa0ddde91c1de7d8082ac000c238 (diff) | |
| download | qtmaildir-deec4e1bbf2a6af6f3f86043080fd5b4df535e03.tar.gz qtmaildir-deec4e1bbf2a6af6f3f86043080fd5b4df535e03.zip | |
docs: reconcile the backlog with the user's notes, open 89 to 92
The 2026-08-15 pass over ~/Documents/Obsidian/note/notes on qtmaildir.md
found four entries with no item here, and one already-closed item with a
case that was never verified by hand.
89: runAutoSync() returns without re-arming the timer when a sync is
already in flight, so an edit made after the running mbsync passed that
account waits for a manual sync or cron. The second half of the same
complaint, rows leaving the list mid-interaction, is a design question
and is recorded as one rather than guessed at.
90: runSavedQuery() clears the account box for a query that names no
account. The line is deliberate and its comment says why, so this needs
a decision rather than a fix.
91 and 92 are unspecified, as the notes themselves say.
Item 66's row gains the one case its fix should already cover: a
single-message id: query whose card would not open is the same empty
MessageIdRole failure, unverified against an id: query.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 165 |
1 files changed, 164 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 63c192c..e1ea2da 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 @@ -130,7 +130,7 @@ taking that too literally. | 63 | No way to see sent mail, and no filter for it | workflow | M | **done** 2026-08-11; see `specs/2026-08-11-sent-mail-design.md` | | 64 | The Sync button carries a mailbox icon, not a refresh one | presentation | XS | **done** 2026-08-11 | | 65 | No full code review and optimization pass | correctness | ? | open, unspecified | -| 66 | Selecting a thread root leaves the message pane blank until a reply has been selected | defect | S | **done** 2026-08-14, unreleased. Not the blank pane it was filed as: the root rendered the CONVERSATION until the thread had been expanded once, then one message. Now always one message, and the conversation view is removed at the user's request | +| 66 | Selecting a thread root leaves the message pane blank until a reply has been selected | defect | S | **done** 2026-08-14, unreleased. Not the blank pane it was filed as: the root rendered the CONVERSATION until the thread had been expanded once, then one message. Now always one message, and the conversation view is removed at the user's request. **One case unverified by hand:** the notes also report a single-message `id:` query whose card would not open, which is the same empty-`MessageIdRole` failure and should be gone; confirm against an `id:` query before treating it as closed | | 87 | Auto mark-read marks a whole thread, including replies never displayed | defect | S | open; measured 2026-08-14. Reachable only after 66 removed the conversation view. Blocked on 88 | | 88 | `threadAt(current.row())` answers about the wrong thread for a reply row | defect | S | open; found 2026-08-14 by shipping a fix that marked an unrelated message read. Row numbers are per parent in a tree | | 67 | The placeholder pane counts unread, flagged and inbox, but not sent or drafts | information | XS | **done** 2026-08-11, shipped in 0.15.0 | @@ -153,6 +153,10 @@ taking that too literally. | 84 | A config problem blocks `test_mainwindow` on a modal nobody can dismiss | testing | S | **done** 2026-08-14, unreleased. `showWarnings()` split: the status label stays in the constructor, `main.cpp` raises the modal after `show()` | | 85 | Nothing on screen can be searched for by right-clicking it | workflow | M | **done** 2026-08-14, unreleased; see `specs/2026-08-14-search-from-message-design.md`. Split from 78; rebuilt the details dialog as rows | | 86 | A right-click search can replace or narrow, but never exclude | workflow | S | **done** 2026-08-14, unreleased; see `specs/2026-08-14-exclude-from-search-design.md`. Follows 85. The `extend` bool became a `SearchMode` enum across four signatures | +| 89 | A sync moves the list under the user's hands, and the auto-sync skips rather than retries | workflow | M | open; from the 2026-08-15 notes pass. Two faults under one complaint, and the larger half is a design question | +| 90 | A saved-query button clears the account selection | workflow | S | open; from the 2026-08-15 notes pass. Cause is a DELIBERATE line; decision needed from the user before any change | +| 91 | Double-clicking a thread could open it in its own window | workflow | ? | open, unspecified; the user marked it "(?) UX not sure" | +| 92 | Nothing distinguishes a tag written by a rule from one the user applied | information | ? | open, unspecified; the user asked it as a question, and the answer decides whether it is a display item or a format change across two repos | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -526,6 +530,165 @@ in CLAUDE.md. **Size: S**, down from M now that item 85 has built the menus and item 81 the seeded dialog. +## 89. A sync moves the list under the user's hands, and the auto-sync skips rather than retries + +**Observed (user, notes):** "the auto sync after a delay needs to be reviewed, +its behavior is not exactly right." Then, more broadly: "the sync in general is +worth rethinking, as it is now is not polished and shows too many moving parts. +messages disappearing from views, lists changing while the user is +interacting." And the workaround the user already found: "setting Inbox as the +default view mitigates the problem as messages are not removed from the list +after being read for 2s." + +**Two separate faults sit under one complaint**, and only the first is small. + +**Cause, the concrete half.** `MainWindow::runAutoSync()` +(`src/mainwindow.cpp:3213`) returns without rescheduling when a sync is already +in flight: + +```cpp +if (m_externalSyncBusy || (m_sync && m_sync->isRunning())) + return; +``` + +The comment beside it argues the edits are not lost, because they reached the +mail store at edit time and the running sync is "very likely" to carry them. +Very likely is not always: an edit made after the running mbsync has already +passed that account's mailbox is not carried, the timer has fired and is not +re-armed, and nothing arms it again until the next edit. The pending count then +sits non-zero until a manual sync or the cron job. That is exactly "not exactly +right", and it is a missing `m_autoSyncTimer->start(delay)` on the skip path +rather than a redesign. `scheduleAutoSync()` (`:3184`) already re-checks +everything on the way in, so restarting the timer there is safe by its own +argument. + +**Cause, the larger half.** Nothing to do with the auto-sync: it is what a +refresh does to the list. The refresh after a sync replaces the result set, and +a query like `tag:unread` no longer matches a thread the user has just read, so +rows vanish from under the pointer. Item 35 built the refresh to keep the user's +place, and it does, but keeping the selection is not the same as keeping the +row: a thread that has left the result set has nowhere to be kept. The user's +own mitigation, using an `tag:inbox` view where reading does not change +membership, is the real diagnosis. + +**Approach.** Ship the timer restart on its own, as an XS fix with a test that +arms the timer while a sync is running and asserts it is still active. Then +treat the list-churn half as a design question and put it to the user before +building: the plausible answers (defer a refresh while the pointer is over the +list, keep a read thread visible until the next explicit query, refresh only +rows rather than the result set) differ enough in feel that guessing wastes the +work. + +**Constraints.** + +- The skip itself must stay. Item 71 requires it and mbsync fails on a second + concurrent run; this item restarts the timer, it does not queue a sync. +- A restart must not turn into a spin against a long external sync. The delay is + the debounce interval, and `SyncMonitor` polls `/proc/locks`, so an + `m_externalSyncBusy` that never clears would re-arm indefinitely at that + interval. Cheap, but say so in the test. +- Do not restore the pre-0.16.0 behaviour by making the delay negative for the + user. `auto_sync_delay_ms` is theirs to set. + +## 90. A saved-query button clears the account selection + +**Observed (user, notes):** "select an account and hit the 'unread' button, the +panel should show unread messages from that account only, instead it refreshes +the 'all accounts' list." + +**Cause, and it is a deliberate line, not an oversight.** +`MainWindow::runSavedQuery()` (`src/mainwindow.cpp:1907`) resets the account box +whenever the saved query names no account: + +```cpp +const int index = saved.account.isEmpty() + ? m_accountBox->findData(QString()) + : m_accountBox->findData(saved.account); +if (index >= 0) + m_accountBox->setCurrentIndex(index); +``` + +The comment above it states the intent: "An unscoped query CLEARS the selection +rather than inheriting whatever was there, which is the defect the rules preview +hit." `runQuery()` (`:2020`) then scopes whatever the box holds, so with the +reset removed the button would inherit the selection and the user would get what +they asked for. + +**So this is a decision, not a bug to fix quietly.** Two behaviours are +defensible and each breaks the other's case: + +- **Inherit.** The account box is a persistent scope and the buttons are + queries within it. This is what the note asks for. The risk is the one the + comment names: a query that already scopes itself, such as a rules preview + with `path:"work/**"`, gets scoped twice and matches nothing. +- **Reset.** A saved query is self-contained and says exactly what it shows. + This is what ships. Its cost is that the account dropdown looks like a filter + and silently is not. + +**Approach.** Ask the user which they want before writing anything. If it is +inherit, the narrow version is to inherit only for hand-written and generated +entries and keep the reset for a preview, since a preview is the one caller that +supplies its own path; the rules preview already forces index 0 itself at +`:1637`, so it does not depend on this line. + +**Constraints.** + +- Whatever is chosen, one rule for every saved-query surface: the buttons, the + menu and the dropdown all reach `runSavedQuery()`, and three behaviours here + would be worse than either one. +- A saved query that DOES name an account must keep overriding the box. That is + not in question and no test should lose it. +- `Account::scopedQuery()` is the only place a scope is applied. Do not add a + second one to make a caller behave. + +## 91. Double-clicking a thread could open it in its own window + +**Observed (user, notes):** "double clicking a message/thread should open it in +its own view (?) UX not sure." + +**Unspecified, and the user says so in the note itself.** Recorded so it is not +lost, not planned. What would have to be decided first: whether "its own view" +is a separate top-level window or a tab, whether it stays live as the list +refreshes under it, and what it does that the message pane does not. There is no +cause to verify because nothing is broken. + +**One constraint worth writing down now**, because it decides the size: the +message pane is a `QWebEngineView` inside `MessageView`, and a second window +means either a second web view, which spawns a Chromium render process per +window, or moving the one view between parents. `CLAUDE.md` records the +per-view process cost as the reason the pane renders a list into a single view. +A design that opens three messages in three windows is three render processes. + +## 92. Nothing distinguishes a tag written by a rule from one the user applied + +**Observed (user, notes):** "should the UI allow to discriminate when a message +has been tagged by a rule?" + +**Asked as a question, and the answer decides the size**, which is why this is +unspecified rather than sized. The information does not exist anywhere today: a +tag written by the `post-new` hook is an ordinary notmuch tag and carries no +provenance, so the UI cannot show what nothing records. + +That makes it two very different items wearing one sentence: + +- **Display only.** If the question is "which of these tags could a rule have + written", it can be answered from `~/.config/mailrules/rules.json` without + storing anything: the rules name their tags, so a chip for a tag that some + rule writes can be drawn differently. Cheap, and honest only as "a rule writes + this tag", never as "a rule wrote THIS one". +- **Real provenance.** If the question is "was THIS message tagged by a rule", + something has to record it, and that is a format change to the shared rules + file or an extra tag per rule, applied by the hook. That is a two-repo change + under "Changing the shared rule format" in `CLAUDE.md`, with a live hook + running every ten minutes on real mail. + +**Approach.** Ask which question the user meant. Do not start on the second one +by inferring it from the first. + +**Constraint.** The hook is not this repo's to weaken. If provenance is wanted, +the marker has to be something the hook can write without touching `unread` or +`inbox`, and without consuming `tag:new` when the rules fail to load. + ## Deferred, unsized, or split out Items noted while triaging but not part of the original list. Same numbering |
