Prompt input
Writes the next message to an agent, then sends, queues or stops it.
Examples
Nothing sent yet.
Waiting for 1 upload
3 queued, held because the first one failed
- FailedRefund Maya Chen
Rate limited. The queue waits until you retry, edit or remove it.
- QueuedDraft her a reply
- QueuedClose the ticket
Shell. Escape to leave
- Empty
- Ready
- Waiting for 2 uploadsBlocked
- Streaming
- Error
read and checked before it ran . The old runbook no longer exists.
States
ready
submitted
streaming
error
Usage
Rules
12 rules
- Must
- Insert a file, run, prompt release or command as a chip through the @ or / menu or a [label](type:id) token via insert(). Because plain "@run 4812" stays text, so Backspace eats it a letter at a time and the serialized value carries no run id.
- Must
- Pass status="submitted" while a send is in flight with no byte back; it is already Stop. Because a disabled composer leaves a slow or stuck request with no way to cancel it.
- Must
- Pass a new generation (the run id) for each run, and settle only the run onStop names. Because without it a stop that lands after the next run began settles the new run.
- Must
- Append to PromptQueue in FIFO order when the person sends while the agent works; never interrupt the run. Because stopping to send throws away the work the agent has done this turn.
- Must
- Keep a failed queued message at the head with status="failed" and its error, and stop draining. Because dropping the head loses the message without a word and sends the rest out of order.
- Must
- Render PromptQueue directly above PromptInput with no gap; they share one line. Because a gap or a wrapper between them breaks the shared line and the queue reads as a second, unrelated box.
- Must
- Pass disabledReason with what happened and when it changes, plus one action, instead of disabled alone. Because disabled alone leaves a dead box with no word on why or when it returns; the reason becomes its description.
- Must
- Pass label (default "Prompt"); never rely on the placeholder for the name. Because the editor is a contenteditable textbox named only by label, and aria-placeholder is not a name.
- Must
- Keep the default inline layout for chat; use layout="stacked" only where the person writes a long brief. Because a two-line box with a toolbar under it spends 40px of the thread on a prompt that is one line most turns.
- Must
- Return early on isComposing or keyCode 229 before reading Enter, Tab, arrows or Backspace. Because the Enter that confirms a Japanese or Chinese candidate would send a half-written prompt.
- Never
- Paint chips over a textarea with a mirror overlay. Use PromptInput. Because the overlay drifts from the text on wrap and scroll, and Backspace deletes one letter of a chip.
- Never
- Disable the composer to block double sends. The editor clears on send and the generation key guards repeats. Because disabled removes the editor from the tab order, so the person cannot write a follow-up or press Escape to stop.
Accessibility
8 checks
- The editor is a contenteditable textbox, aria-multiline, named by label; it points at the open list with aria-activedescendant; chips are contenteditable=false.
- Enter sends (holds while files upload), Shift+Enter breaks the line.
- @ after a space opens resources, / as the whole input opens commands, ! opens shell; arrows or Ctrl+P and Ctrl+N move, Enter or Tab picks.
- Escape closes the menu, then leaves shell, then cancels a held send, then stops the run.
- Backspace next to a chip removes it whole; Mod+Z restores it; arrows hop over it (mirrored in RTL).
- Up in an empty editor edits the last queued message; Alt+Up and Alt+Down move a queue row.
- Toolbar buttons are 28px with aria-labels; the editor is 16px so iOS does not zoom.
- PromptInput sends on its own and passes type="button" to its Send, so a surrounding form never submits; a bare SendButton is type="submit" while ready.
Edge cases
10 edge cases
- Long text and URLs
- Wraps; the editor grows to 8 lines (10 stacked) then scrolls, and the buttons stay on the bottom line.
- Chips with an attachment, narrow
- A long chip truncates; the attachment keeps its name.
- Right to left
- Layout mirrors; the chip hop reads the editor direction.
- Enter during upload
- The send is held and fires when the last upload finishes, with Cancel; a failed file blocks it.
- Inside a dialog, @ then Escape twice
- The first closes the menu, the second closes the dialog.
- Paste of 3 lines or 150 characters
- Collapses into a "Pasted n lines" chip and is sent in full; Mod+Z collapses it again.
- Paste past maxLength
- Refused; the status line says how much fits.
- Narrow
- A toolbar under 320px drops the tools to their own row, and under 384px the status line takes a row of its own; inline stays one row.
- Send fails
- status="error" shows Send again; the caller keeps the text.
- Drag of a type accept refuses
- The overlay takes the fail wash and says what it takes, top-left, before the drop.
API
25 props
onSubmit(value: PromptValue, attachments: Attachment[]) => voidonValueChange(value: PromptValue) => voiddefaultValuestringdraftKeystringplaceholderstringdefault'Ask the agent to do something'waitingCount, onReviewWaitingnumber, () => voiddefault0layout'inline' | 'stacked'default'inline'labelstringdefault'Prompt'status'ready' | 'submitted' | 'streaming' | 'stopping' | 'error'default'ready'onStop, generation(generation) => void, string | numberdisabled, disabledReason, disabledActionboolean, ReactNode, ReactNodedefaultfalsementions, commandsMentionItem[], CommandItem[]default[]shell, mode, onModeChangeboolean, 'normal' | 'shell', (mode) => voiddefaultfalseattachments, onAttach, onRemoveAttachment, onRetryAttachment, acceptAttachment[], (files: File[]) => void, (id) => void, (id) => void, stringdefault[]maxLengthnumberdefault100000tools, meta, voiceReactNode, ReactNode, Omit<PromptInputMicProps, 'size'>onEditQueued, onSendQueuedNow() => boolean, () => voidPromptQueue items{id, text, status?: 'queued' | 'sending' | 'failed', error?}[]defaultrequiredPromptQueue onSendNow, onRetry, onEdit, onRemove, onMove(id) => void, (id) => void, (id, text) => void, (id) => void, (id, index) => voidPromptQueue editingId, onEditingChangestring | null, (id) => voidPromptQueue returnFocus, labels() => void, PromptQueueLabelslabelsPromptInputLabelsdefaultEnglish wordsSendButton status, onStop, generation, empty, blockedReasonSendStatus, (generation) => void, string | number, boolean, stringdefault'ready'Mention type, children, missing, onRemoveMentionType, ReactNode, boolean, () => voiddefaultrequired, requiredrefRef<PromptInputHandle>
Tokens
25 tokens
--line-control- oklch(var(--ink-c) / 0.16)
--line-frame- oklch(var(--ink-c) / var(--line-frame-alpha))
--line-row- oklch(var(--ink-c) / 0.08)
--surface-card- oklch(var(--grey-c))
--code-prompt- oklch(var(--ink-c) / 0.40)
--border-focus- var(--ink)
--type-body- 340 14px/20px var(--font-sans)
--type-code- 400 13px/20px var(--font-mono)
--focus-halo- 0 0 0 3px oklch(var(--ink-c) / 0.08)
--elev-control- 0 1px 2px -1px oklch(var(--text-on-field-c) / calc(var(--shadow-alpha) * 0.6))
--text-1- var(--ink)
--text-3- oklch(var(--ink-c) / 0.66)
--fail-wash- oklch(94.5% 0.027 26)
--fail-text- oklch(51% 0.180 26)
--elev-overlay- 0 0 0 1px var(--line-frame), 0 12px 24px -12px oklch(var(--text-on-field-c) / var(--shadow-alpha))
--surface-hover- oklch(var(--ink-c) / 0.05)
--control-sm- 28px
--type-label-sm- 400 12px/16px var(--font-sans)
--type-tag- var(--type-code-sm)
--text-2- oklch(var(--ink-c) / 0.80)
--dur-fast- 120ms
--dur-base- 160ms
--ease-step- cubic-bezier(0.2, 0, 0, 1)
--attachment-max- 256px
--popup-max- 320px