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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| draft | EditDraft | defaultEditDraft | The held message: { id, agent, model, from, subject, body, heldAt }; paragraphs in body are separated by a blank line. |
| recipients | EditRecipient[] | defaultEditRecipients | Who it goes to: { id, name, email, kind?: "to" | "cc" }. |
| attachments | EditAttachment[] | defaultEditAttachments | Files it carries: { id, name, size } with size in bytes. |
| flags | EditFlag[] | 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 | — / draft.body | The message text, controlled or initial. |
| onValueChange | (text) => void | — | Fires with the text as it is typed, reverted or undone. |
| onEdit | (text, changes) => void | — | 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 | — | 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 | — | Sent back to the agent with the reviewer's note and the text as it stands. |
| onDiscard | () => void | — | The draft was discarded after the confirm. |
| status | "ready" | "loading" | "empty" | "error" | "ready" | Whether a draft is here to review. |
| onRetry | () => void | — | Try again was pressed after the draft failed to load. |
| now | number | Oct 17 2026, 14:00 UTC | The moment the hold is timed from, in ms. |
| trackStyle | "inline" | "clean" | "bars" | "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" | "plain" | The envelope the message folds into: plain, a striped airmail border, or an address window. |
| accent | "cobalt" | "plum" | "mint" | "ink" | string | "cobalt" | Insertions, Approve and the seal: a named pigment or any CSS colour. |
| height | number | 560 | Tallest the frame grows, in px, before its body scrolls. |
| disabled | boolean | false | Read only: the message can be read, not changed or sent. |
| className | string | — | Classes for the frame. |