Steer composer
ProSend a message into a run while it works, and see whether the agent has read it. Each message says where it is: sending, sent but not picked up yet, picked up (and when), or not delivered with the reason and a Retry. A message isn't shown as read until the service says so. The note above the box says when the agent reads messages, or that the run is quiet, offline or waiting on the person (a message doesn't answer its question). A finished run takes no messages and says so. Enter sends and Shift+Enter breaks a line.
Preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/steer-composer
Brings in: button, locale, relative-time, run-heartbeat, status, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import type { SteerMessage } from "@/components/steer-composer"
import { SteerComposer } from "@/components/steer-composer"
const minutesAgo = (m: number) => new Date(Date.now() - m * 60_000).toISOString()
const initial: SteerMessage[] = [
{ id: "m1", text: "Skip the archived orders; they’re read-only.", sentAt: minutesAgo(6), state: "picked-up", pickedUpAt: minutesAgo(5) },
{ id: "m2", text: "Use the EU region for the new column’s default.", sentAt: minutesAgo(1), state: "queued" },
]
export default function Example() {
const [messages, setMessages] = React.useState(initial)
const timers = React.useRef<ReturnType<typeof setTimeout>[]>([])
React.useEffect(() => () => timers.current.forEach(clearTimeout), [])
const move = (id: string, patch: Partial<SteerMessage>, after: number) =>
timers.current.push(setTimeout(() => setMessages((ms) => ms.map((m) => (m.id === id ? { ...m, ...patch } : m))), after))
return (
<SteerComposer
className="max-w-md"
agentName="Build agent"
run={{ status: "running", lastHeardAt: minutesAgo(0.5) }}
messages={messages}
onSend={(text) => {
const id = String(Date.now())
setMessages((ms) => [...ms, { id, text, sentAt: new Date().toISOString(), state: "sending" }])
// Your service says when it has the message, and again when the agent reads it.
move(id, { state: "queued" }, 600)
move(id, { state: "picked-up", pickedUpAt: new Date().toISOString() }, 4000)
}}
onWithdraw={(id) => setMessages((ms) => ms.filter((m) => m.id !== id))}
/>
)
}<SteerComposer>
| Prop | Type | Default | Description |
|---|---|---|---|
| runrequired | { status: RunStatus; lastHeardAt?: Date | string | undefined; hostOffline?: HostOffline | undefined } | — | The run being steered: its status, when it was last heard from, and whether its host is offline. |
| messagesrequired | SteerMessage[] | — | Messages already sent, oldest first, each with where it is. |
| onSendrequired | (text: string) => void | — | Called with the person's words. Add the message to `messages` (as "sending" or "queued") to show it. |
| onRetry | ((id: string) => void) | undefined | — | Called with a failed message's id when the person picks Retry. Without it, there's no Retry. |
| onWithdraw | ((id: string) => void) | undefined | — | Called with a queued message's id to take it back before the agent reads it. Without it, there's no Withdraw. |
| agentName | string | undefined | — | The agent's name, from your app, used in "Build agent hasn't picked it up yet". |
| pickupNote | string | null | undefined | — | When your agent reads new messages, in your words. Defaults to "between steps"; null hides it. |
| staleAfter | number | undefined | STALE_AFTER | How long a running run may be silent before it's called quiet, in milliseconds. Defaults to two minutes. |
| placeholder | string | undefined | — | — |
| className | string | undefined | — | Extra classes for the outer element. |
Strings
The fixed words, in English by default. Change any of them under steerComposer in <LocaleProvider strings={{ … }}>; the table is exported as steerComposerStrings.
| Key | English |
|---|---|
| label | Message to the run |
| messages | Messages to the run |
| placeholder | Add a note or change direction |
| send | Send |
| sending | Sending… |
| queued | Sent. The agent hasn’t picked it up yet. |
| queuedBy | Sent. {agent} hasn’t picked it up yet. |
| pickedUp | Picked up |
| pickedUpBy | Picked up by {agent} |
| failed | Not delivered |
| failedWhy | Not delivered: {reason} |
| retry | Retry |
| retryLabel | Retry “{text}” |
| withdraw | Withdraw |
| withdrawLabel | Withdraw “{text}” |
| pickupNote | The agent reads new messages between steps, so it may finish what it’s doing first. |
| waitingNote | The run is waiting on you. A message here doesn’t answer what it asked. |
| staleNote | {status}, so it may not read this soon. |
| offlineNote | {status}. Messages wait until it’s back. |
| ended | This run has ended, so it can’t take messages. |
| hintSend | Enter to send, Shift+Enter for a new line |
Types
- SteerState
- Where a message to a running run is. "sending": on its way to your service. "queued": your service has it, and the agent hasn't read it yet. "picked-up": the agent has read it (at `pickedUpAt`). "failed": it wasn't delivered (`reason` says why).
"sending" | "queued" | "picked-up" | "failed"- SteerMessage
- One message sent into a run while it works.
{ id: string /** The person's words, shown as plain text. */ text: string sentAt: Date | string state: SteerState /** When the agent read it, when the service reports that. */ pickedUpAt?: Date | string | undefined /** Why it wasn't delivered. Shown with "failed". */ reason?: string | undefined }