Installation
npx shadcn@latest add @pixeldosa/agent-steerThe example above also uses Live Status Line. To copy it as-is, install everything it needs:
npx shadcn@latest add @pixeldosa/agent-steer @pixeldosa/live-status-lineRequires `cn` at `@/lib/utils`. The steers list is yours: set each status from what the agent actually reports. Keep a Stop control visible next to it — steering is not a replacement for stopping.
Usage
"use client";
import { AgentSteer } from "@/components/ui/agent-steer";
<AgentSteer
running={run.active}
currentStep={run.step}
steers={steers}
suggestions={["Not that file — use middleware.ts"]}
onSteer={(text, mode) => agent.redirect({ text, mode })}
onWithdraw={(id) => agent.withdraw(id)}
/>
Why it exists
Real use changes direction halfway. Most agent UIs give you two options: Stop, which throws away the work so far, or wait, which lets the agent keep going the wrong way. Agent Steer is the third option.
Features
- Status comes from the agent, not the key press. A redirect reads "Sent — waiting for the agent" until the agent acknowledges it. Then it moves to queued, applied, or declined with a reason. It never looks applied when it isn't.
- Enter waits for a clean break. A steer applies after the current step, where the agent can re-plan. Interrupt now is a separate control, because abandoning a tool call halfway can leave a half-written file or a half-sent request.
- Withdrawable. Change your mind about the change of mind, as long as it hasn't been applied.
- Suggestions fill, they don't send. A one-tap redirect is still your sentence to edit.
- Relative to something. The current step is shown above the input, so "not that one" has a "that".
Statuses
| Status | Meaning | Who sets it |
|---|---|---|
pending | Sent, not yet acknowledged | You, on send |
queued | Accepted, applies after the current step | The agent |
applied | The agent is now following it | The agent |
declined | Couldn't apply; put the reason in note | The agent |
Examples
Mapping an agent's events to statuses:
agent.on("redirect:received", ({ id }) => update(id, { status: "queued" }));
agent.on("redirect:applied", ({ id }) => update(id, { status: "applied" }));
agent.on("redirect:rejected", ({ id, reason }) => update(id, { status: "declined", note: reason }));
Beside a status line, with Stop still visible:
<LiveStatusLine state="working" status={run.step} startedAt={run.startedAt} onCancel={stop} />
<AgentSteer running currentStep={run.step} steers={steers} onSteer={steer} />
Props
| Prop | Type | Description |
|---|---|---|
running | boolean | Required. Steering is disabled when nothing is running. |
currentStep | string | What the agent is doing now. |
steers | { id, text, mode, status, note? }[] | Your redirects and their real status. |
suggestions | string[] | Fill the input for editing. Never sent automatically. |
onSteer | (text, mode: "next" | "now") => void | Required. |
onWithdraw | (id) => void | Shows Withdraw on pending and queued redirects. |
placeholder | string |
Accessibility
- The input is labelled by the current step, so screen readers hear what's being redirected.
- Status changes to the latest redirect are announced politely, in the same words shown on screen.
- Enter submits the default, after-this-step steer. Interrupt needs its own button press.
- The pending shimmer falls back to static text under reduced motion.
When not to use
Not a replacement for Stop. Keep a Stop control visible. It isn't a chat composer either:
use it only while a run is in flight. Before a run, correct the plan with Intent Preview
or Agent Plan.
why it's built this way
Engineering Notes
Built for the gap between Stop and wait: in real use, people change their minds mid-run, and today's agent UIs make them throw away the work so far to say so. Two decisions carry the trust. First, the component owns no status. Each redirect's status comes from the caller, meaning from the agent: it shows 'Sent — waiting for the agent' until the agent acknowledges, so pressing Enter never makes a redirect look like it has taken effect. Declined redirects show the agent's reason rather than disappearing. Second, the default applies at the next step boundary, where the agent can re-plan cleanly. Interrupting is a separate, secondary control, because abandoning a tool call mid-flight can leave half-written files or half-sent requests, and that risk should be chosen rather than triggered by the Enter key. Redirects that haven't been applied can be withdrawn. Suggestions fill the input rather than sending, so a one-tap redirect is still the user's sentence. The live region announces status changes to the latest redirect in words that match the visible text.
how it moves, and when it doesn't
Motion Notes
Almost still. The agent's motion belongs to the status surface beside it, such as Live Status Line, not to the steer control. The one moving element is the shared `pd-shimmer` on a pending redirect's status. Under the turn-taking grammar that is the only state where work is genuinely happening on the machine's side for that redirect; queued, applied and declined are settled facts and hold still. Colour transitions on hover and focus run at `--pd-duration-instant`, and the shimmer falls back to static text under prefers-reduced-motion and where background-clip text is unsupported.