aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md235
1 files changed, 233 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 be52da9..4039753 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
@@ -19,8 +19,10 @@ came from one such pass on 2026-08-04 and included two defects that had gone
unrecorded here for a while. Items 39 to 45 came from the 2026-08-05 pass, which
found one more defect (41, a message body silently dropped by the MIME walk) and
one item that cannot be planned at all until the user says where the thing it
-manages lives (44). Compare the two at the start of a session; the procedure is
-in `CLAUDE.md`.
+manages lives (44). Items 121 to 123 came from the 2026-08-20 pass, which found
+that item 74 had closed only half of what its note asked for, and that the
+README had gone stale enough to document a mandatory config key by omitting it.
+Compare the two at the start of a session; the procedure is in `CLAUDE.md`.
Numbering is stable. New items append with the next free number and never
renumber, so a note referring to "item 7" keeps meaning the same thing. An item
@@ -185,6 +187,12 @@ taking that too literally.
| 118 | No way to empty the trash from inside the app | workflow | S | open, 2026-08-17. **Blocked on 103**, which creates the trash in the first place. Deliberately left out of 103's spec at the user's request rather than squeezed in |
| 119 | The unsynced-changes count cannot be opened to see what it counts | information | S | open, 2026-08-19, from the notes. One of the four things it sums carries no message ids at all, so a list cannot be complete without a change to how the count is kept |
+| 121 | The thread list shows nothing while a query is running | feedback | S | open, 2026-08-20, from the notes. Follows item 74, which fixed the status-bar half and left the list itself blank |
+| 122 | The README documents a version of the app that no longer exists | documentation | M | open, 2026-08-20, from the notes. Delete-to-trash is entirely undocumented, including a config key a user must now set |
+| 123 | Sending mail is not designed | v2 | ? | open, 2026-08-20, from the notes. Brainstorm only, explicitly `#plan-only`; the user places most open UX behind it |
+
+| 124 | The worker reads the index directory as the mail root | defect | S | open, 2026-08-20. Blocks moving the index to an SSD. Under a split `mail_root`/`path` config, Delete would move mail INSIDE the index directory, where mbsync cannot see it |
+
Sizes are rough: XS under an hour, S a sitting, M a session.
---
@@ -805,6 +813,229 @@ specifically to find out what those were.
for the fourth, and the item is not complete without it.
+## 121. The thread list shows nothing while a query is running
+
+**Observed (user, from the notes):** "can we show a spinner in the left panel
+while 'Searching' is going? Especially at first run, the loading wait is several
+seconds, and the status bar starts updating 'Searching N threads' after the
+first have already appeared. Before that the program seems broken."
+
+**This is the half of item 74 that was never built**, and the note is precise
+about which half. Item 74 closed on 2026-08-15 having fixed the status bar,
+which used to set "Searching..." once and hold it for the whole walk. The count
+the note describes is that fix working as designed: it is written from
+`m_model->rowCount()` in `onThreadsReady`, so by construction it cannot report
+anything before the first batch has landed.
+
+**Cause (verified in the code).** `MainWindow::runQuery` clears the model and
+sets the status text (`src/mainwindow.cpp:2414`), and nothing else in the view
+changes. The thread list is then an empty `QTreeView` until `appendBatch` runs
+on the first batch, so **a query in progress and a query that matched nothing
+render identically**. There is no busy state on the view at all.
+
+**The gap is measured, and item 74's numbers understate it badly.** Re-measured
+on 2026-08-20 against the user's real inbox, seven minutes after boot, with the
+index verifiably unread (0.0% of 1037 MB resident). Item 74's figures came from
+`posix_fadvise(POSIX_FADV_DONTNEED)` eviction, which does not reproduce a real
+cold boot on this hardware:
+
+| phase | item 74, 2026-08-11 | measured cold, 2026-08-20 | warm |
+|---|---|---|---|
+| `search_threads` returns | 411 ms | **673 ms** | 2 ms |
+| first batch of 200 rows | 642 ms | **2008 ms** | 12 ms |
+| walk complete | 5714 ms | **38618 ms** | 154 ms |
+| threads | 4444 | 4628 | 4628 |
+
+So the list is blank for **two seconds**, and keeps growing for **thirty-eight**,
+on 4% more mail. The user's note said "several seconds" and the note was right.
+
+**The cause is the storage, not the code.** `/data` is `/dev/sda1`, a 7200rpm
+platter (`rotational: 1`); warm, the identical walk is 154 ms, a 250x
+difference. Item 124 is the prerequisite for moving the index to the NVMe SSD
+already in the machine, which would make this gap ~12 ms and reduce this item to
+a nicety.
+
+**Approach.** A busy state on the left pane between `runQuery` and the first
+`onThreadsReady`, cleared by whichever of the first batch or `queryFinished`
+arrives first. The empty-result case must be distinguishable from it: when
+`queryFinished` reports zero, the pane should say so rather than returning to a
+blank list, which is the same ambiguity one step later.
+
+The likely shape is an overlay or a placeholder row rather than a literal
+spinner widget, but that is a design question for the user, not a decision to
+take here. A spinner also has to be animated by the UI thread, which is free
+here since the work is on the worker, but that is worth stating because it is
+the usual reason a spinner does not spin.
+
+**Constraints.**
+
+- **A background refresh must stay silent.** `onThreadsReady` returns early on
+ the refresh branch and `onQueryFinished` does the same, deliberately, so a
+ sync-driven refresh does not flicker the status bar. A busy indicator that
+ ignored that guard would make every cron sync flash the list. That silence is
+ already a test, and it should cover this too.
+- **Item 74's decision not to address the cold cost was taken on wrong
+ numbers** and is worth revisiting, though not here. It judged a 5.7 s wait not
+ worth prefaulting 1.1 GB; the real figure is 38.6 s. The answer is not
+ prefaulting either way: it is item 124 plus moving the index off the platter.
+ This item makes the remaining wait legible, nothing more.
+- Nothing about the query timing may change.
+
+**Size: S.**
+
+## 122. The README documents a version of the app that no longer exists
+
+**Observed (user, from the notes):** "documentation needs updating, EG the
+README.md reports various things not up-to-date anymore."
+
+**Cause (verified).** `README.md` was last touched on 2026-08-15 by b405e32,
+which moved the SlackBuild out to the `my-slackbuilds` repo. Everything released
+since then is absent from it. Releases 0.19.0 through 0.26.1 all landed after
+that commit.
+
+**Measured, by grepping both documents for the same terms:**
+
+| term | README | CHANGELOG |
+|---|---|---|
+| `trash` | 0 | 14 |
+| `restore` | 0 | 7 |
+| `Select all` | 0 | 3 |
+| `deleted-from` | 0 | 0 |
+
+**One of these is worse than stale documentation.** Item 103 made a per-account
+`trash` key MANDATORY: an account without one produces a config warning, and
+Delete cannot work. The README is the only place a user reads about configuring
+an account, and it does not mention the key at all. So the documented config
+produces a warning against the current binary, and the feature that needs it is
+undocumented. The `deleted-from:<folder>` tag is likewise invisible, and a user
+who sees it on a message has nowhere to look it up.
+
+**Approach.** An audit against the changelog rather than a rewrite: walk the
+sections from 0.19.0 forward and check each user-visible change for a README
+home. The config section and the keyboard-shortcut table are the two most
+likely to have drifted, since both enumerate things that have been added to.
+
+**Constraints.**
+
+- **The changelog is the evidence, not memory.** Every entry since b405e32 is
+ written down; work from it.
+- **`### Upgrading` sections are the priority.** They exist precisely because a
+ user's config or habits had to change, and those are the paragraphs whose
+ absence from the README costs the user a broken setup rather than a moment of
+ confusion.
+- The "Development Approach" section at the bottom is required by the user's
+ global preference and must survive any edit.
+- No personal details, per the same preference: account names in examples stay
+ generic.
+
+**Size: M.** The audit is most of it; the writing is small once the list exists.
+
+## 123. Sending mail is not designed
+
+**Observed (user, from the notes):** "we should start brainstorming sending
+emails. Most of the open issues are UX. #v2 #plan-only #new-branch".
+
+**Not a defect and not implementation work.** The note tags it `#plan-only`, and
+this repo's own scope statement agrees: `CLAUDE.md` records that v1 is
+read-and-organize only and that compose and send are v2. Recorded here so the
+backlog stops being silent about the largest thing the user has written down.
+
+**What it blocks.** Item 72 (khard/khal) is explicitly placed after send by the
+user's own note, and cannot be specified before it. The completion machinery
+that would serve recipient completion already exists as `QueryCompleter`, and
+the `QLineEdit::setCompleter` trap in `CLAUDE.md` applies directly to any
+multi-recipient field, so there is prior art to reuse rather than a blank page.
+
+**What it does NOT change.** The architecture note that this application does
+**no network protocol work at all** is load-bearing: fetching is `mbsync` via
+`assets/mailsync.sh`, and sending should be an external script on the same
+model, not an SMTP client written here. A design that puts a socket in this
+process is out of scope regardless of how the UI turns out.
+
+**Approach.** Brainstorm first, on its own branch, producing a spec under
+`docs/superpowers/specs/` before any code. The open questions are all UX, as the
+note says: where a composer lives, how a draft is stored so `notmuch` can see
+it, what reply and forward do to the thread the user is looking at, and how a
+queued message reaches the sending script.
+
+**Constraints.**
+
+- **Ask the user before designing.** They have not said what they pictured, and
+ this is the item where guessing costs the most.
+- Drafts are already visible to the app: item 67 counts them in the placeholder
+ pane, so a draft folder is configured and indexed.
+- `#new-branch` is the user's own tag on it.
+
+**Size: `?`** until the brainstorm has happened. It is the largest open item by
+some distance.
+
+## 124. The worker reads the index directory as the mail root
+
+**Observed (measured, 2026-08-20):** not reported from use. Found while
+measuring item 121's cold-start cost, which established that the 1.1 GB notmuch
+index sits on a 7200rpm platter (`/dev/sda1`, `rotational: 1`) while an NVMe SSD
+sits idle in the same machine. Moving the index to the SSD is a notmuch
+configuration change and needs no code, but qtmaildir does not survive it.
+
+**Cause (verified against notmuch 0.39 and a throwaway database).** notmuch
+supports splitting the index from the mail with two keys:
+
+```ini
+[database]
+mail_root=/data/Mail
+path=/home/you/.local/share/notmuch
+```
+
+Under that layout `notmuch_database_get_path()` returns the **index**
+directory, not the mail root. Measured on a split test database:
+
+| accessor | legacy (`path` only) | split |
+|---|---|---|
+| `notmuch_database_get_path()` | mail root | **index dir** |
+| `notmuch_config_get(NOTMUCH_CONFIG_MAIL_ROOT)` | mail root | mail root |
+
+`notmuchworker.cpp` calls `get_path()` at six sites and treats every one as the
+mail root: lines 297, 755, 900, 901, 1062, and the `relativeFilePath()` calls
+at 364, 380 and 918 that consume them.
+
+**One of those six moves mail, and it is the dangerous one.**
+`moveMessages` composes its destination as
+`root + "/" + destFolder + "/cur"` (`src/notmuchworker.cpp:755`). Under a split
+config `root` is the index directory, so Delete would move the message into
+`<index>/Trash/cur`: outside the Maildir, invisible to mbsync, and gone from
+every other client. That is item 103's stranded-mail failure with a new cause,
+and this repo has already shipped that class of bug once.
+
+The rest degrade rather than destroy. `relativeFilePath()` against the wrong
+root yields `../../../data/Mail/account/cur/...` instead of `account/cur/...`,
+so no path matches an account prefix and every row resolves to no account,
+which is exactly what the comment at line 294 already warns about.
+
+**Approach.** Replace `notmuch_database_get_path()` with
+`notmuch_config_get(m_db, NOTMUCH_CONFIG_MAIL_ROOT)` at the six sites.
+
+**No conditional is needed, and that is the point.** `MAIL_ROOT` returns the
+mail root under BOTH layouts, verified above: under a legacy `path`-only config
+it equals `get_path()`, so the change is a no-op against the current
+configuration and correct against the split one. A fallback to `get_path()` when
+`MAIL_ROOT` is NULL is the tempting belt-and-braces addition and should be
+resisted unless a NULL is actually observed, since it reintroduces the wrong
+answer on the path where it matters.
+
+**Constraints.**
+
+- **`moveMessages` is the site to test hardest.** A wrong root there reaches the
+ mail server, per the "Delete MOVES the file" note in `CLAUDE.md`.
+- The test fixture must build a database whose index is NOT inside the mail
+ root, or it cannot tell the two accessors apart: under the ordinary fixture
+ layout both return the same string and a mutation stays green.
+- `notmuch_config_get` returns a string owned by notmuch (`notmuch.h:2585`);
+ do not free it.
+- Nothing about the legacy layout may change. The migration is the user's to
+ perform, separately, once this ships.
+
+**Size: S.** Six call sites and a fixture that can tell them apart.
+
## Deferred, unsized, or split out
Items noted while triaging but not part of the original list. Same numbering