summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-15 09:37:54 +0200
committerDanilo M. <danix@danix.xyz>2026-08-15 09:37:54 +0200
commitdeec4e1bbf2a6af6f3f86043080fd5b4df535e03 (patch)
tree7c0e13f02d2f99eba0bb6fd55be7e10f46ac79b9 /docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md
parent38b9466aea7daa0ddde91c1de7d8082ac000c238 (diff)
downloadqtmaildir-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/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.md165
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