aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-24 12:41:46 +0200
committerDanilo M. <danix@danix.xyz>2026-08-24 12:41:46 +0200
commit58f13ad9d78a07aab1d683462834a2493078744d (patch)
treebd2273b161b214b986c11a8d00d3e20138c9f47b /docs/superpowers
parentb0d612c8ea232674ac0734b1121cfd0bb50d532b (diff)
downloadqtmaildir-58f13ad9d78a07aab1d683462834a2493078744d.tar.gz
qtmaildir-58f13ad9d78a07aab1d683462834a2493078744d.zip
feat(compose): open a draft to finish it
Item 153. DraftStore had a write() and no reader, and nothing opened a composer from an existing message, so a draft rendered like ordinary mail and could never be finished or sent. ComposeContextBuilder::forDraft() reads one back. A new Kind::Draft seeds every field verbatim: the subject takes no Re:/Fwd: prefix, and the body goes in exactly as it was left, with none of seedBody()'s quote framing. It is reachable by double-click and by an edit_draft action in the Message menu. Three things the shape of this depends on. A resumed draft must OWN its file. Maildir has no in-place edit, so an autosave writes a new file and unlinks the old one; a composer that did not know its own path would leave the original behind and one message would become two. ComposeContext::draftPath carries it into m_draftPath, which the autosave already knew how to replace. MimeParser had no bcc, and nothing had ever needed one. MessageBuilder writes Bcc into the draft file deliberately and explains why, so a resumed draft that ignored it would drop every blind recipient from the message the user then finishes and sends, reporting nothing. edit_draft is gated on the file being inside a configured drafts folder, matched on the PATH. A `draft` tag is not enough: notmuch surfaces the Maildir D flag as one, and a message flagged by another client sits in the inbox. Offered on ordinary mail, the composer would own a file it did not write and the first autosave would delete a received message. And a live defect found on the way, which is most of why this took as long as it did. updateComposeActions() ran only from onSelectionChanged. Both signals fire for an ordinary click, so nothing had noticed; but running a query and setting the current index emits currentRowChanged ALONE, so the enablement was computed against the previously selected row. Edit draft stayed disabled on a draft selected that way, and the reply family had the same blind spot with no test that could see it. Now connected to both. Reading currentRowChanged is safe here for the reason CLAUDE.md gives: it answers "which row is current", and no count is read. WorkerBackedWindow::AccountSpec gains a drafts field, which the two new tests need and which no fixture could express before.
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md44
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md2
2 files changed, 45 insertions, 1 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 133a761..0f7181b 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
@@ -7348,3 +7348,47 @@ what a drafts folder is; they are separate readers and neither should start
importing the other.
---
+
+## 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
+button: "Double clicking on a draft should open the message in the editor
+window", and "there's no edit action anywhere, a draft is useless as is".
+
+**Cause (verified in code).** `DraftStore` had a `write()` and no reader, and
+nothing anywhere opened a composer from an existing message. A draft therefore
+rendered in the message pane like ordinary mail and could never be finished.
+
+**Outcome.** `ComposeContextBuilder::forDraft()`, a `Kind::Draft` that seeds
+every field verbatim, `edit_draft` in the Message menu, and double-click.
+
+**Three things worth keeping.**
+
+**A resumed draft must own its file.** Maildir has no in-place edit, so an
+autosave writes a new file and unlinks the old; a composer that did not know
+its own path would leave the original behind and one message would become two.
+`ComposeContext::draftPath` carries it into `m_draftPath`, which the autosave
+already knew how to replace.
+
+**`MimeParser` had no `bcc` and nothing had ever needed one.**
+`MessageBuilder` writes Bcc into the draft file deliberately and says why, so a
+resumed draft that ignored it would drop every blind recipient from the message
+the user then finishes and sends, silently. That is the failure this item was
+most likely to ship.
+
+**The gate is not cosmetic.** `edit_draft` is offered only on a file inside a
+configured drafts folder, matched on the PATH: a `draft` tag is not enough,
+since notmuch surfaces the Maildir D flag as one and a message flagged by
+another client sits in the inbox. Offered on ordinary mail, the composer would
+own a file it did not write and the first autosave would DELETE a received
+message.
+
+**And a live defect found on the way, which is the reason this took as long as
+it did.** `updateComposeActions()` ran only from `onSelectionChanged`. Both
+signals fire for an ordinary click, so nothing had ever noticed; but running a
+query and setting the current index emits `currentRowChanged` ALONE, so the
+enablement was computed against the previously selected row. Measured: Edit
+draft stayed disabled on a draft selected that way, and the reply family had
+the same blind spot without a test that could see it. It is now connected to
+both signals. Reading `currentRowChanged` is safe here for the reason
+`CLAUDE.md` gives: it answers "which row is current", and no count is read.
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 79320bd..e203856 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
@@ -222,7 +222,7 @@ taking that too literally.
| 150 | The receive-only ribbon stays up after the message that raised it is gone | defect | S | **done** 2026-08-24, unreleased. One line in `MessageView::clear()`, beside the blocked-content bar, the stale notice and the attachment bar it already reset by hand. Only `setReceiveOnlyAccount()` hid the ribbon, which every SELECTION change reaches, so a row-to-row move was never the reproducer: it survived the FOUR routes that blank the pane without one (`clear_pane`, `clear_selection`, a new query, a multi-row selection). The first test written for it passed against the defect for exactly that reason |
| 151 | The message-pane bars blend into the UI and carry no severity | presentation | S | **done** 2026-08-24, unreleased. Two severities as the user asked: yellow for a warning that only explains (the receive-only ribbon), blue for one offering an action (remote content blocked, stale thread), each with its own light and dark set read off `QPalette::Base` as `HtmlBuilder` does. The blocked row had to become a WIDGET first: it was a bare `QHBoxLayout`, which has nothing to paint a ground on, and its six `hide()` sites then had to move to the wrapper or a painted empty strip would show. Both action bars put the button right of a stretch |
| 152 | Signatures are not managed at all | v2 | ? | open, 2026-08-24, from the notes, and the user added a constraint the same day: a signature is **not tied to an account**, and is switched from a control in the composer's editor bar. That rules out the obvious `[account.*] signature` key as the whole answer. Still unspecified in the rest: where the text is stored, how it interacts with the quote, and whether the HTML part gets its own form |
-| 153 | A draft cannot be opened for editing, so it is write-only | defect | M | open, 2026-08-24, from the notes, found by the user the moment item 138 gave drafts a button. Verified: `DraftStore` has a `write()` and no reader, nothing anywhere calls anything like `loadDraft`, and no action opens a composer from an existing message. A draft therefore renders in the message pane like ordinary mail and can never be finished or sent. `ComposeContextBuilder` already parses a file into recipients and a body for Reply and Forward, so the parsing exists; what is missing is a context KIND that owns the original (a resumed draft must replace its file on save, not accumulate a second one) and a way in. **The two halves of the note are one item:** double-click on a draft, and an Edit action for every other route |
+| 153 | A draft cannot be opened for editing, so it is write-only | defect | M | **done** 2026-08-24, unreleased. `ComposeContextBuilder::forDraft()` reads a draft back into a context; a new `Kind::Draft` seeds the fields verbatim, takes the body with no quote framing, and carries `draftPath` so the autosave REPLACES the file instead of leaving a second copy. `MimeParser` gained `bcc`, which nothing read before: `MessageBuilder` writes Bcc into the draft deliberately, so a resumed draft that ignored it would silently drop every blind recipient. Reachable by double-click and by an `edit_draft` action, gated on the file being in a configured drafts folder because opening ordinary mail this way would make the first autosave DELETE a received message. Found a live defect on the way, see the section |
| 154 | No read confirmation | v2 | ? | open, 2026-08-24, from the notes. `Disposition-Notification-To`, which is a header `MessageBuilder` would add and a request the message pane would have to honour or ignore on the receiving side. Unspecified: whether this is send-side only, and what the reader is asked |
| 155 | No urgency switch on an outgoing message | v2 | S | open, 2026-08-24, from the notes: low, regular, high. `X-Priority` and `Importance`, headers `MessageBuilder` adds; regular writes neither. A control in the composer, and the same question item 144 answered for the HTML toggle applies to where it sits |
| 156 | No delivery confirmation | v2 | ? | open, 2026-08-24, from the notes. Distinct from 154: this is a DSN (`Return-Receipt-To`, or the ESMTP NOTIFY parameter), which is the sending server's to honour rather than the reader's client. Whether it can be requested at all depends on the `send_command`, so this may not be this application's to offer |