aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs
AgeCommit message (Collapse)AuthorFilesLines
14 daysdocs(backlog): specify item 44 as a shared tagging-rule storeDanilo M.1-0/+341
Item 44 sat as "open, unspecified" because nothing in this application applies rules at sync time, and the item could not be planned until it was known whether such rules existed anywhere. They do: the notmuch post-new hook holds hand-written `notmuch tag` lines scoped to tag:new, carrying their reasoning in shell comments. The design moves them to a tool-neutral JSON store that both qtmaildir and mailctl read, with unknown fields preserved across a write by either tool so neither owns the format. A rule carries no scope, so the same rule serves the hook, a dry run and a future backfill. Also in this pass: - Item 61's cause is established, not open. It is the user's cron sync holding the mbsync lock: 0 failures in 30 runs with no lock held, 30 in 30 with one held. The fix is item 38's existing seam applied across the suite. The document still said "not established" and proposed a load hypothesis that had already failed to reproduce. - Item 74 records the first-start latency measured this session. The delay is the notmuch index paging in from disk, 5714 ms cold against 154 ms warm for the same 4444-thread query, and is not addressable here. What it did expose is a real defect: the status bar holds "Searching..." for the whole walk while rows are already arriving.
2026-08-11feat(sent): add a Sent view, flat and by recipientDanilo M.1-1/+61
Adds a `sent` key to [account.*] naming that account's sent folder, and a Sent button beside the saved queries that composes its query from every account carrying one. An account without the key is omitted silently, as a real account may keep no sent mail locally. With no account selected the button spans all of them; selecting one narrows it through the existing scope wrap rather than a second path. Composed at run time rather than shipped as a [queries] entry. A saved query is one fixed string: it cannot narrow to the selected account, and it goes stale the moment an account is added or a provider renames a folder. The design and the measurements behind it are in docs/superpowers/specs/2026-08-11-sent-mail-design.md. Three things there are worth repeating here. The composed path is QUOTED, and that is load-bearing. A real provider nests its sent folder under a bracketed parent, and "[" and "]" are Xapian syntax: unquoted, the query parses rather than matches and returns nothing while looking entirely plausible. Composition happens in one place so there is one chance to get it right, and a bracketed path is pinned in a test. Recipients are opt-in per query, which is a performance contract rather than a preference. notmuch_message_get_header(m, "To") is not served from the index, it reads the message file: folding every thread of a 4411-thread inbox took 38.2 seconds against 251 ms for the 601-thread sent view. The worker skips the walk entirely unless asked, and the refresh path carries the same flag so a background sync cannot blank the column mid-read. Always folding is mutation-tested: the data would be right and only the cost wrong, which nothing else here would notice. The messages reached through the thread are owned by it and freed with it, so recipientsOf() holds them raw and finishes while the thread is alive, exactly as walkReplies does. An NmMessage wrapper there is a double-free. Sent mail is presented flat, and the pane follows. A message you sent otherwise drags in the replies you received, so a view labelled Sent shows conversations rather than what you sent. ThreadListModel::setFlatMode() makes hasChildren() and ReplyCountRole answer differently and changes nothing else; runQuery() sets it on EVERY run, so any other query restores the tree on its way through and the flag cannot outlive the button that set it. The pane needed its own fix for the same reason: the single-message path depends on a field only filled when a thread is expanded, which never happens in a flat list, so loadThread() gained matchedOnly and drops the messages that did not match instead of rendering them as stubs. Recipients replace the sender through the existing SendersRole rather than a new one, so the delegate needs no branch and cannot disagree with the model about which name a row shows. It falls back to the sender when a To header is absent or unparseable, since a blank where a name belongs reads as a rendering fault. Address parsing uses GMime: a display name may contain a comma, so "Rossi, Mario" <m@example.org>, info@example.net is two addresses and splitting reports three. internet_address_list_parse returns NULL for an empty string, which is a crash if unguarded. Backlog item 63.
2026-08-11docs: specify item 63, sent mail, in its own design fileDanilo M.1-0/+180
The backlog entry could not be planned because it turned on a fact about the user's mail rather than a design choice. Measured against the real database: no `sent` tag exists at all, so the XS branch is dead. Every account keeps sent mail in a folder, and the folders disagree across three shapes, with one account having no sent folder whatsoever. That rules out a `<maildir>/Sent` convention and forces a per-account key. Decisions recorded: a per-account `sent` key composed at run time rather than a shipped [queries] entry, which cannot narrow to the selected account and goes stale when an account is added; an account without the key omitted silently, as `drafts` already is; all-accounts combines and one account scopes through the existing wrap; and recipients on the card in a Sent view, since the sender is the user on every row. Three constraints came from probing rather than memory. A bracketed provider path contains `[` and `]`, which are Xapian syntax, so quoting is load-bearing and the composed query returns its real count only because each path is quoted. notmuch has get_authors and no recipients call at any level, so the To summary must be folded per message in the worker, under the same thread-ownership rule as walkReplies. And GMime's internet_address_list_parse returns NULL for an empty string, which is a crash if unguarded; it also parses a comma inside a quoted display name correctly, so comma-splitting is out. Size revised M, up from S: the recipients half is a new ThreadSummary field, a worker-side per-message walk, the first GMime address parsing in this codebase, and a card that has to know which view it is in. The spec lives in its own file rather than inline. The backlog is past four thousand lines and its job is to say what is open and how big, which a hundred lines of design buries. The entry keeps the finding and the size and points at the spec, following item 53. "Adding to this document" now states the split rule so the next specified item does not land inline again. Account keys and folder names in the spec are placeholders. The counts and the folder shapes are the real measurement and are what the design turns on. Backlog item 63.
2026-08-09docs: make the account colour the card's accent, not a chipDanilo M.1-3/+51
The plan left the account chip unspecified, because where it sits on a card was never decided and inventing a place would have been a guess. The user's answer replaces it rather than placing it: a coloured bar down the card's left edge, the reply spines inheriting that colour, and matching swatches in the account dropdown. It is a net simplification. The chip ate a third of line 2 on every card to repeat a name the user already knows, which is the table-of-records texture item 53 is about; the bar says the same thing in a few pixels and leaves line 2 to the subject. Three details that are easy to get wrong and are specified rather than left to the implementer: - The raw account colour is never drawn as a line. It is chosen to be a chip's FILL, with text drawn on top in whatever stays legible against it. A thin line on the pane's own background has a different job: followable down a long expansion without competing with the senders beside it. The accent blends toward QPalette::Base by the same 0.35 weight threadLineColour() already uses, keeping the hue and dropping the shout. The dropdown swatch does use the raw colour, being a filled patch rather than a line. - A reply resolves its THREAD's colour by walking to the root. AccountColourRole is empty on a message row, so a spine reading its own index would fall back to the neutral line under an accented root and break the continuous edge the design is built on. - Reply cards carry no bar of their own. Two vertical lines a few pixels apart in one gutter is what option B looked like, and the spine already carries the accent. colourFor() never failing is kept deliberately: an account with no colour= key gets a stable colour derived from its tag name, so adding an account and forgetting to colour it degrades to something usable rather than to nothing. kAccentWidth ships at 3px as a starting value. Whether five accounts are tellable apart at that width, on this user's screen and theme, is not decidable from a mockup or a test, so Task 10 gains a step that settles it against real cards, in both themes, with the guidance to widen the bar before touching the user's own colour choices.
2026-08-09docs: close the card list's open questions, and record a navigation defectDanilo M.1-3/+75
Both items the spec left open are settled, and one of them turned out to be a defect rather than the addition it was filed as. Item 20 deferred "move between messages without returning to the list" as an addition on top. It is a repair: next_thread and prev_thread are selectRow(current.row() + 1), and a tree numbers rows per parent, so from the last reply of an expanded thread row+1 names a sibling that does not exist and the action silently does nothing. rowCount() with no argument counts top-level threads, compounding it. Recorded as item 60, since it exists on the branch whether or not the card list is built, and it is a fresh instance of the exact rule that branch's own commit message states. The user asked for arrow keys to skip within a thread and Shift+arrows to skip between threads, scoped so the keys still work normally in the message pane, menus and entry bars. Two findings changed the shape of that: - Up/Down need no binding and no code. QTreeView's built-in navigation walks VISIBLE rows, so it already steps into an expanded thread's replies, and being the view's own key handling rather than a shortcut it is inert whenever focus is elsewhere. The requested focus behaviour is automatic. - Arrow keys must never become keymap actions. Every action is a QAction with WindowShortcut, dispatched before the focused widget sees the key, and Qt withholds only plain LETTERS from editable widgets. Return already proved this by breaking the query bar and needing a per-widget ShortcutOverride filter scoped to one widget and one key. Up as a window shortcut would break the query bar, the tag dialog and the web view at once. Shift+Up/Down was rejected for thread-jumping: it is the built-in extend-selection that multi-row tagging and item 20's action scope depend on. Alt+Up/Down instead, verified free across the whole keymap. The way back to a whole thread after clicking a reply is the root card, which is always visible above its replies. Escape is deliberately not overloaded, since it already means clear-selection with clear-pane on Shift+Escape, and a third meaning is the half-an-action problem item 50 exists to fix. The remaining open item is only whether the message pane should change to match, which stays out of scope: this spec is the left pane.
2026-08-09docs: specify the thread pane as a card listDanilo M.1-0/+229
Item 53 recorded that message rows read as a table and left the approach unspecified, with four directions ranging from spanning columns on reply rows to abandoning message rows entirely. The user's answer is wider than all four: the column grid is wrong for the WHOLE left pane, threads included. Threads and replies both become cards in a single column, three lines each, at one uniform height. Sender and date, then the subject with the flag, attachment and reply-count marks inline, then the tag chips. Replies indent by depth with a continuous spine, capped at depth 4. Three decisions worth their reasoning, since each closed an option that looked cheaper: - Uniform height keeps setUniformRowHeights(true), which is the single cheapest property of the design. A blank third line under untagged cards buys constant sizeHint arithmetic everywhere else. - Uncapped indent with a horizontally scrollable pane was asked for and rejected: it reopens item 51 in a worse form. Cards are viewport width, so the pane has no horizontal scroll range at all, and item 51 is resolved for free rather than fought. - A reply's line 3 shows only the tags its thread does not have. The full per-message set was rejected on measurement, not taste: of 48691 messages in the user's database, 7 carry unread and 75 carry flagged, and both are already shown as the sender's weight and the mark on line 2. Everything else is applied per thread and identical on every message in it, so full sets would render blank on nearly every reply and identical chips on the rest. The design is a net removal. ThreadListView::paintEvent, the tag strip's band arithmetic, SubjectDelegate, RowStyleDelegate, the five Column enumerators and the decorative header all go; one CardDelegate paints the whole card. That retires the two bug classes CLAUDE.md documents for the strip, a deleted row cut in half and every other row showing a bare stripe, both of which existed because the strip spanned cells it did not own. The column header was decorative, so a sort dropdown adds a feature rather than replacing one. Two entries only, newest and oldest, passed to notmuch. Sorting by sender or subject would have to happen in the model after results arrive, which fights the batching that makes a 10k-thread query paint immediately. Item 51 is marked resolved by 53 rather than left as separate work.
2026-08-04docs: scrub account names from comments, tests and fixturesDanilo M.1-2/+2
Real Maildir account keys had reached comments and test data: provider-and-mailbox names across three source files, one of them carrying a surname, plus a real address used as example data in the notmuch test fixture and the design spec. The user's standing rule is that maildir and account names never reach a commit, and this is about to become a public repository, which is what makes it consequential rather than untidy. Replaced with generic keys that carry the same shape, since the length is the point in every one of these comments: a 33-character account tag is why the chip label exists and why the tag column was removed. The measurements stay. They are the evidence behind those decisions and are not personal details. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04docs: add the query completion implementation planDanilo M.1-3/+3
Twelve tasks, TDD throughout, tokenizer first: the parsing rules are where the defects will be and they need neither a widget nor a database to test. CompletionEntry gets its own header. Config needs it and QueryCompleter needs Config, so declaring it in querycompleter.h would make the two headers include each other. Also corrects the spec: NmTags already exists in nmraii.h, it does not need adding.
2026-08-04docs: make the mimetype completion list user-extensibleDanilo M.1-3/+44
Mimetypes are the one completion list with no enumerator and an open-ended set, so [completion] extra_mimetypes appends to the built-ins. Appending rather than replacing means a typo or a short list cannot leave completion worse off than the defaults. Entries are comma-separated with an optional description after '|'. The two separators differ because QSettings splits comma lists itself, so a description containing a comma would otherwise be torn into two entries. The other lists stay fixed: prefixes come from notmuch, paths from the configured accounts, dates are closed once symbolic and relative forms are covered, and tags are read from the database. A user-editable copy of any of them would only drift from its source. For prefixes specifically, the upgrade path is deriving the list from the installed notmuch, not making it editable.
2026-08-04docs: spec query bar completion, require translatable stringsDanilo M.1-0/+240
Design for backlog item 17. Completion covers query prefixes, tag values, date values, path values and mimetype values, each carrying a description so the bar documents the query language while it is typed. A new QueryCompleter class owns it, with the cursor-context tokenizer as a pure function so the parsing rules are testable without a widget or a database. NotmuchWorker gains an all-tags call, which did not exist. Addresses for from:/to: are out of scope: libnotmuch exposes no all-addresses call. Both prefixes still appear so the vocabulary reads complete. Account maildirs belong to path:, not folder:. Account::scopedQuery builds path:"<maildir>/**", and folder: is a different matcher in notmuch, against the folder name rather than the directory path. Also records the translatable-strings rule in CLAUDE.md. Existing code is not yet audited against it.
2026-08-04docs: remove personal details from the verification recordDanilo M.1-4/+4
The manual verification notes named the real threads used to test against a live mailbox. Most of it was incidental, but one was not: the automated sender identified in items 12, 13 and 16-18 is a healthcare provider's patient-notification service, and naming it disclosed a medical relationship. Also removed a correspondent's username, a locatable GitHub thread id, a real maildir name in the spec's example config, and absolute home paths. Every finding is restated generically and none lost substance: "a message with 3 inline cid: parts" carries the same technical weight as naming the sender did. Counts and timings are kept deliberately, since those are the evidence behind the claims rather than anything identifying. Commit history and the v0.1.0 tag were checked and were already clean, so no history rewrite is needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04feat: add MailSync process wrapperDanilo M.1-2/+2
Runs the configured sync script through QProcess, merging stdout and stderr into one log so a failing mbsync run has something to show. The script is never run through a shell: the command is a config value, and splitCommand keeps its arguments literal. Two fixes against the drafted version: - start() no longer calls waitForStarted(). It blocked the UI thread for up to five seconds, which contradicts the spec's requirement that the UI stay usable during sync, and it swallowed launch failures into a bare false return. A missing script now surfaces asynchronously through errorOccurred as finished(false, -1) with an explanatory log line, so the spinner cannot hang with nothing to explain it. - Removed a double-emit guard I had added on the assumption that QProcess follows errorOccurred(FailedToStart) with finished(). Verified it does not: FailedToStart is emitted instead of finished, never before it. The guard was dead state and the comment justifying it was wrong. Also corrects the sync interval throughout: the user's cron runs every 10 minutes, not hourly. The shorter interval strengthens the flock rationale rather than weakening it, since collisions with a manual sync are that much more likely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04docs: unit-test NotmuchWorker against a throwaway databaseDanilo M.1-3/+7
The spec deferred NotmuchWorker to manual verification on the grounds that testing it needs a real notmuch database. Building a fake one in a temporary directory answers that objection, so Task 8 now gets real coverage. This is the only code in the project that writes to a notmuch index, so a bug there corrupts real mail state. It was the wrong place to have the weakest verification. Task 13's manual checklist stays, as confirmation against real data rather than as the only coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04docs: scope the document-load exemption to an exact URLDanilo M.1-0/+9
The interceptor previously trusted the whole qtmaildir: scheme, so a hostile message body referencing qtmaildir://anything would have been allowed. That made the interceptor's correctness depend on the scheme handler in a different, later task. It now trusts only the exact base URL passed to setHtml() and fails closed when none is set. Records the resulting obligation on MessageView to call setDocumentUrl() with that same URL. Also corrects the attachment path guard to a separator-aware comparison and notes that it is unreachable defence-in-depth: safeFilename() runs first, so no caller-supplied name can reach it. A test driving saveTo() expecting refusal cannot pass; test safeFilename() instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04feat: add Config with account, query, and sync parsingDanilo M.1-2/+2
Accounts use [account.work] rather than [account/work]: QSettings' INI backend treats "/" as its own hierarchical group separator, so a literal slash in a section header parses as a nested group and trips QSettings::FormatError, silently breaking childGroups() enumeration. A dot carries no such meaning and keeps the format flat. Saved-query order is alphabetical (QSettings::childKeys() sorts), not file order; documented in code and tests rather than left to a false assumption.
2026-08-04docs: fold full thread rendering and batch tagging into spec and planDanilo M.1-3/+26
Both were narrowed in the first draft and are now in scope. Newsletter threads make whole-thread rendering fundamental, and bulk archive is the main thing a GUI buys over neomutt. Two consequences fell out of thread rendering and are recorded in both documents. The thread renders as one document in one web view rather than one view per message, since a view each would spawn a Chromium render process each. Sharing a document makes cid: references collide when two messages use the same Content-ID, which is common across newsletters from one sender, so every reference is namespaced to cid:<prefix>!<id>. Deciding which messages render expanded needs the query, so loadThread now intersects it with the thread and MessageRef carries a matched flag. Batch tagging resolves thread ids to message ids on the worker in one combined query. Undo stores thread ids and re-resolves, staying correct after the selection moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04docs: add qtmaildir v1 design specDanilo M.1-0/+430
Qt6/notmuch mail client, GUI counterpart to neomutt. Read and organize only in v1; compose and send deferred to v2. Key decisions and their reasoning are recorded in the spec: C++/Qt6 over PySide6, QtWebEngine over QTextDocument for HTML mail, GMime for parsing, direct libnotmuch rather than shelling out to mailctl, and calling the existing sync script rather than reimplementing it so qtmaildir stays inside the flock guard shared with the hourly cron sync. Dependency availability verified on the development machine: Qt6 6.11.1 (WebEngine included in the monolithic package), libnotmuch 0.39, GMime 3.2.15. Notably notmuch installs no notmuch.pc, so CMake must locate it without pkg-config. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>