aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md99
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md83
2 files changed, 100 insertions, 82 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 84685c6..951b338 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
@@ -10678,3 +10678,102 @@ qtmaildir's own Delete strips `inbox` (item 168).
green with a spam-folder case and a two-file (spam + inbox) case; after the
cleanup, `notmuch search --output=files 'tag:inbox' | grep -i '/spam/'` is
empty.
+
+## 204. No recipient completion, in the composer or the query bar
+
+**Observed (user, 2026-09-18, settling item 72):** "as it is today I don't have
+a way of accepting an invitation, I have to copy-paste the addresses for any new
+email from somewhere."
+
+**Cause (verified in the code).** The composer's recipient fields are plain
+`QLineEdit`s with no completer of any kind, and nothing in `src/` reads a
+contact store: `grep` finds no vcard, vCard or CardDAV anywhere. Completion
+exists in this application only for tags (`QueryCompleter`, `TagDialog`).
+
+**The data is on disk and verified, 2026-09-18.** 117 `.vcf` files under
+`~/.local/share/vdirsyncer/contacts/`, written by the `contacts` pair in
+`~/.config/vdirsyncer/config`. They are vCard **3.0**, one card per file, each
+carrying `UID`, `FN`, `N`, `EMAIL` and frequently a base64 `PHOTO`.
+
+**Two traps in the layout.** The pair writes to
+`~/.local/share/vdirsyncer/contacts/`, NOT `~/.local/share/contacts/`, which is
+an Akonadi directory holding only a README warning against touching it: the
+obvious path reads the wrong store. And the vdirsyncer config is per-machine
+and holds a `password.fetch` command, so the path is a key in `qtmaildir.conf`
+(as `notmuch_config` is) and nothing about that file is read here.
+
+**libical does NOT parse these.** libical 3.0.20 is installed with headers and
+a `libical.pc`, but its vCard support landed in 4.0; `libicalvcal` is the old
+vCalendar-1.0 converter, not a vCard parser. So this is a hand parse, which is
+proportionate for four fields, and the reason to keep it in a namespace of free
+functions over values: it must be testable against fixture files without a
+widget, as `MimeParser` is.
+
+**What the parse must handle, and it is the whole risk.** vCard 3.0 FOLDS long
+lines: a continuation begins with a space or tab and the fold can land anywhere,
+including inside a base64 `PHOTO` and inside an address. Unfolding comes before
+any field split. `EMAIL` carries parameters before the colon
+(`EMAIL;TYPE=INTERNET,PREF:...`), a card may carry SEVERAL, and `N` is
+semicolon-structured with backslash escapes. A card with no `EMAIL` is not an
+error and simply does not complete.
+
+**Completion mechanics are already decided by this repository's own scars.** A
+recipient field holds MORE THAN ONE value, so `QLineEdit::setCompleter()` is
+wrong for exactly the reason `CLAUDE.md` records twice: attach with
+`QCompleter::setWidget()`, drive `setCompletionPrefix()` from `textEdited`, and
+replace the token under the cursor on `activated`. A test must TYPE the keys;
+`setText()` does not drive a completer at all and passes against the bug.
+
+**Constraints.** Read-only: this item writes no vcard (that is 205). A card is
+untrusted input in the ordinary sense, since it arrives from a DAV server, so a
+`FN` reaching a label is plain text like every header value
+(`MessageDetailsDialog`'s rule). Personal data: fixtures are written by hand
+with example.org addresses, never copied from the real store.
+
+**The query bar gets the same candidates**, widened into this item by the user
+on 2026-09-18: "it's nice to be able to search by address from my known
+contacts." `QueryCompleter` completes `from:` and `to:` with NOTHING today, and
+`querycompleter.cpp:652-656` says why: "addresses need an enumerator libnotmuch
+does not expose". The vcards ARE that enumerator, so that comment is half wrong
+the moment the store exists and must be corrected rather than left to mislead.
+
+The seam is already the right shape and needs no new mechanism. `setTags()` is
+fed by the worker's `allTagsReady` and replaces the tag candidates; contacts get
+the same treatment, and `entriesFor()` grows a `from:`/`to:` branch beside the
+`path:` one. Two differences from the composer half, and they are why this is
+not literally the same code: the query bar completes ONE token in notmuch's
+grammar, so it inserts a bare address rather than `Name <addr>`, and the value
+goes through `SearchTerm::quote()` like everything else this application puts in
+a query, since a display name can hold a space and a quote.
+
+**Executed 2026-09-18 to 2026-09-24 via subagent-driven-development**: seven
+signed commits, `3303c78..7454096`. `ContactStore` parses the vCard 3.0
+directory, `[general] contacts_dir` locates it (tilde-expanded), the composer's
+To/Cc/Bcc and the query bar's `from:`/`to:` complete from one shared
+`QCompleter` attached with `setWidget()`, and `MainWindow` loads the store once
+and feeds both consumers. Four controller rulings departed from the plan: a
+`setContacts()` setter instead of a fifth constructor parameter, the accessor
+named `contactsDir()`, flat vCard fixtures, and tilde expansion for
+`contacts_dir`. One genuine plan gap: `splitRecipients()` was made quote-aware,
+since the plan's quoted-display-name insertion would otherwise be cut in half
+by the splitter on the way to `OutgoingMessage`. The final whole-branch review
+found one Important issue, untrusted `FN` control characters and RFC 5322
+specials reaching the recipient header, fixed in 7454096 by stripping control
+characters and always quoting a non-empty name; re-reviewed clean. Hand-tested
+and confirmed working by the user. One residual, not a defect in this work: the
+vCard `EMAIL` value reaches the header unsanitised (an embedded `\r`); GMime
+fails the send closed on it, so it is a one-line `ContactStore` follow-up if it
+is ever hit in practice, not filed as its own item.
+
+**No brainstorm was needed for this item, and the plan was written**:
+`plans/2026-09-18-contact-completion.md`, six tasks. Four decisions were taken
+with the user on 2026-09-18 and were not open questions:
+
+1. The path is a `[general]` key, defaulting to
+ `~/.local/share/vdirsyncer/contacts/`. Empty means the feature is off, with
+ no error: a machine with no vdir is the ordinary case for anyone else.
+2. The store loads once, whole, when it is first needed. 117 files is nothing,
+ and a watcher is speculative until a measurement says otherwise.
+3. A candidate matches on the name AND the address, and the composer inserts
+ `Name <addr>`.
+4. `FN` and every `EMAIL`; `PHOTO`, `ADR` and `TEL` are ignored entirely.
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 30c0031..8334ddf 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
@@ -141,7 +141,7 @@ taking that too literally.
| 70 | Pane icons are a private set where the main window uses the system theme | presentation | M | **done** 2026-08-11; six shipped SVGs |
| 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 | ? | **split 2026-09-18** into 204 (recipient completion), 205 (contact editing), 206 (calendar window) and 207 (invitations), after the user named what they want. Nothing is planned under this number any more; it stays as the index entry the notes' single line maps to. The edge-tts reminder half is out of scope: its own project |
-| 204 | No recipient completion, in the composer or the query bar | workflow | S | open, 2026-09-18, from 72's split and the user's own words: "I have to copy-paste the addresses for any new email from somewhere." Read-only over the 117 synced vcards. **Widened the same day at the user's request** to feed `from:`/`to:` in the query bar, which `QueryCompleter` completes with nothing today. Independent of 205, 206 and 207 and blocks none of them; the user starts here. **No brainstorm needed**, the shape is settled and the plan is written (`plans/2026-09-18-contact-completion.md`, six tasks, not started): see the entry |
+| 204 | No recipient completion, in the composer or the query bar | workflow | S | **done** 2026-09-24, shipped, built via subagent-driven-development, `3303c78..7454096`. `ContactStore` over the vdirsyncer vCard 3.0 directory feeds one shared `QCompleter` for the composer's To/Cc/Bcc and the query bar's `from:`/`to:`. Hand-tested and confirmed. One review fix (control-character/RFC 5322 sanitisation of `FN`) landed and was re-reviewed clean. Section in the closed file |
| 205 | Contacts can be read but not edited from the app | workflow | M | open, 2026-09-18, from 72's split, **asked for by the user** while settling 204. Writing a vdir, so it shares the write-during-sync question with 206 and the fold/escape rules with 204's parser. Needs a brainstorm of its own |
| 206 | No calendar: events cannot be viewed, added or edited | workflow | L | open, 2026-09-18, from 72's split, **asked for by the user** in place of khal. Its own TOP-LEVEL WINDOW and **libical**, both settled by the user. Spec-and-branch scale, not a Tuesday pickup. Blocks 207 |
| 207 | An invitation cannot be accepted, refused or sent | workflow | M | open, 2026-09-18, from 72's split, **asked for by the user**: "as it is today I don't have a way of accepting an invitation." `text/calendar` in the pane, an iMIP reply through the existing `send_command`. **Blocked on 206**, which owns the calendar it writes to |
@@ -1615,87 +1615,6 @@ part-way through `moveMessages()` can leave a message renamed on disk and not
yet reindexed. Treat a reproduction as touching real mail and run it against a
throwaway index if at all possible.
-## 204. No recipient completion, in the composer or the query bar
-
-**Observed (user, 2026-09-18, settling item 72):** "as it is today I don't have
-a way of accepting an invitation, I have to copy-paste the addresses for any new
-email from somewhere."
-
-**Cause (verified in the code).** The composer's recipient fields are plain
-`QLineEdit`s with no completer of any kind, and nothing in `src/` reads a
-contact store: `grep` finds no vcard, vCard or CardDAV anywhere. Completion
-exists in this application only for tags (`QueryCompleter`, `TagDialog`).
-
-**The data is on disk and verified, 2026-09-18.** 117 `.vcf` files under
-`~/.local/share/vdirsyncer/contacts/`, written by the `contacts` pair in
-`~/.config/vdirsyncer/config`. They are vCard **3.0**, one card per file, each
-carrying `UID`, `FN`, `N`, `EMAIL` and frequently a base64 `PHOTO`.
-
-**Two traps in the layout.** The pair writes to
-`~/.local/share/vdirsyncer/contacts/`, NOT `~/.local/share/contacts/`, which is
-an Akonadi directory holding only a README warning against touching it: the
-obvious path reads the wrong store. And the vdirsyncer config is per-machine
-and holds a `password.fetch` command, so the path is a key in `qtmaildir.conf`
-(as `notmuch_config` is) and nothing about that file is read here.
-
-**libical does NOT parse these.** libical 3.0.20 is installed with headers and
-a `libical.pc`, but its vCard support landed in 4.0; `libicalvcal` is the old
-vCalendar-1.0 converter, not a vCard parser. So this is a hand parse, which is
-proportionate for four fields, and the reason to keep it in a namespace of free
-functions over values: it must be testable against fixture files without a
-widget, as `MimeParser` is.
-
-**What the parse must handle, and it is the whole risk.** vCard 3.0 FOLDS long
-lines: a continuation begins with a space or tab and the fold can land anywhere,
-including inside a base64 `PHOTO` and inside an address. Unfolding comes before
-any field split. `EMAIL` carries parameters before the colon
-(`EMAIL;TYPE=INTERNET,PREF:...`), a card may carry SEVERAL, and `N` is
-semicolon-structured with backslash escapes. A card with no `EMAIL` is not an
-error and simply does not complete.
-
-**Completion mechanics are already decided by this repository's own scars.** A
-recipient field holds MORE THAN ONE value, so `QLineEdit::setCompleter()` is
-wrong for exactly the reason `CLAUDE.md` records twice: attach with
-`QCompleter::setWidget()`, drive `setCompletionPrefix()` from `textEdited`, and
-replace the token under the cursor on `activated`. A test must TYPE the keys;
-`setText()` does not drive a completer at all and passes against the bug.
-
-**Constraints.** Read-only: this item writes no vcard (that is 205). A card is
-untrusted input in the ordinary sense, since it arrives from a DAV server, so a
-`FN` reaching a label is plain text like every header value
-(`MessageDetailsDialog`'s rule). Personal data: fixtures are written by hand
-with example.org addresses, never copied from the real store.
-
-**The query bar gets the same candidates**, widened into this item by the user
-on 2026-09-18: "it's nice to be able to search by address from my known
-contacts." `QueryCompleter` completes `from:` and `to:` with NOTHING today, and
-`querycompleter.cpp:652-656` says why: "addresses need an enumerator libnotmuch
-does not expose". The vcards ARE that enumerator, so that comment is half wrong
-the moment the store exists and must be corrected rather than left to mislead.
-
-The seam is already the right shape and needs no new mechanism. `setTags()` is
-fed by the worker's `allTagsReady` and replaces the tag candidates; contacts get
-the same treatment, and `entriesFor()` grows a `from:`/`to:` branch beside the
-`path:` one. Two differences from the composer half, and they are why this is
-not literally the same code: the query bar completes ONE token in notmuch's
-grammar, so it inserts a bare address rather than `Name <addr>`, and the value
-goes through `SearchTerm::quote()` like everything else this application puts in
-a query, since a display name can hold a space and a quote.
-
-**No brainstorm is needed for this item, and the plan is written**:
-`plans/2026-09-18-contact-completion.md`, six tasks. Four decisions were taken
-with the user on 2026-09-18 and are not open questions:
-
-1. The path is a `[general]` key, defaulting to
- `~/.local/share/vdirsyncer/contacts/`. Empty means the feature is off, with
- no error: a machine with no vdir is the ordinary case for anyone else.
-2. The store loads once, whole, when it is first needed. 117 files is nothing,
- and a watcher is speculative until a measurement says otherwise.
-3. A candidate matches on the name AND the address, and the composer inserts
- `Name <addr>`.
-4. `FN` and every `EMAIL`; `PHOTO`, `ADR` and `TEL` are ignored entirely.
-
-
## 205. Contacts can be read but not edited from the app
**Observed (user, 2026-09-18):** "I don't care much for khard, I'd love to be