KQ-2082 · EDIT/BEFORE/SEND

Edit Before Send

A review frame for a message an agent wants to send: the draft reads as tracked changes against your edits, Edit swaps it for a real textarea sized to its text, every change is a stop you can walk to and revert, and policy flags hold the send until their words are gone, their checks drawing on springs.flick when they are. Approve shrinks the message into an envelope's pocket on springs.glide, folds the flap shut on springs.snap with a seal on flick, waits sealed for the host, then slides it out of the frame on glide; a failed send opens the flap and unfolds the sheet with the reason, and Request changes shrinks the message toward the agent with your note. Cmd or Ctrl+Enter approves from anywhere in the frame, Cmd or Ctrl+Z reverts the focused change or the last edit, and at narrow widths the recipients collapse into a count.

KQ-2082 · EDIT/BEFORE/SEND
Fernworks procurement agentFernworks Model 3 · held 6 minHeld for review
To
Subject
Order confirmation: 240 brass gauge fittings
Files
PO-2026-114.pdf84 KBFitting spec rev C.pdf1.2 MB

Message

1 change

Hi Ines,

Thanks for the revised quote. We would like to confirm the order for 240 brass gauge fittings at $14.20$13.80 per unit, delivered to our Fieldline depot by October 30.

Payment terms will be net 15 from delivery, as discussed. Please send the signed purchase order back by Friday so we can lock in the slot.

Best regards, Fernworks Procurement

1 of 1Draft said “$14.20”
Change “net 15” first. The supplier contract says net 30. Change the terms first.

held · 1 flag · 1 change

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/edit-before-send

Props

draft
EditDraft
default defaultEditDraft
The held message: { id, agent, model, from, subject, body, heldAt }; paragraphs in body are separated by a blank line.
recipients
EditRecipient[]
default defaultEditRecipients
Who it goes to: { id, name, email, kind?: "to" | "cc" }.
attachments
EditAttachment[]
default defaultEditAttachments
Files it carries: { id, name, size } with size in bytes.
flags
EditFlag[]
default defaultEditFlags
What the sending policy found: { id, severity: "block" | "warn", match, message }; a block holds Approve while its words are in the text.
value / defaultValue
string
default — / draft.body
The message text, controlled or initial.
onValueChange
(text) => void
default —
Fires with the text as it is typed, reverted or undone.
onEdit
(text, changes) => void
default —
Fires when an edit settles (leaving the editor, a revert, an undo) with every change against the draft as { from, to }.
onApprove
(text) => void | Promise
default —
Approve and send was pressed; the envelope waits sealed until a returned promise resolves, and a rejection unfolds it with the reason.
onRequestChanges
(note, text) => void
default —
Sent back to the agent with the reviewer's note and the text as it stands.
onDiscard
() => void
default —
The draft was discarded after the confirm.
status
"ready" | "loading" | "empty" | "error"
default "ready"
Whether a draft is here to review.
onRetry
() => void
default —
Try again was pressed after the draft failed to load.
now
number
default Oct 17 2026, 14:00 UTC
The moment the hold is timed from, in ms.
trackStyle
"inline" | "clean" | "bars"
default "inline"
How changes are marked: struck and underlined, the clean text with carets where words went, or the clean text with change bars and counts in the margin.
envelope
"plain" | "airmail" | "window"
default "plain"
The envelope the message folds into: plain, a striped airmail border, or an address window.
accent
"cobalt" | "plum" | "mint" | "ink" | string
default "cobalt"
Insertions, Approve and the seal: a named pigment or any CSS colour.
height
number
default 560
Tallest the frame grows, in px, before its body scrolls.
disabled
boolean
default false
Read only: the message can be read, not changed or sent.
className
string
default —
Classes for the frame.