aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-08-20-compose-and-send-design.md
AgeCommit message (Collapse)AuthorFilesLines
4 daysfeat(compose): derive a reply's recipients and headers, item 123Danilo M.1-4/+35
ComposeContext, task 7 of the compose-and-send plan. Address parsing, recipient derivation, the References chain, subject prefixing and account resolution, as free functions over values so they test without a painter. Recipient derivation was designed from the spec rather than transcribed: the plan's draft omitted it and its tests could not compile, calling QVERIFY(config.load(path)) against a void return. Six defects found in review, each pinned by a test checked against the mutation that breaks it: - Message-ids reached GMime bare, and GMime writes an EMPTY header for a bare addr-spec rather than complaining. In-Reply-To and References both shipped blank, so every reply would have arrived as an orphan thread with nothing wrong to see locally. MessageBuilder now brackets on write, in the one place that composes those headers rather than in each caller. - internet_address_to_string was called with FALSE for the encode flag, so a display name carrying a raw newline rendered with the newline intact. That is a header-injection primitive. - A reply to the user's own message addressed the user. It now goes to that message's original recipients, mirroring their To/Cc split, which is what the Sent view and a follow-up on unanswered mail need. - A From parsing to no mailbox left To empty, reachable from real mail ("From: Mailer Daemon"). MessageBuilder treats an empty recipient list as success, so the message would have been handed to the send command with nobody to deliver to and filed in Sent looking sent. - The References header was split on whitespace alone, so a client's non-conformant "<a@x>,<b@y>" became one token and the bracket strip produced the fabricated id "a@x>,<b@y". - Reply and forward prefixes were recognised in English only, doubling every AW:, SV:, WG: and Re[2]: a mixed-locale mailbox receives. Single-letter spellings are deliberately excluded: with R: recognised, "R: report on Q3" reads as a prefix and a genuine first reply threads nowhere. The mailbox-only guard in parseAddressHeader survived its first mutation check, because removing it still yields no recipients: the invalid GObject cast makes GMime's own assertion return NULL. That is undefined behaviour papered over by an assertion G_DISABLE_CHECKS compiles out, so the test now asserts on the emitted critical rather than on the count. Registering the log handler on a NULL domain catches nothing; the criticals carry "GLib-GObject" and "gmime". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoaLBowZ6w1JNx6SEhDP1L
6 daysdocs: lay out the send popup, item 123Danilo M.1-5/+40
Three rows in every state, so nothing reflows: status label, bar, right-aligned Undo. The bar changes mode rather than place, determinate and draining during the countdown because that has measurable progress, indeterminate once send_command starts because a send does not. Undo stays visible after it disables. A control that vanishes re-lays out the popup mid-operation, and a greyed one says why cancelling is no longer possible where an absent one looks like it was never offered. The status label sizes from the longest string it can hold in the current language rather than from its content: Italian "Rimozione della bozza..." is longer than "Removing draft...", so a content-sized label resizes the popup between stages, which is the jumping the fixed layout exists to prevent. Item 134 gains a requirement from this: the extracted widget must expose both bar modes, not only the indeterminate one MainWindow happens to need today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDq53rMd3AQp7QmcZzpuBM
6 daysdocs: put sending behind a cancellable delay, item 123Danilo M.1-20/+61
The user asked for Gmail's undo-send, and it answers a question the spec had left open: what Cancel means during a send. It means nothing, if offered while send_command is running. Killing an SMTP client mid-transaction leaves an unknown send, since the message may have reached the server in full before the kill, and that is worse than either clean outcome. Moving the cancel window before the command starts makes Undo mean genuinely nothing happened. The popup owns the whole operation, countdown through completion, rather than a countdown popup handing over to a status bar. One widget changing state in one place, and it keeps the eye-catching surface the user asked for. Modal to the composer only, so a second composer and the main window stay usable. No close button and no Escape: during the countdown a dismissal cannot say whether it means cancel or send now. send_delay_ms defaults to 5000, and zero skips it. The test for this asserts a negative: Undo leaves the stub command never run. A test asserting only that the composer reopened would pass against a design that ran the command and discarded the result, which is exactly what the delay exists to prevent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDq53rMd3AQp7QmcZzpuBM
6 daysdocs: choose warning surfaces by consequence, item 123Danilo M.1-11/+39
Two corrections from the user, both of which the spec had wrong. A failed sent-copy write was put in the main window's status bar, on the reasoning that the composer closes so the message needs somewhere persistent. Wrong instinct: the fix for "the window is gone" is a dialog, not a quieter surface. It is the one failure here that produces a silent divergence between what the recipient received and what the local archive holds, and nobody discovers that from a line that showed for a few seconds. It gets a modal. A failed autosave stays in the composer but as a persistent banner rather than a status-area line, since the quit path already escalates that state to a dialog and depends on it surviving. Stated as a rule at the head of the section, because the user's point was general: modal for silent divergence, banner for mid-task, status bar only for what is already obvious. Second correction: the composer's busy indicator is not built inline. A second instance of MainWindow's progress-bar-plus-label pairing is where a widget class earns itself, and "this codebase builds small UI inline" describes what the code does rather than justifying repeating it. Item 134 extracts it and converts MainWindow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDq53rMd3AQp7QmcZzpuBM
6 daysdocs: specify how a send shows progress, item 123Danilo M.1-2/+35
The spec said "disabled with a spinner" without saying where, which leaves a popup as a reasonable reading of it. A popup is wrong here: it would be modal over a window that is already disabled, and it can be dismissed while the operation continues, which is the indicator ambiguity items 18, 19, 28 and 54 each closed once. Progress goes in the composer's own status bar, through the three stages the operation actually has, since a failure filing the sent copy means something different from a failure sending. The window closing is the success message. The indicator is an indeterminate QProgressBar built inline, matching MainWindow's m_syncProgress rather than factoring out a shared widget: this codebase builds small UI inline, and two progress bars do not justify a third class. Also settles what the staged display implies for a sent-copy write that fails after a successful send: the composer still closes, because holding it open for a message already sent invites sending it twice, and the warning goes to the main window's status bar instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDq53rMd3AQp7QmcZzpuBM
6 daysdocs: specify the composer's formatting toolbar, item 123Danilo M.1-3/+52
The spec said "the editor is plain text" and left it there, which reads as "you are on your own with the syntax". Storage format and editing affordances are separate decisions and only the first was stated. The toolbar is text transformation over the markdown source, not rich-text editing: bold, italic, code, strikethrough, link and quote, selection-aware, with the cursor landing between the tokens when there is no selection. Its shortcuts belong to the composer window's own scope and do not touch KeyMap, which matters for item 132: the two namespaces should not be conflated when that rule is revisited. Live syntax highlighting is a follow-up (item 133) rather than part of this: agreeing with the grammar about nesting and about code spans is the expensive half, and it is better judged after living with the toolbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDq53rMd3AQp7QmcZzpuBM
6 daysdocs: record that cmark-gfm is stock Slackware, item 123Danilo M.1-0/+8
The spec called it a new dependency needing a SlackBuild REQUIRES entry. It is a new dependency, but /var/log/packages/ shows cmark-gfm-0.29.0.gfm.13-x86_64-3 with no _danix tag, so it is stock and REQUIRES lists only non-stock dependencies. Also records the staleness cost accepted with it: cmark-gfm tracks an older CommonMark base (0.29 era) than the stock plain cmark (0.31.2). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDq53rMd3AQp7QmcZzpuBM
6 daysdocs: specify compose and send, item 123Danilo M.1-0/+537
Brainstormed with the user. Design only, no code, which is what the item's #plan-only tag asked for. The decision that shaped everything: there is no MTA on the machine, so "an external script on the same model as mailsync.sh" had no model to copy. Send becomes a per-account send_command taking the message on stdin, exactly as [sync] command already works, which keeps the no-network-protocol rule intact without naming an MTA. An account with no send_command is receive-only by construction, which is how one of the five accounts is meant to work. Reply, reply-all and forward are disabled on its mail behind a ribbon that says why. The body is markdown parsed by cmark-gfm rather than a hand-written parser for a limited set: the two share no code, so the small one is deleted wholesale the moment the set widens. Four new units, three of them widget-free and testable without a painter. MessageSender is deliberately a separate unit rather than a method on the composer, so a future outbox wraps the funnel instead of reworking it. Item 123's section is replaced by a pointer to the spec, per this document's own rule for a fully specified item. The brainstorm opened items 128 to 132, including a review of the every-action-has-a-shortcut rule, which the user raised: six more actions takes it past the point where a chord for everything is useful. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDq53rMd3AQp7QmcZzpuBM