Agent Panel
An agent on a record as a collaborator — whom it acts for, its state and actions, instructions, latest messages, a reply box and runs with Undo.
- Status
- Since
0.25.89- Accessibility pattern
- polite announcements, named controls, state not by colour
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Agent Panel before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify agent-panelInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/agent-panelRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/button, @vegastack/collapsible, @vegastack/dropdown-menu, @vegastack/hover-card, @vegastack/input-group, @vegastack/person-avatar, @vegastack/record-aside, @vegastack/record-chip, @vegastack/relative-time, @vegastack/spinner, @vegastack/textarea, @vegastack/tooltip, @vegastack/use-announcer, @vegastack/disclosure-trigger.
Usage
import {
AgentChip,
AgentPanel,
AgentSlot,
AgentStateRing,
} from "@/components/ui/agent-panel";
<AgentPanel
agent={{ name: "Claude Code", via: "Priya", access: "Read & write" }}
state="working"
startedAt={run.startedAt}
progress={run.lastNote}
instructions={delegation.instructions}
onInstructionsChange={canSteer ? saveInstructions : undefined}
latest={events}
latestTotal={eventCount}
onShowAll={() => filterActivity(agent.id)}
onReply={canSteer ? reply : undefined}
runs={runs.map((run) => ({
...run,
onView: () => openRun(run),
onUndo: () => undo(run),
}))}
onViewAgent={openAgent}
onChangeAgent={canSteer ? openDelegate : undefined}
onStop={canSteer ? stop : undefined}
onRemove={canRemove ? remove : undefined}
/>;The panel is presentational: the host owns the delegation and passes its state, its data and the
callbacks. The callbacks decide what shows: pass onReply, onInstructionsChange and onStop
only to the person who delegated, onRemove to them and to admins, and onReconnect to the
agent's owner. state="none" renders nothing — the Delegate slot carries the empty state.
Each state reads the props it needs: queued (queuedHint once it has waited a while), working
(startedAt, progress), question (question, choices, onAnswer), approval (approval,
onApprove, onDecline), failed (error, onRetry), blocked (blockedReason,
onReconnect), done (finishedAt, durationSeconds, summary, onMarkDone) and stopped
(stoppedBy, finishedAt, changeCount, onDelegateAgain). A finished, failed or stopped card
whose agent was disconnected since takes disconnected and adds a muted note — it never turns back
into Needs you.
Scope
Placement follows one rule: the rail card on a record page (variant="card", directly under
Properties — above it while it needs you, data-attention), the section in a peek
(variant="section"), the pill in a peek's pill row and a phone's facts row (AgentChip),
the one-line Delegate property (AgentSlot) and the state mark or ring in lists and boards
(AgentStateBadge, AgentStateRing). AgentHoverCard puts the agent's facts behind its avatar or
name anywhere it appears. Choosing an agent is the DelegatePopover; an agent's edit to a document
is the AgentEditBanner. Every string comes in through labels and stateLabels, with the English
defaults shown here.
Anatomy
Examples
States
The same card in every state: Queued with Stop and the hint after a while; Needs you with the question, one-tap answers and Reply…; Wants approval with Approve and Decline; Failed with Retry and Remove; Blocked with Reconnect for the owner; Done with its date, run time, closing message and Mark task done; Stopped with what stays and Delegate again — and a done card whose agent was disconnected since. History's Undo shows disabled with its reason when the run can't be undone.
Section
In a peek the panel is a collapsible section whose header is the one-line summary and the state's quick action. It opens unless the work is done or stopped.
Instructions
Draft the proposal from M-15.
Latest
- Message: Two prices conflict; using v2
- Change: Status → In progress
History
- Run · 3 changes
Needs you
Which price list — v2 or the client's?
Done3 minProposal drafted in P-42.
History
- Run · 3 changes
Slot, chip, badge and ring
AgentSlot is the Delegate property's value on one line — the agent's name truncates, its state
word never does — with the ghost "Delegate to an agent…" when empty and the muted reason when the
viewer can't delegate. AgentChip is the same as a pill; AgentStateBadge the state as icon and
word; AgentStateRing rings an agent's avatar amber while it needs you and red when it failed.
Hover the agent for AgentHoverCard.
- Assignee
- Delegate
- Delegate
- Delegate
- Delegate
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
agent* | AgentPanelAgent | — | The agent. |
state* | AgentState | — | Where the delegation stands; none renders nothing (the Delegate slot is empty). |
approval | React.ReactNode | — | What the agent asks to do (state approval): "Move 3 tasks to Trash". |
blockedReason | React.ReactNode | — | Why it can't go on (state blocked): "Its token expired". |
changeCount | number | — | How many of its changes stay (stopped). |
choices | readonly AgentPanelChoice[] | [] | One-tap answers to the question. |
defaultOpen | boolean | true, false when done or stopped | The section's initial open state. |
disconnected | boolean | false | The agent was disconnected since: a finished, failed or stopped card adds the muted "Agent since disconnected" and never turns back into Needs you. |
durationSeconds | number | — | How long the run took (done), in seconds: "3 min". |
error | React.ReactNode | — | The agent's problem (state failed): "Couldn't open M-15 — no access." |
finishedAt | string | number | Date | — | When it finished (done) or was stopped (stopped). |
instructions | string | null | — | What the agent was asked to do. |
labels | Partial<AgentPanelLabels> | — | Replace any of the strings (AgentPanelLabels). |
latest | readonly AgentPanelEvent[] | [] | The agent's latest messages and changes, newest first; the first three show. |
latestTotal | number | — | How many there are in all, for "Show all 5". |
now | number | — | Pin the clock for elapsed and relative times (docs, tests). |
onAnswer | ((value: string) => void) | — | Answer the question with a choice; the choice buttons show when set. |
onApprove | (() => void) | — | Approve; shows when set. |
onChangeAgent | (() => void) | — | Menu: Change agent; shows when set. |
onDecline | (() => void) | — | Decline; shows when set. |
onDelegateAgain | (() => void) | — | Delegate again (stopped); shows when set. |
onInstructionsChange | ((instructions: string) => void | Promise<void>) | — | Save edited instructions — only for a viewer who may steer: ✎ shows, and an empty instruction offers "Add instructions". A returned promise keeps the editor open until it settles, and open on a rejection. |
onMarkDone | (() => void) | — | Mark the record done (done, when it isn't done on its own); shows when set. |
onOpenChange | ((open: boolean) => void) | — | Called when the section opens or closes. |
onReconnect | (() => void) | — | Reconnect (blocked) — pass it to the agent's owner only. |
onRemove | (() => void) | — | Remove the agent from the record (menu, and the Failed line); shows when set. |
onReply | ((text: string) => void | Promise<void>) | — | Send a reply to the agent — only for a viewer who may steer; the reply box shows when set. A returned promise keeps the text until it resolves. |
onRetry | (() => void) | — | Retry (failed); shows when set. |
onShowAll | (() => void) | — | Show them all (filter the activity to this agent); "Show all" shows when set. |
onStop | (() => void) | — | Stop (queued and working lines, and the menu while it is on the work); shows when set. |
onViewAgent | (() => void) | — | Menu: View agent; shows when set. |
open | boolean | — | The section's open state (controlled). |
progress | React.ReactNode | — | The agent's latest progress line while working ("Reading the 3 linked pages…"). |
question | React.ReactNode | — | The agent's open question (state question). |
queuedHint | boolean | false | Queued a while (the host's 10 minutes): add the "picks up work when it's running" hint. |
replyDisabledReason | React.ReactNode | — | Why the reply box is disabled ("It's stopped"), shown under it. |
runs | readonly AgentPanelRun[] | [] | The agent's runs, newest first, under History. |
startedAt | string | number | Date | — | When the agent started (working: "Working · 4 min"). |
stateLabels | Partial<AgentStateLabels> | — | Replace any state word (AgentStateLabels). |
stoppedBy | string | — | Who stopped it (stopped): "Priya". |
summary | React.ReactNode | — | The agent's closing message (done): "Proposal drafted in P-42." |
variant | "card" | "section" | "card" | card — the record page's rail card. section — the peek's collapsible section, its header
a one-line summary; open unless the work is done or stopped. |
Data attributes and CSS variables on AgentPanel
| Attribute | Values |
|---|---|
data-attention | "" |
data-slot | "agent-panel" | "agent-panel-content" | "agent-panel-header" |
data-state | mirrors a prop or state value |
data-variant | "card" | "section" |
AgentPanelRun
| Prop | Type | Default | Description |
|---|---|---|---|
at* | string | number | Date | — | When the run made its last change. |
changeCount* | number | — | How many changes the run made. |
id* | string | — | A stable key. |
onUndo | (() => void) | — | Undo the run's changes; Undo shows when set. |
onView | (() => void) | — | Open the run's changes; View shows when set. |
undoDisabledReason | string | — | Why the run can't be undone ("Edited since"): Undo shows disabled with this as its tooltip and description. |
undoing | boolean | false | The undo is running: Undo spins and is disabled. |
AgentPanelEvent
| Prop | Type | Default | Description |
|---|---|---|---|
at* | string | number | Date | — | When. |
id* | string | — | A stable key. |
kind* | "change" | "message" | — | A message from the agent (💬) or a change it made (↻). |
text* | React.ReactNode | — | The message, or the change ("Status → In progress"). |
AgentSlot
| Prop | Type | Default | Description |
|---|---|---|---|
agent | AgentPanelAgent | null | — | The agent delegated to; none shows the ghost placeholder. |
disabledReason | React.ReactNode | — | Why the viewer can't delegate ("Only Priya can delegate — assign it to yourself first"): the empty slot shows it, muted, as text instead of a button. |
loading | boolean | — | Shows a spinner over the label, blocks activation and sets aria-busy. The label keeps its
box at opacity: 0, so the button's width does not move and its accessible name survives
(API-5, A11Y-12). |
placeholder | React.ReactNode | "Delegate to an agent…" | The empty slot's ghost text. |
readOnly | boolean | false | Show the agent as text, not a button (the viewer can't change it). |
size | "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | — | |
state | AgentState | "working" when an agent is set | Where its work stands. |
stateLabels | Partial<AgentStateLabels> | — | Replace any state word (AgentStateLabels). |
variant | "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | — |
Data attributes and CSS variables on AgentSlot
| Attribute | Values |
|---|---|
data-disabled | "" |
data-empty | "" |
data-slot | "agent-slot" |
data-state | mirrors a prop or state value |
AgentChip
| Prop | Type | Default | Description |
|---|---|---|---|
agent | AgentPanelAgent | null | — | The agent delegated to; none shows the dashed placeholder pill. |
loading | boolean | — | Shows a spinner over the label, blocks activation and sets aria-busy. The label keeps its
box at opacity: 0, so the button's width does not move and its accessible name survives
(API-5, A11Y-12). |
placeholder | React.ReactNode | "Delegate" | The empty pill's text. |
size | "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | — | |
state | AgentState | "working" when an agent is set | Where its work stands. |
stateLabels | Partial<AgentStateLabels> | — | Replace any state word (AgentStateLabels). |
variant | "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | — |
Data attributes and CSS variables on AgentChip
| Attribute | Values |
|---|---|
data-empty | "" |
data-slot | "agent-chip" |
data-state | mirrors a prop or state value |
AgentStateRing
| Prop | Type | Default | Description |
|---|---|---|---|
state* | AgentState | — | The state: amber ring while it needs you, red when it failed, none otherwise. |
srLabel | boolean | true | Say the state to screen readers after the avatar ("Needs you"). Turn it off when the state's
word is already written beside it (an AgentStateBadge). |
stateLabels | Partial<AgentStateLabels> | — | Replace any state word (AgentStateLabels). |
Data attributes and CSS variables on AgentStateRing
| Attribute | Values |
|---|---|
data-slot | "agent-state-ring" |
data-state | mirrors a prop or state value |
AgentHoverCard
| Prop | Type | Default | Description |
|---|---|---|---|
agent* | AgentPanelAgent | — | The agent. |
children* | React.ReactNode | — | The trigger — the agent's PersonAvatar (trigger="avatar") or its name (trigger="name"). |
className | string | — | Classes for the trigger button. |
labels | Partial<AgentHoverCardLabels> | — | Replace any of the strings (AgentHoverCardLabels). |
onViewAgent | (() => void) | — | Open the agent's page; the View agent link shows when set. |
state | AgentState | — | Where its work on this record stands, shown under its access. |
stateLabels | Partial<AgentStateLabels> | — | Replace any state word (AgentStateLabels). |
trigger | "avatar" | "name" | "avatar" | avatar — the children sit in a round ghost button named for the agent. name — inline text
(an activity's actor) in a quiet text button. |
Data attributes and CSS variables on AgentHoverCard
| Attribute | Values |
|---|---|
data-icon | "inline-end" |
data-slot | "agent-hover-card" | "agent-hover-card-avatar" | "agent-hover-card-name" |
Accessibility
- A change of state while the panel is on screen is announced politely ("Claude Code: Needs you"); the state it mounts with is not. Relative times tick silently.
- The state is always an icon and a word; colour is never the only cue. The ring's state is
said to screen readers unless the word is already beside it (
srLabel={false}). - The ⋯ menu is named for the agent ("More actions for Claude Code") and lists only the actions the viewer has. The agent's bot is decoration; the words name it.
- A run that can't be undone keeps a focusable, disabled Undo whose reason is its tooltip and its accessible description.
- The section's header is a heading holding the disclosure button, with
aria-expanded.
| Key | Action |
|---|---|
| Tab | Move through the menu, the state's actions, ✎, Reply and runs. |
| Enter / Space | Activate a button, or open and close the section. |
| Ctrl + Enter | Send a reply, or save edited instructions (⌘ on a Mac). |
| Esc | Cancel editing the instructions. |
| Contract | States tested |
|---|---|
| Behaviour | none, queued, queued-hint, working, question, approval, failed, blocked, done, stopped, disconnected, instructions-edit, latest, reply, reply-disabled, history-undo, undo-disabled, menu, section-collapsed, slot-empty, slot-disabled, chip, ring, hover-card, labels |
| Accessibility | polite-state-announcement, named-menu, decorative-avatar, silent-relative-time, undo-disabled-reason, state-word-not-colour, browser-accessibility-test |
| Visual | card-states, section, slot, chip, badge-ring, hover-card |