aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md175
1 files changed, 175 insertions, 0 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 7314700..7a52984 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
@@ -7349,6 +7349,81 @@ importing the other.
---
+## 152. Signatures are not managed at all
+
+**Observed (user, 2026-08-24, from the notes):** listed under "some basic
+functionalities not brainstormed which didn't enter the first Send
+implementation", as:
+
+> signatures:
+> - not tied to an account, with a switch in the editor bar UI.
+
+**Specified 2026-08-24.** The design is in
+`docs/superpowers/specs/2026-08-24-signatures-design.md`. Read that rather
+than this section, which records only what the brainstorm settled and why.
+
+**The constraint is the shape of the item, and it survived a second key.**
+Not tied to an account rules out `[account.*] signature` as the whole answer.
+The user then asked for that key anyway, as a convenience, and it does not
+reopen the constraint: the account supplies a STARTING value, the editor-bar
+switch keeps every signature reachable under any account, and changing From:
+stops re-seeding the moment the user touches the switch. Seeding is not
+binding.
+
+**One choice must serve both forms, and that costs nothing.**
+`MessageBuilder` already derives `text/plain` from `markdownBody` verbatim and
+`text/html` from `MarkdownRenderer::toHtml()` over the same string
+(`messagebuilder.cpp:321-324`). A markdown signature in the buffer therefore
+gets both, correctly, with no change to `MessageBuilder` and no second code
+path. The user's "transparent to the user" requirement is a property the
+pipeline already has.
+
+A two-file variant (`work.md` plus an optional `work.html` overriding the
+rendered form) was chosen and then dropped by the user the same session: it
+buys designed HTML signatures at the cost of the signature no longer being
+visible in the editor, since the two parts diverge and the buffer can hold
+only one of them.
+
+**The switch is stateless, by using the delimiter rather than tracking a
+range.** `seedBody()` deliberately refuses to track "my text" and "the quote"
+as separate pieces (`composewindow.cpp:640-644`), and a signature switch is a
+toggle by definition, so it cannot duck that question the way the quote did.
+It answers it without state: the signature is the last `-- ` block not
+followed by quoted lines, found by scanning. Nothing to desync from the undo
+stack, and it survives editing above it.
+
+**`signature_position` covers both placements over one scan.** The user's own
+habit is `end`, which is the default; `above_quote` exists because other
+clients offer it. The scan needed the quote-aware clause for `above_quote`
+anyway, so the key is roughly ten lines rather than one, and a naive tail rule
+would have eaten the quote under the other placement.
+
+**A delimiter alone must not authorise a deletion.** The block after `-- ` is
+replaced only when its text matches one of the signatures on disk; otherwise
+the new one is inserted and nothing is removed. `-- ` can reach the buffer
+pasted in with quoted text from another client, and the unguarded scan would
+have silently deleted everything after it. The failure is now directional: a
+wrong guess adds a visible duplicate rather than losing the user's writing.
+Raised by the user against the first draft of this design.
+
+Two markers were considered for the same problem and refused. A zero-width
+character SHIPS in the sent message, fingerprinting the client in outgoing
+mail, and must survive the draft round trip through GMime, quoted-printable
+and `MimeParser`, which is exactly what normalises such characters away. A
+doubled delimiter (`--` plus two spaces) is not the RFC 3676 separator, so no
+receiving client would fold or strip the signature, and trailing whitespace is
+unreliable through the same pipeline.
+
+**Two things the design refuses.** No signature editor: the directory is
+edited with the user's own editor, and a text editor inside a mail client is
+not this project's to build. And a resumed draft seeds nothing, because the
+saved body already carries whatever signature it was written with, and
+seeding again would put a second one on a message written once.
+
+**Closed 2026-08-24** (unreleased). See the status table row for the outcome.
+
+---
+
## 153. A draft cannot be opened for editing, so it is write-only
**Observed (user, 2026-08-24).** Found the moment item 138 gave drafts a
@@ -7438,3 +7513,103 @@ bump either way: an ignored optional field is not a breaking change.
**Size: S.** Removing a field, two UI affordances and their tests.
**Closed 2026-08-24** (unreleased). See the status table row for the outcome.
+
+---
+
+## 158. A freshly saved draft is invisible until a sync indexes it
+
+**Observed (user, 2026-08-24, by hand).** Composing a new message or resuming
+a draft, then closing the composer, the draft cannot be found again in the
+Drafts view until a sync runs or the cron job fires. "Refresh the search"
+does not bring it back.
+
+**Cause (verified in the code).** Autosave writes the draft to the Maildir
+drafts folder and stops there: `saveDraftNow()` -> `DraftStore::write()` at
+`composewindow.cpp`, with no indexing step. The Drafts view is a notmuch
+query, `Config::allDraftsQuery()` -> `Account::draftsQuery()` ->
+`path:".../Drafts/**"`, and "refresh" re-runs that query against the existing
+index. Only `assets/mailsync.sh` runs `notmuch new`. So the file exists on
+disk and is invisible to the view.
+
+**Outcome.** `saveDraftNow()` emits `draftSaved(path, previousPath)`;
+`MainWindow::openComposer()` connects it to a new
+`NotmuchWorker::indexDraftFile()`, which indexes the one file the way
+`moveMessages()` does and removes the previous revision so a rewrite leaves no
+ghost. The send path unlinks a draft it had indexed while composing, so
+`draftRemoved(path)` -> `removeIndexedFile()` drops that entry.
+
+**Measured, and it makes the fix smaller than the item guessed.**
+`notmuch_database_index_file` assigns NO tags at all, unlike `notmuch new`,
+which would add `draft inbox unread` from `new.tags` and the `:2,D` flag. So
+the "strip whatever index_file assigns" concern is moot: a draft indexed this
+way cannot leak into a `tag:inbox` or `tag:unread` view, and no stripping is
+needed. The drafts view is path-based, so zero tags is exactly enough.
+
+**Size: S.** One worker slot, one signal, and their tests.
+
+**Closed 2026-08-24** (unreleased). See the status table row for the outcome.
+
+
+## 159. The Drafts view lists threads, so a draft is unreachable by double-click
+
+**Observed (user, 2026-08-25):** "drafts should be treated like \"Sent\",
+listing only actual draft messages and not threads, otherwise I can double
+click on a thread message and nothing happens."
+
+**Cause (verified in code):** `Config::builtinFilter()` in `src/config.cpp`
+sets `filter.flat = true` for the `sent` generator only, at line 1011. The
+`drafts` branch below it leaves the default `false` with a comment stating the
+choice explicitly: "NOT flat, like Trash and unlike Sent: a draft reply
+belongs with the conversation it answers." That was item 138's decision and it
+is the thing the note contradicts.
+
+The consequence the user reports follows from it. A thread row stands for
+`ThreadSummary::firstMessageId`, which in a Drafts view is the first MATCHED
+message of the conversation, and that is not necessarily the draft. Item 153
+gated `edit_draft` on the file living in a configured drafts folder precisely
+so that opening ordinary mail this way cannot make the first autosave delete a
+received message, so the row is inert rather than harmful. Inert is still
+"nothing happens".
+
+**Built 2026-08-25**, after confirming the reversal with the user.
+
+**Not one line, and the reason is the part worth keeping.** The obvious fix is
+`filter.flat = true` in the `drafts` branch. That ships a defect: the view mode
+was decided in THREE places that each hardcoded a comparison against `"sent"`,
+and they have to agree.
+
+- `builtinFilter()` sets it for the button.
+- `loadSavedQueries()` reapplies it on read, so a hand-edited or migrated file
+ cannot produce a threaded Sent view.
+- `saveSavedQueries()` SKIPS writing it when the generator already implies it,
+ because a key carrying no information is one a hand-editor must read past.
+
+Setting only the first does not break the save/load pair, and it is worth being
+exact about why: the writer's skip knew only about `sent`, so it would have
+STORED `"flat": true` for drafts, and the reader would have honoured it. That
+round trip survives by accident.
+
+What does NOT survive is a file that carries no `flat` key: one written by an
+older build, migrated from elsewhere, or hand-edited, which is the case the
+reader's fallback exists for. It comes back THREADED against a flat button, and
+the writer then persists that disagreement on the next save. The reader is the
+load-bearing site, and it is the one a per-branch fix leaves untouched.
+
+`generatorIsFlat()` is the fix: one closed set beside `generatorTag()`, called
+from all three sites. `builtinFilter()` sets `filter.flat` once from it rather
+than inside a branch, so the set cannot drift from the labels below it.
+
+**Trash deliberately did not follow.** A deleted message still belongs to its
+conversation, and nothing in the trash has to be reachable for editing. The
+test asserts this, so a future change that flattens every folder filter fails
+rather than passing quietly.
+
+**Testing.** `theDraftsFilterIsThreadedNotFlat` asserted the old behaviour and
+is inverted rather than deleted, keeping the history in its comment. The
+round-trip is covered by extending `aGeneratedEntryWritesNoRedundantKeys`,
+which already asserted exactly that property for `sent`, rather than by a
+second test that would have restated it. Mutation-checked: reverting
+`generatorIsFlat()` to `sent` alone fails both.
+
+Suite 37 of 38; the failure is `undoMovesTheMessageBack`, item 136,
+pre-existing and on an unrelated path.