← All posts

Every action tells you what happened

Vocion had no notifications at all. It has one now — four tones, a pending state with a floor under it, and a rule that nothing on a decision screen advances before the server says it did. Release notes 2026-09-15 — vocion-core#351, #348.

Vocion Teamvocion-v2.74.0

Status: shipped. vocion-core #351 (f243436e) and the pending states in #348 (28439fe2) are merged onto release/2026-09-15-rail and deployed to a deployment we operate. Both are still landing on main individually behind that release branch.

Until this afternoon Vocion had no toast. Not a thin one, not a bad one — none. You approved something and the row went away, and the only evidence that anything had happened was the absence of the thing you had just been looking at.

Two changes fix that, and they only work together.

Something tells you what changed

Every resolved decision now raises a notification built to one shape — toast.success('<verb> · <what>', { description: '<what happens next>' }). The title names the verb and the thing it was applied to; the line under it says what the system will now do. A failure raises toast.error with the message the server actually returned, and the screen stays exactly as it was so you can try again or change your mind.

Four tones — success, error, info and pending. Errors stay until you dismiss them; everything else clears itself after five seconds; at most three are on screen at once. There is a toast.update to rewrite a notification in place and a toast.promise for the submit-then-report shape, so one action produces one notification rather than two.

It is built on the Radix toast primitive that was already in the dependency tree rather than on a new library, which is why it inherits swipe-to-dismiss, the screen-reader announcement and the focus behaviour without anyone writing them. The whole thing is one file and one test file — components/ui/toast.tsx and toast.test.ts, eight tests. The store is module-level, so any client component calls toast.success(...) with no provider at the call site; <Toaster /> mounts once, in the dashboard layout.

Nothing moves before the server agrees

A notification is only worth anything if the screen it appears over is telling the truth. So the second half: no decision screen advances optimistically.

A submit in flight. The chosen option is the verb on the button, and while the request is in flight the button is disabled with a spinner and the option list is frozen. The next question does not appear until this one has landed.

Every submitting control on the decision surface goes through one helper, withMinimumPending, with MIN_PENDING_MS = 400 — approve, decline, snooze and regenerate on the proposal card, the quick verbs on a row, resume and cancel on a run, adopt and reject on a rule, and Submit and Next in the ask sheet. The control is disabled and spinning for at least 400 ms and for as long as the server actually takes, whichever is longer. The keyboard verbs check the same pending flag, so a twice is not two approvals.

The floor is there for a specific reason: a request that returns in 90 ms with no visible state looks exactly like a click that did not register. Four hundred milliseconds is long enough to read as I am doing it and short enough not to feel slow.

The one behavioural change worth naming: in a group of questions, Next now submits the question you are on before moving to the next one. It used to advance first. If the submit fails, the question and your selection stay on screen and the receipt at the end lists the outcome with a Fix on the failures.

Where this lives in the repo

vocion-core#351 (f243436e) and #348 (28439fe2), on release/2026-09-15-rail.

Two honest notes. #348 shipped against a placeholder at components/ui/toast so it could build before the notification branch existed; #351 replaced it, and the placeholder is gone from the release branch. And the notification surface is in-app only — it is not a notification centre, there is no history, and nothing is delivered to you when the tab is closed. Mail and Slack remain the out-of-app channels.

Related: One place to decide.