summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/plans')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md114
1 files changed, 91 insertions, 23 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 0a1f4dc..c0c1c1e 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
@@ -100,7 +100,7 @@ taking that too literally.
| 41 | A message whose HTML body carries a `Content-Id` renders blank | correctness | S | **done** |
| 42 | "Syncing..." says nothing about what is being synced | feedback | S | **done** |
| 43 | No "Mark all read" for the current view | workflow | S | **done** |
-| 44 | No way to manage the filters applied at sync time | workflow | ? | open, unspecified |
+| 44 | No way to manage the filters applied at sync time | workflow | M | open, specified 2026-08-12; see `specs/2026-08-12-tagging-rules-design.md`. Spans this repo and `mailctl` |
| 45 | Two Sync buttons, and only one of them works properly | correctness | S | **done** |
| 46 | `uiStateSurvivesARestart` fails under the offscreen platform | testing | XS | **done** |
| 47 | The query bar looks unfinished, and cannot be cleared by mouse | presentation | XS | **done** |
@@ -117,7 +117,7 @@ taking that too literally.
| 58 | `message_zoom` documents a 0.5 to 3.0 range and enforces none of it | correctness | XS | **done** |
| 59 | Archive and Mark all read shipped with the same icon | presentation | XS | **done** |
| 60 | Next thread dead-ends on the last reply of an expanded thread | defect | XS | **done**; already fixed by 5487d58, see below |
-| 61 | `test_mainwindow` fails intermittently, about 1 run in 20 | testing | S | open; predates the card list, reproduced on f72dba9 |
+| 61 | `test_mainwindow` fails intermittently, about 1 run in 20 | testing | S | open; cause established 2026-08-11 (the user's cron sync holds the mbsync lock), fix is item 38's seam applied suite-wide |
| 62 | No config option for the date format on a card | presentation | XS | **done** 2026-08-11 |
| 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 |
@@ -130,6 +130,7 @@ taking that too literally.
| 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 |
+| 74 | "Searching..." keeps claiming a query is running while rows are already arriving | feedback | XS | open; cause measured 2026-08-11, the delay itself is the cold page cache and is not fixable here |
Sizes are rough: XS under an hour, S a sitting, M a session.
@@ -3009,15 +3010,35 @@ So the item is not "expose the existing filters in the UI". It is one of:
a change to what this application is: v1 is read-and-organize over an index
someone else fills.
-**Next step: ask the user which, and where the rules live today.** The
-dry-run request is the strongest hint that they have something in mind that
-already runs, since a dry run only makes sense against rules that exist.
-
-**Constraint if it is built here:** `CLAUDE.md` records that this project does
-no network protocol work at all and that fetching is external. A filter engine
-that rewrites the Maildir would not violate that literally, but it would put
-qtmaildir in the business of moving mail, which is a decision to take
-deliberately rather than by implementing a dialog.
+**Answered and specified 2026-08-12.** It is the first option. The rules exist,
+in the notmuch `post-new` hook inside the user's Maildir: hand-written
+`notmuch tag` lines, each scoped to `tag:new`, tag-only by design. They carry
+substantial reasoning in shell comments about which senders each rule
+deliberately excludes.
+
+The design is `specs/2026-08-12-tagging-rules-design.md`. In short: the rules
+move to `~/.config/mailrules/rules.json`, a tool-neutral store both qtmaildir
+and `mailctl` read, with unknown fields preserved across a write by either tool
+so neither owns the format. `post-new` becomes a Python loop over that file,
+living in the mailctl repository. A rule stores no scope, so the same rule
+serves the hook (scoped `tag:new`), a dry run (whole corpus, counts only) and a
+future backfill. qtmaildir gets `TagRules` plus a management dialog; mailctl
+gets read-only `rules list|show|dry-run`.
+
+**The constraint below was considered and is not triggered.** No rule engine is
+built in qtmaildir and nothing rewrites the Maildir: the tagging still happens
+in the notmuch hook, and qtmaildir edits the rule file and counts matches.
+
+**Backfill is deliberately out of v1**, per the user's decision, and is the one
+piece that will force a revision to `CLAUDE.md`'s "no destructive-action
+confirmation, undo instead" rule, which the user has said is due for revision
+anyway. A rule that is safe against arrivals is not safe unscoped.
+
+**Constraint if a rule engine were ever built here:** `CLAUDE.md` records that
+this project does no network protocol work at all and that fetching is external.
+A filter engine that rewrites the Maildir would not violate that literally, but
+it would put qtmaildir in the business of moving mail, which is a decision to
+take deliberately rather than by implementing a dialog.
## 45. Two Sync buttons on the main window
@@ -4038,18 +4059,20 @@ suite passed 15/15 immediately after failing twice on the same binary. What
distinguished the failing runs was other work happening on the machine at the
time, which points at timing rather than at leaked state between tests.
-**Cause: not established.** Both tests are timer- and QProcess-driven, and the
-obvious hypothesis is that a `QTimer` or a `runExternalSync` step is being given
-a deadline that a loaded machine misses. That was not confirmed: synthetic CPU
-load across four cores did not reproduce it in 8 runs, so the trigger is
-narrower than "the machine is busy". Do not fix this from the hypothesis alone.
-
-**Approach.** Run the binary in a loop with `-v2` until it fails and keep the
-full log, rather than re-running until it passes. The two assertions are about
-state that should be set synchronously by the time they run, so the question to
-answer first is which step is actually asynchronous: if `sendThreadTagChange`
-reaches the worker on a queued connection, the test may be asserting before it
-lands, and the fix is a `QTRY_COMPARE` rather than a longer timeout.
+**Cause: established 2026-08-11, and it is the user's own cron sync.** The
+trigger is another process holding the mbsync lock while the suite runs, not
+machine load: measured 0 failures in 30 runs with no lock held, and 30 failures
+in 30 runs with one held. Reproduce deterministically with
+`flock /tmp/mbsync.lock -c 'sleep 60'` in one shell and the suite in another.
+This supersedes the earlier "not established" reading and the load hypothesis,
+which synthetic CPU load had already failed to confirm.
+
+**Approach.** Item 38 already built the seam: `MainWindow::setLocksPathForTesting`
+is a static hook that points the lock check at a path the test controls, so a
+test that sets it cannot see the user's real sync. The fix is giving the rest of
+the suite that same seam, most likely from a fixture or an init hook rather than
+per-test, so a newly added test gets it without having to remember. One of item
+71's tests already uses the seam, so there is a worked example to copy.
**Constraint.** A flaky test is worse than a missing one, because it teaches
everyone to re-run the suite instead of reading it. This one already cost a
@@ -4584,6 +4607,51 @@ to stay findable under its number.
**Size: S**, and it is bookkeeping, so it competes with real work rather than
blocking it.
+## 74. The first query after boot sits on "Searching..." for seconds
+
+**Observed (user, 2026-08-11):** the first start of the day takes noticeably
+longer to show its default view, and the delay happens while the status bar
+reads "Searching...".
+
+**Cause: the notmuch index paging in from disk, not this application's code.**
+Measured by instrumenting `main()`, the `MainWindow` constructor and
+`NotmuchWorker::runQuery` behind an environment variable, then running the same
+`tag:inbox` query (4444 threads) warm and again after evicting the index from
+the page cache with `posix_fadvise(POSIX_FADV_DONTNEED)`:
+
+| phase | warm | cold |
+|---|---|---|
+| `notmuch_query_search_threads` returns | 0 ms | 411 ms |
+| first batch of 200 reaches the model | 10 ms | 642 ms |
+| walk complete, all 4444 threads | 154 ms | 5714 ms |
+
+A 37x difference over the identical code path. The index measured 1.1 GB. The
+in-process startup costs nothing by comparison: `QApplication` in 20 ms, the
+whole `MainWindow` constructor in ~120 ms, and the window is shown and
+interactive at ~196 ms in both the warm and the cold run.
+
+**There is nothing to fix in the query path**, and the measurement exists mainly
+so this is not re-investigated. Two things it did establish that are worth
+keeping. The default startup view is whichever saved query `startup_query` names,
+defaulting to `Unread`, so a user with an empty unread view never sees this at
+all and a user whose default is Inbox always does. And batching already works:
+rows land from 642 ms cold, long before the 5714 ms finish.
+
+**The one real defect it exposed is the status bar.** "Searching..." is set once
+in `runQuery` and cleared only on `queryFinished`, so it keeps claiming the
+query is running for the full 5.7 s while rows are visibly arriving behind it.
+That makes a slow query read as a frozen one. The fix is to update the text per
+batch with the count so far rather than holding one string, which changes no
+timing and only stops the bar from lying.
+
+**The user declined this on 2026-08-11**, having asked for the explanation
+rather than a change. Recorded as open because the status bar is still
+inaccurate, not because anything is expected to happen.
+
+**Size: XS** for the status bar. The cold-cache cost itself is not addressable
+here and should not be attempted: prefaulting 1.1 GB at startup to make one
+query look fast is a worse trade than the wait.
+
## Deferred, unsized, or split out
Items noted while triaging but not part of the original list. Same numbering