aboutsummaryrefslogtreecommitdiffstats
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-closed.md97
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md206
2 files changed, 236 insertions, 67 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
index bcb6431..945aacd 100644
--- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
+++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
@@ -10278,3 +10278,100 @@ when it was written with `tr()`, and a comma joins a list the same way in
Italian; a translatable separator is a string for a translator to get wrong for
no gain.
+## 184. New mail waits up to ten minutes, because sync is a fixed cron tick
+
+**Done 2026-09-13, outside this repository.** Built as `mail-watcher`, its own
+project at `~/Programming/GIT/mail-watcher`, designed in that repository's
+`docs/superpowers/specs/2026-09-13-mail-watcher-design.md` and confirmed running
+on this machine as `~/bin/mail-watcher.sh --config ~/.config/mail-watcher/config.ini`.
+Nothing in `src/` changed, which is the outcome this entry argued for: qtmaildir
+is as ignorant of IMAP as it was, and the no-network-protocol rule is intact.
+
+**The three decisions this entry said could not be made by reading the repository
+were made against the live servers.** A watcher of ours rather than a third-party
+daemon, so there is no new SlackBuild to maintain; both hosts were probed and
+advertise IDLE; and the connection count was the real constraint, since watching
+all 50 synced folders would have sat one folder under the mail host's cap. The
+answer was default-watch with an explicit exclude list, so the failure mode is
+watching too much rather than silently missing mail, which matters because
+server-side rules file mail directly into folders without touching INBOX.
+
+**Both constraints held.** The cron tick stays as a backstop, so a watcher that
+dies silently degrades to the old ten minute wait rather than stopping mail; and
+`/tmp/mbsync.lock` is still the shared mutex, so a triggered sync, a cron sync
+and a click in qtmaildir serialise as before.
+
+**Item 174 was the blocker and was the right one.** `mailsync.sh` already writes
+`~/.local/state/qtmaildir/syncstatus.json`, so the watcher had a tested reporting
+channel to trigger into rather than making the application's guesswork about
+external runs worse.
+
+The original entry follows.
+
+### The entry as it stood while open
+
+**Observed (user, 2026-08-29):** "the 10 minutes fixed tick has always bothered
+me, I want the changes to my mail to be immediate, the 10 minutes mark is a
+compromise, it had started at 30 min and was awful."
+
+**Split the want in two, because only half of it is open.**
+
+- **Outgoing is already immediate.** An edit arms `auto_sync_delay_ms`, 2
+ seconds by default (item 71), so marking a message read reaches the server
+ without waiting for the tick.
+- **Incoming is the gap.** Mail that arrives is invisible until the next cron
+ run of `mailsync.sh`, so the wait is uniform on [0, 10] minutes with a mean
+ of five.
+
+**Why a faster tick is not the fix.** Polling every minute is ten times the
+connections and the server load for a mean wait of thirty seconds, and it is
+still a poll: the wait is bounded by the interval however small it gets. The
+answer to "tell me when something arrives" is IMAP IDLE, where the server holds
+the connection and speaks first.
+
+**Where IDLE may live, and where it must not.** `AGENTS.md` states this
+application does NO network protocol work at all: fetching and sending are
+external commands, which is what keeps a mail client out of TLS, authentication
+and an IMAP state machine. IDLE inside qtmaildir would break that rule outright
+and is not on the table.
+
+It does not need to be inside. mbsync has no IDLE mode, being a batch syncer
+that runs and exits; the tools that hold a connection and TRIGGER a sync are
+separate programs (`goimapnotify` and the older `imapnotify` are the usual
+ones). That shape fits the architecture exactly: the watcher replaces the cron
+line, runs `mailsync.sh <channel>` on activity, and qtmaildir stays as ignorant
+of IMAP as it is today. The script's flock still serialises a triggered sync
+against a manual one.
+
+**Blocked on item 174**, and not merely sequenced after it. A watcher makes
+syncs arrive at unpredictable times rather than on a known tick, which makes
+the application's current guesswork about external runs worse. Item 174's
+status file is the reporting channel this needs, and building it first means
+the watcher has a tested one to write into.
+
+**Decisions needed before any code, none of which can be made by reading this
+repository.**
+
+- **Which watcher**, or a small one of ours. A third-party daemon means a
+ SlackBuild in the `my-slackbuilds` repo and a package to maintain.
+- **What the mail server supports.** IDLE is optional in IMAP, and a server
+ without it leaves polling as the only mechanism.
+- **How many connections.** One per account per watched folder, held open
+ indefinitely; some servers cap concurrent connections.
+
+**Constraints.**
+
+- **Keep a slow cron tick as a backstop.** A held connection drops on network
+ sleep, a server restart or a laptop suspend, and a watcher that dies silently
+ stops mail altogether, which is worse than a ten minute wait. Belt and
+ braces: the watcher for latency, a slow tick so a dead watcher is survivable.
+- **The lock is the shared mutex** and stays so. A triggered sync, a cron sync
+ and a click in qtmaildir must continue to serialise through
+ `/tmp/mbsync.lock`, or two mbsync runs corrupt Maildir UID state.
+- **This is not a qtmaildir daemon.** A long-running qtmaildir service was
+ considered and rejected in the same conversation: it answers none of items
+ 174 or 125 better than a file does, and it adds a process that can wedge and
+ take mail delivery with it. What the user wants is a watcher, which is a
+ different thing in a different place.
+
+
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 27da155..6792353 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
@@ -257,7 +257,7 @@ taking that too literally.
| 181 | The thread dashboard does not follow a write to the conversation it shows | defect | XS | **done 2026-08-29**, unreleased, on `thread-row-identity`, from the notes. The dashboard draws a `ThreadDigest` built by the worker from the INDEX, which arrived only on selection, so a tag write moved the model and the card and left the pane reporting the count the conversation had when it was opened. Reachable from the dashboard's OWN Mark all read button. Re-requested from `onTagsApplied()`, where the write is confirmed: queued beside the write it races it and answers from the state before it, which is how the first fix passed review and failed the test. Section in the closed file |
| 182 | An edit made during a sync is announced twice and never says it is waiting | defect | XS | **done 2026-08-29**, unreleased, on `thread-row-identity`, found by hand. The hold branches set a deliberately NON-transient label; all three callers overwrote it a line later with the bare action, so the user was told the write had landed and then told again when it really did. `announceAction()` adds the wait to the action rather than replacing it, since that announcement is what stands in for the confirmation dialog this project rules out. Section in the closed file |
| 183 | `undoingAMarkReadRestoresOnlyWhatWasUnread` fails about 1 run in 9 under the full suite | testing | ? | open, 2026-08-29, measured. Item 176's regression test, which guards the undo that rewrote 44 messages of real mail. Nine runs on master: 4 standalone, 3 under `ctest -R mainwindow`, 3 under the FULL parallel suite, and the single failure was in the last group. Not a regression, the base commit behaves the same. Probably the same root cause as item 136 and worth solving with it |
-| 184 | New mail waits up to ten minutes, because sync is a fixed cron tick | workflow | ? | open, 2026-08-29, from the user: the 10 minute tick "has always bothered me", and it is already a compromise down from 30. Outgoing edits are immediate (`auto_sync_delay_ms`), so this is the INCOMING half only. Polling faster is not the answer; IMAP IDLE is, and it lives in a watcher that triggers `mailsync.sh`, NOT in qtmaildir, which does no network protocol work. Needs decisions first: which watcher, whether it packages on Slackware, and what the server supports. **Blocked on 174**, whose status file is the reporting channel this needs anyway |
+| 184 | New mail waits up to ten minutes, because sync is a fixed cron tick | workflow | ? | **done 2026-09-13**, outside this repo and confirmed running on this machine (PID watching, `~/bin/mail-watcher.sh --config ~/.config/mail-watcher/config.ini`). Built as `mail-watcher`, its own repository at `~/Programming/GIT/mail-watcher`, designed in its own `docs/superpowers/specs/2026-09-13-mail-watcher-design.md`. It took the shape this entry argued for and settled the three decisions it listed: a watcher of ours rather than a third-party daemon, so no new SlackBuild; one Python file, standard library only; one thread per watched folder with its own reconnect, one trigger loop owning every `mailsync.sh` invocation with a debounce, default-watch with an explicit exclude list. Both constraints held: the cron tick stays as a backstop, and `/tmp/mbsync.lock` is still the shared mutex. Nothing in `src/` changed, which is the point, the no-network-protocol rule is intact. Section in the closed file. Original entry: open, 2026-08-29, from the user: the 10 minute tick "has always bothered me", and it is already a compromise down from 30. Outgoing edits are immediate (`auto_sync_delay_ms`), so this is the INCOMING half only. Polling faster is not the answer; IMAP IDLE is, and it lives in a watcher that triggers `mailsync.sh`, NOT in qtmaildir, which does no network protocol work. Needs decisions first: which watcher, whether it packages on Slackware, and what the server supports. **Blocked on 174**, whose status file is the reporting channel this needs anyway |
| 185 | The message-pane bar offers Reply and Forward on a trashed message | presentation | S | **done 2026-08-29**, unreleased, with 186. The bar has a third branch keyed on the SELECTION being in a trash folder, the same predicate the menus use: Restore, Delete permanently and Empty trash replace the reply pair, and Restore alone is tinted. Added `purge`, the selection-scoped sibling of `empty_trash`, which inherits both its safeguards. Refilled from the digest as well as from the selection, since a conversation's trash-ness is not known until every path is reported. Section in the closed file. Original entry: `MainWindow::refreshMessageBarActions()` (`mainwindow.cpp:2311`) swaps the bar's message half for a DRAFT and for nothing else, so the trash view shows the two actions that make least sense there. The notes ask for Restore and Delete permanently in their place, and for Delete to move here from the main toolbar (item 186). The visibility rules already exist in `refreshTrashActions()`; what is missing is the bar consulting them |
| 186 | Delete sits on the main toolbar rather than beside Reply and Forward | presentation | XS | **done 2026-08-29**, unreleased, with 185. Moved to the message bar's ordinary branch; still in the Message and context menus. Section in the closed file. Original entry: `toolBar->addAction(... "delete")` at `mainwindow.cpp:2251`. The user places it with the message actions, so this rides with item 185 rather than being done alone: moving it before the bar is trash-aware leaves Delete in a bar that still offers Reply on trashed mail |
| 187 | There is no Spam view beside Trash | workflow | M | open, **specified 2026-09-10** in `specs/2026-09-10-spam-view-design.md`, which covers 190 and 195 too; read that rather than this row. Grew again: the user added Empty Spam (a MOVE to the trash, per account) and the `deleted-from:` -> `moved-from:` rename. 2026-08-29, from the notes; **shape settled 2026-08-29** after two corrections and three decisions from the user. Spam works like Trash: path-based, a mandatory per-account `spam` key, and Mark spam MOVES the file. Every account can now reach a spam folder, the three Gmail ones having gained `[Gmail]/Spam` in `.mbsyncrc` this session. Grew from S to M: the move path, the origin tag and a cleanup pass are three parts, and it changes what an existing action does. See the entry |
@@ -270,6 +270,9 @@ taking that too literally.
| 194 | No abuse reporting from a flagged message | workflow | L, split | open, 2026-09-08, from the notes and **confirmed by the user the same day as a feature they want and will build**. Parse a flagged `.eml`, extract IOCs, resolve abuse contacts via RDAP, generate X-ARF (RFC 5965), fan out to AbuseIPDB/URLhaus/VirusTotal and to abuse desks, backed by MISP via PyMISP. **One gesture here, the engine in a sidecar**: the split is architectural (four outbound protocols, which `src/` does not do) and not a judgement on the feature. qtmaildir's half is a message-bar button that marks spam and offers to report, with a confirmation; it is S and buildable before the sidecar exists. The user is a security consultant filling a phishing database, so the sidecar is the point rather than an accessory. Needs a spec for the sidecar; the qtmaildir half needs only 187/190 settled. Two of the user's constraints are safety properties: redact recipient identifiers before submission, and never fetch remote content during parsing |
| 195 | Mark spam leaves the message unread | defect | XS | open, 2026-09-10, from the notes ("marking a message as spam without reading it doesn't remove the unread tag"). Verified: the action at `mainwindow.cpp:1788` adds `spam` and removes `inbox`, and names no other tag, so an unmarked message keeps `unread` and every unread count keeps counting it. Small on its own; it touches the same action item 187 rewrites into a move, so doing it inside 187 costs nothing and doing it alone is a two-word change to one `tagSelected()` call. One question for the user: whether marking spam should mark read, or whether the tag should simply not be part of the unread views once 187 makes the view path-based |
| 196 | Spam is never tagged automatically | workflow | ? | open, 2026-09-10, from the notes ("the app should be able to tag spam automatically leveraging intel from abusectl"). Depends on 194's sidecar existing: `~/Programming/GIT/abusectl` is a repo but nothing is on `PATH`, so the intel this would read does not yet have a shape to read. Also unspecified in direction: the natural home is the `post-new` hook rather than `src/`, since tagging at sync time is what `assets/hooks/mailrules.py` already does, and a rule sourced from an external database is a format question for both readers (see "Changing the rule format"). Ask the user what abusectl would expose before designing anything |
+| 198 | The unsynced-changes list never says which account a message belongs to | presentation | S | open, 2026-09-13, from the notes ("when clicking on the bottom right status bar, there's no way to discriminate what message belongs to what account"). The click opens `PendingChangesDialog` (item 119). Verified: `PendingChangeRow` (`pendingchangesdialog.h:32-50`) carries subject, action, `startsMessage` and `messageCount` and no account, so a list of five subjects across five accounts reads as one undifferentiated run. The data is reachable rather than missing: `accountForMessagePath()` (`mainwindow.cpp:6219`) resolves an account from a path, and `resolvePendingSubjects()` already walks every id in the worker and answers positionally, so the account is one more field on an existing round trip. One asymmetry to settle first: a held THREAD edit carries a thread id rather than a message id (`pendingChangeSnapshot()`, `mainwindow.cpp:5699`), and a thread can in principle span accounts, so the thread rows need a rule of their own rather than the message answer |
+| 199 | The window chrome uses the system icon theme, and the user wants a shipped set | presentation | M-L | open, 2026-09-13, from the notes ("we should ship our own icons, color themeable to be consistent in every theme a user may implement, since icons are a brand identity"). This deliberately REVERSES item 70, which drew the split as "panes are ours, chrome is the system's" and shipped `Marks` for the panes only; the note asks for the other half too, so it is a decision to revisit rather than a defect. Verified: the `themeIcons` table at `mainwindow.cpp:2211` and six `QIcon::fromTheme` sites in `composewindow.cpp` are every chrome icon, all resolved from the desktop theme. The mechanism already exists and is proven, `Marks::pixmap` compositing `fill="currentColor"` with `CompositionMode_SourceIn` so one asset serves a light and a dark palette, and `src/marks.h` records why it is compiled-in string literals rather than a `.qrc`. The size is the ARTWORK, not the code: item 70's six marks are shipped, this is roughly forty actions, each needing a drawing. Needs a decision from the user on scope before it can be sized honestly, and on whether the system theme stays as a fallback for an action with no shipped icon |
+| 200 | qtmaildir cannot be launched at a given account, thread or message | workflow | M | open, **specified 2026-09-13** in `specs/2026-09-13-cli-selectors-design.md`; read that rather than this row. The user settled three things: a second launch STEERS the running window over a `QLocalServer` rather than opening a second one, the selectors are `--account`/`--thread`/`--message` (`--query` dropped as the one with no caller), and a selector matching nothing opens the window normally and says so in the status bar. The design shrank on one side and grew on the other: `recoverStaleThread()` already runs `thread:<id>` with a deferred selection and is reused as a third caller, so the selectors are the small half, while the socket (connect-first ordering, stale-socket recovery, a degrade path when no socket is possible) is the real work and adds `Qt6::Network` to the component list. Original entry: open, 2026-09-13, from the notes ("the program should accept cli parameters like `--account` or `--thread`/`--message`, so that another app can launch qtmaildir opening that account's inbox or a certain message/thread"). Verified: `main.cpp:38-66` hand-rolls a `strcmp` loop over `argv` for `--version` and `--help` only, both answering before `QApplication` exists, which is deliberate and documented. Parsing is the small half and `QCommandLineParser` covers it; the item is bigger than it looks for two reasons. There is NO single-instance mechanism (no `QLocalServer` anywhere in `src/`), so a second launch opens a second window against the same notmuch database rather than steering the running one, and notmuch permits only one open handle per process. And the selector has to reach a query the startup path does not currently take, since `--thread` names a row that may not be in the configured startup view at all. Needs a decision from the user first: whether a second launch should focus the running window (which is the useful behaviour for "another app launches qtmaildir" and is the whole cost of the item) or simply start with a different query |
| 197 | No way to say a message is not spam | workflow | S | open, 2026-09-10, split out of the 187 design at the user's decision rather than built into it. Restore already covers what qtmaildir moved: a message it marked carries `moved-from:` and goes back where it came from. The gap is mail the PROVIDER's filter caught, which was never in an inbox and carries no origin tag, so "not spam" has no recorded destination to return it to. Needs two answers before it can be planned: where such a message goes (the account's inbox is the obvious guess and is a guess), and whether anything should tell the provider its filter was wrong, which is network work this application does not do and would belong in a sidecar like item 194's. No seam is needed in the meantime: `sendMove()` already takes any destination and any tags |
Sizes are rough: XS under an hour, S a sitting, M a session.
@@ -1317,72 +1320,6 @@ same discipline applies here.
- The suite baseline is currently ONE known failure. Anything that makes it two
intermittently costs the property that a red suite means something.
-## 184. New mail waits up to ten minutes, because sync is a fixed cron tick
-
-**Observed (user, 2026-08-29):** "the 10 minutes fixed tick has always bothered
-me, I want the changes to my mail to be immediate, the 10 minutes mark is a
-compromise, it had started at 30 min and was awful."
-
-**Split the want in two, because only half of it is open.**
-
-- **Outgoing is already immediate.** An edit arms `auto_sync_delay_ms`, 2
- seconds by default (item 71), so marking a message read reaches the server
- without waiting for the tick.
-- **Incoming is the gap.** Mail that arrives is invisible until the next cron
- run of `mailsync.sh`, so the wait is uniform on [0, 10] minutes with a mean
- of five.
-
-**Why a faster tick is not the fix.** Polling every minute is ten times the
-connections and the server load for a mean wait of thirty seconds, and it is
-still a poll: the wait is bounded by the interval however small it gets. The
-answer to "tell me when something arrives" is IMAP IDLE, where the server holds
-the connection and speaks first.
-
-**Where IDLE may live, and where it must not.** `AGENTS.md` states this
-application does NO network protocol work at all: fetching and sending are
-external commands, which is what keeps a mail client out of TLS, authentication
-and an IMAP state machine. IDLE inside qtmaildir would break that rule outright
-and is not on the table.
-
-It does not need to be inside. mbsync has no IDLE mode, being a batch syncer
-that runs and exits; the tools that hold a connection and TRIGGER a sync are
-separate programs (`goimapnotify` and the older `imapnotify` are the usual
-ones). That shape fits the architecture exactly: the watcher replaces the cron
-line, runs `mailsync.sh <channel>` on activity, and qtmaildir stays as ignorant
-of IMAP as it is today. The script's flock still serialises a triggered sync
-against a manual one.
-
-**Blocked on item 174**, and not merely sequenced after it. A watcher makes
-syncs arrive at unpredictable times rather than on a known tick, which makes
-the application's current guesswork about external runs worse. Item 174's
-status file is the reporting channel this needs, and building it first means
-the watcher has a tested one to write into.
-
-**Decisions needed before any code, none of which can be made by reading this
-repository.**
-
-- **Which watcher**, or a small one of ours. A third-party daemon means a
- SlackBuild in the `my-slackbuilds` repo and a package to maintain.
-- **What the mail server supports.** IDLE is optional in IMAP, and a server
- without it leaves polling as the only mechanism.
-- **How many connections.** One per account per watched folder, held open
- indefinitely; some servers cap concurrent connections.
-
-**Constraints.**
-
-- **Keep a slow cron tick as a backstop.** A held connection drops on network
- sleep, a server restart or a laptop suspend, and a watcher that dies silently
- stops mail altogether, which is worse than a ten minute wait. Belt and
- braces: the watcher for latency, a slow tick so a dead watcher is survivable.
-- **The lock is the shared mutex** and stays so. A triggered sync, a cron sync
- and a click in qtmaildir must continue to serialise through
- `/tmp/mbsync.lock`, or two mbsync runs corrupt Maildir UID state.
-- **This is not a qtmaildir daemon.** A long-running qtmaildir service was
- considered and rejected in the same conversation: it answers none of items
- 174 or 125 better than a file does, and it adds a process that can wedge and
- take mail delivery with it. What the user wants is a watcher, which is a
- different thing in a different place.
-
## 187. There is no Spam view beside Trash
@@ -1694,3 +1631,138 @@ which is a documented guess rather than a recorded destination.
destination and any tag lists, so a Not-spam action is a caller rather than a
capability. Provisioning for it now would be a hook with one hypothetical
caller, which is what YAGNI names.
+
+## 198. The unsynced-changes list never says which account a message belongs to
+
+**Observed (user, from the notes):** "when clicking on the bottom right status
+bar, there's no way to discriminate what message belongs to what account."
+
+**Cause.** Verified, not assumed. The bottom-right click is the unsynced-changes
+indicator, whose list is item 119's `PendingChangesDialog`. Its row struct
+(`pendingchangesdialog.h:32-50`) carries a subject, an action, `startsMessage`
+and `messageCount`, and nothing else. Five outstanding changes across five
+accounts therefore draw as five subjects in one undifferentiated run, and the
+user cannot tell which sync will carry which.
+
+**The data is reachable, which is what makes this S rather than unspecified.**
+`MainWindow::accountForMessagePath()` (`mainwindow.cpp:6219`) already answers
+exactly this question from a path, and is what Delete, Restore and Empty trash
+use. `resolvePendingSubjects()` already crosses to the worker with every id and
+answers positionally, so the account is one more field on a round trip that
+happens anyway rather than a new mechanism.
+
+**Approach.** Add the path (or the resolved account) to what the worker returns
+beside the subject, resolve it through `accountForMessagePath()` on the way
+back, and draw it on the row that opens each run, where the subject already is.
+The positional contract is the thing to be careful with: `onPendingSubjectsResolved()`
+refuses a reply whose lengths disagree, deliberately, and a third list has to
+be checked the same way.
+
+**One asymmetry to settle first.** A held THREAD edit contributes a row keyed on
+a THREAD id, not a message id (`pendingChangeSnapshot()`, `mainwindow.cpp:5699`),
+and thread rows are already drawn differently, carrying a message count where a
+message row carries -1. A thread can in principle hold messages from more than
+one account, so the thread rows need their own answer rather than the message
+one: the account of the thread's first message is a guess, and naming two
+accounts on one row may be the honest output. Decide that before writing the
+resolver, since it changes what the worker has to return.
+
+**Constraints.** The account name is config text, not mail content, so it is
+safe to draw plainly; the subjects beside it are already the untrusted half and
+are already handled. Nothing here touches a write path, so there is no undo
+question.
+
+## 199. The window chrome uses the system icon theme, and the user wants a shipped set
+
+**Observed (user, from the notes):** "we should ship our own icons, color
+themeable to be consistent in every theme a user may implement, since icons are
+a brand identity."
+
+**This reverses item 70 rather than completing it, and that is the point.** Item
+70 answered the same question in 2026-08-11 and drew the line deliberately:
+the PANES are ours, the CHROME is the system's, six SVGs shipped for the marks
+and `QIcon::fromTheme` kept everywhere else. `AGENTS.md` records that split as
+the item's whole purpose. The note asks for the other half, so this is a design
+decision to revisit with the user, not a defect and not an oversight.
+
+**Cause.** Verified. Every chrome icon comes from the desktop theme: the
+`themeIcons` table at `mainwindow.cpp:2211`, the built-in filter icons at
+`mainwindow.cpp:3124`, and six `QIcon::fromTheme` calls in `composewindow.cpp`.
+The consequence the note names is real and already observed in this backlog:
+item 190 found that `bug` resolves in 0 of 24 system themes, so a button drawn
+with it would be blank on most desktops. An icon this project cannot see is an
+icon it cannot design against.
+
+**The mechanism already exists and is proven.** `Marks` (`src/marks.h`) carries
+its payloads as compiled-in string literals generated from
+`assets/icons/marks/*.svg`, which stay the editable originals; `Marks::pixmap`
+composites the real colour over a `fill="currentColor"` render with
+`CompositionMode_SourceIn`, which is exactly the "color themeable" property the
+note asks for, and is why one asset serves a light and a dark palette. The
+reason it is not a `.qrc` is recorded in `src/CMakeLists.txt` and applies here
+unchanged: a qrc in the static library registers itself from a global
+initialiser the linker drops, and the tests link the library.
+
+**So the code is the small half and the ARTWORK is the item.** Item 70 shipped
+six marks; this is on the order of forty actions, each needing a drawing that
+reads at toolbar size and at menu size. That is why the size is M-L and why it
+cannot be narrowed without the user.
+
+**Two decisions from the user before this can be planned.**
+
+1. **Scope.** The toolbar alone is a small set and the most visible one; every
+ action in every menu is the whole forty. The note says "icons" without
+ drawing that line.
+2. **Whether the system theme stays as a fallback** for an action with no
+ shipped icon. Keeping it means the two sets sit side by side during the
+ transition, which is the opposite of the consistency the note asks for;
+ dropping it means an unshipped action has no icon at all until one is drawn.
+ Item 70's own test (`noTwoActionsShareAnIcon()`) and the rule that every
+ action must carry an icon both key on the current table and would need
+ rereading against whichever answer is chosen.
+
+## 200. qtmaildir cannot be launched at a given account, thread or message
+
+**Observed (user, from the notes):** "the program should accept cli parameters
+like `--account` or `--thread`/`--message`, so that another app can launch
+qtmaildir opening that account's inbox or a certain message/thread."
+
+**Cause.** Verified. `main.cpp:38-66` walks `argv` with `std::strcmp` and
+recognises `--version`/`-v` and `--help`/`-h`, nothing else. Both answer and
+return BEFORE `QApplication` is constructed, which is deliberate and documented
+in the file: `--version` has to work on a machine where the GUI cannot open.
+Anything else on the command line is ignored silently.
+
+**Parsing is the small half.** `QCommandLineParser` is stdlib for this and
+replaces the `strcmp` loop, with the one constraint that the early-exit options
+must keep answering without a `QApplication`.
+
+**Two things make this M rather than S, and both are the interesting part.**
+
+1. **There is no single-instance mechanism.** No `QLocalServer` or
+ `QLocalSocket` appears anywhere in `src/`. A second launch therefore opens a
+ second window against the same notmuch database, and notmuch permits only one
+ open handle per process, so two processes is two handles and the read-write
+ burst the write path depends on becomes a contention question. The note's own
+ framing, "another app can launch qtmaildir", is the case where the
+ application is usually ALREADY RUNNING, so the useful behaviour is to steer
+ the running window rather than to start a second one.
+2. **A selector has to reach a query the startup path does not take.**
+ `--account` is close to free, since the account selector and the built-in
+ filters already compose that query and `Config::resolvedQuery()` exists for
+ exactly this. `--thread` and `--message` are not: they name a row that may
+ not be in the configured startup view at all, so the startup path has to
+ accept an arbitrary query and then select a row within its result, which is
+ a selection-after-load problem the window solves nowhere else.
+
+**One decision from the user before this can be planned.** Whether a second
+launch should hand its arguments to the running window and focus it, or simply
+start with a different query. The first is what makes the feature useful to an
+external caller and is essentially the whole cost of the item; the second is
+close to free for `--account` alone. They are different items sharing one line
+in the notes.
+
+**Constraints.** Arguments are untrusted input in the ordinary sense: a
+`--thread` value reaches a notmuch query, so it goes through `SearchTerm`'s
+quoting like every other query this application builds, rather than being
+concatenated at the call site.