summaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-20 12:49:57 +0200
committerDanilo M. <danix@danix.xyz>2026-08-20 12:49:57 +0200
commit02d27c9eb963b3357c8f9aa113fbb3eb14f9e7e7 (patch)
treee95dc0ce7422e7e0cfff49199a1189d42728154e /docs
parentdca7818517b4444cac5a7c27feb9ff8791e96e8f (diff)
downloadqtmaildir-02d27c9eb963b3357c8f9aa113fbb3eb14f9e7e7.tar.gz
qtmaildir-02d27c9eb963b3357c8f9aa113fbb3eb14f9e7e7.zip
docs: put sending behind a cancellable delay, item 123
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
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/specs/2026-08-20-compose-and-send-design.md81
1 files changed, 61 insertions, 20 deletions
diff --git a/docs/superpowers/specs/2026-08-20-compose-and-send-design.md b/docs/superpowers/specs/2026-08-20-compose-and-send-design.md
index af13c2f..f410d00 100644
--- a/docs/superpowers/specs/2026-08-20-compose-and-send-design.md
+++ b/docs/superpowers/specs/2026-08-20-compose-and-send-design.md
@@ -256,29 +256,59 @@ The include-order rule applies to every new file that touches GMime: gmime
headers before any Qt header in the same translation unit, because glib
declares a struct field named `signals`.
-### Sending blocks, visibly, and does not queue
+### Sending runs behind a cancellable delay, and does not queue
-Send disables the composer and shows progress **in the composer's own status
-bar**, not in a popup. A modal over a window that is already disabled adds a
-second thing to look at and a dialog that can be dismissed while the operation
-continues, which is the ambiguity items 18, 19, 28 and 54 each closed once.
-
-The composer is the indicator: disabled, showing a stage, and closing itself
-when the whole operation succeeded. Success needs no message, because the window
-closing is the message.
-
-The stages are shown as they happen, since the operation is genuinely several
-and a failure in the second or third means something different from a failure in
-the first:
+**Send opens a popup that owns the whole operation**, from a cancellable
+countdown through to completion. It is modelled on Gmail's undo-send, and it is
+what settles what "cancel" can mean here.
```
-Sending... send_command is running
-Filing sent copy... DraftStore writing to the account's sent folder
-Removing draft... the draft revision is unlinked
+Sending in 5... [ Undo ] countdown running, Undo live
+Sending... [ Undo ] send_command running, Undo disabled
+Filing sent copy... DraftStore writing to the sent folder
+Removing draft... the draft revision is unlinked
+ popup and composer both close
```
-The indicator is an **indeterminate `QProgressBar`** (`setRange(0, 0)`) beside a
-status label, which is what `MainWindow` already does for the sync indicator
+**The delay is where cancelling is safe, and it is the only place it is.**
+Nothing has reached a server during the countdown, so Undo means genuinely
+nothing happened. Killing `send_command` once it is running does not: the
+message may have been handed to the server in full before the kill, so the
+result is an *unknown* send, which is worse than either clean outcome. Undo
+therefore disables itself the moment the command starts, and there is no cancel
+after that.
+
+This is what the delay buys in practice, and it is the case the user named:
+pressing Send and immediately noticing the missing attachment. Undo returns the
+composer exactly as it was, editable, popup gone, nothing sent.
+
+**In a popup rather than the composer's status bar.** A countdown nobody notices
+is a countdown that does not work, and the objection to status bars is precisely
+that they do not catch the eye. The same reasoning that puts a failed sent copy
+in a modal puts the undo window in one.
+
+**The popup owns the progress too**, rather than handing over to the status bar
+once the countdown elapses: one widget changing state in one place, instead of a
+popup vanishing and something else appearing somewhere else.
+
+Two properties it must have:
+
+- **Modal to the composer, not to the application.** Sending from one composer
+ must not freeze a second composer or the main window.
+- **No close button and no Escape dismiss.** During the countdown a dismissal is
+ ambiguous, since it could mean cancel or send now. During the send there is
+ nothing to dismiss. Undo is the only control and it disables itself.
+
+The composer's inputs are disabled for the whole operation, countdown included:
+body, recipient fields, subject, attachment controls, formatting toolbar and
+Send. The message must not change between the user pressing Send and the bytes
+being built. Undo re-enables all of it.
+
+`[compose] send_delay_ms` sets the countdown, default 5000. **Zero skips it**
+and sends at once, for anyone who finds it irritating.
+
+The progress element is an **indeterminate `QProgressBar`** (`setRange(0, 0)`)
+beside a label, which is what `MainWindow` already does for the sync indicator
(`m_syncProgress`) and for the same reason: neither operation has measurable
progress.
@@ -290,8 +320,8 @@ rather than justifying repeating it. Whether 134 lands before or after this
work, the composer uses the shared widget: if it has not happened yet, this work
creates the class and converts `MainWindow` as part of the same change.
-Exit 0 closes the window. Non-zero re-enables it with everything intact and
-shows the command's stderr.
+Exit 0 closes the popup and the composer. Non-zero closes the popup and
+re-enables the composer with everything intact, showing the command's stderr.
**There is no outbox in this design**, and the reason it is not simply
"deferred" is that it needs its own indicator story. This project has four
@@ -490,6 +520,7 @@ send_command = msmtp -a work -t
quote_position = above ; above | below
send_html = true ; seeds New and Forward; Reply seeds from the original
autosave_interval_ms = 30000
+send_delay_ms = 5000 ; the undo window before sending; 0 sends at once
default_account = work
attachment_warn_bytes = 26214400
```
@@ -601,6 +632,16 @@ exiting non-zero with stderr, one that does not exist. The stub writes stdin to
a file the test reads back, proving the message arrived intact. Asserts exactly
two outcomes.
+**The send delay needs its own test, and the property that matters is a
+negative one**: that Undo during the countdown leaves the stub command **never
+run at all**. A test asserting only that the composer reopened would pass
+against a design that ran the command and threw the result away, which is the
+whole failure the delay exists to prevent. Assert the stub wrote no file.
+
+Also: `send_delay_ms = 0` skips the countdown, and Undo is disabled the instant
+the command starts. Drive the countdown with a short delay rather than waiting
+five seconds in a test.
+
**`test_draftstore`**: Maildir filename validity and uniqueness, the previous
revision unlinked, the dirty check skipping a write, and an unwritable directory
reporting rather than throwing.