Installation
npx shadcn@latest add @pixeldosa/agent-figureThe example above also uses Button. To copy it as-is, install everything it needs:
npx shadcn@latest add @pixeldosa/agent-figure @pixeldosa/buttonRequires `cn` at `@/lib/utils` and the agent marker colours (`--agent-working`, `--agent-waiting`, `--agent-blocked`, `--agent-done` and their `-soft` fills) from @pixeldosa/pixeldosa-theme.
Usage
"use client";
import { AgentFigure } from "@/components/ui/agent-figure";
<AgentFigure pose="searching" label="Checking 5 sources" />
Map what your run is doing to a pose and pass it in. The component doesn't read your
stream or own the run.
Features
- Its body shows its state. Every pose maps to something a run really does: listening, planning, searching, reading, comparing, working, asking, blocked, done. There are no moods.
- It shows how sure it is.
confident,probableandunsureare for handing over a result. Drive them from your real confidence, the same tiers asConfidence Meter. Never pick one to reassure. - It's still on your turn. While the agent works, its pen lines redraw slightly and small parts move in steps. When it's waiting on you, blocked or done, every line holds still.
- Words come first. A visible label sits beside the figure by default. The drawing supports the label; it never replaces it.
- Drawn in code. Seeded strokes render the same on server and client, and every colour comes from the theme, so it follows light and dark mode.
- Four markers, one job each. Blue is working, orange means it needs you, red is blocked, green is done. Everything else is ink.
Variants
The work poses
Your turn: it holds still
How sure it is
Mark
The head alone, for a status line, an avatar or a button.
Examples
Beside the step a run is on, with the status text doing the talking:
<AgentFigure pose={run.pose} size="lg" labelPosition="below" label={run.status} announce />
As a subtle presence in a dense tool, with only the head:
<AgentFigure variant="mark" pose={run.pose} label={run.status} />
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
pose | AgentPose | required | One of the fourteen poses above. |
variant | "figure" | "mark" | "figure" | mark draws the head alone. |
size | "sm" | "md" | "lg" | "md" | Figure: 48, 96 or 160px. Mark: 16, 24 or 32px. |
label | string | pose label | What the pose means, in words. |
hideLabel | boolean | false | Hides the visible label; the drawing then carries it as its accessible name. |
labelPosition | "end" | "below" | "end" | Where the visible label sits. |
announce | boolean | false | Announces label changes through a polite live region. |
still | boolean | false | Holds the lines still on the agent's turn too. |
Accessibility
- With a visible label, the drawing is hidden from assistive technology and the label is read as ordinary text.
- With
hideLabel, the drawing becomes an image named bylabel. announceis off by default. Turn it on only when nothing else on the page announces the run's status, so screen reader users don't hear it twice.- Under
prefers-reduced-motion, nothing moves and every pose stays a distinct drawing. - The frame clock stops when the figure is off screen or the tab is hidden.
When not to use
- As the only signal. Always pair it with words: a label,
Live Status Line, or the step list it sits beside. - In dense tables and toolbars. Use
Agent Presencein itslineform there. - To show feelings. If a pose would make the agent look more certain, sorry or pleased than the system is, don't use it.
why it's built this way
Engineering Notes
The character is generated, not illustrated: each stroke is a polyline pushed off its true path by a seeded wobble and smoothed with Catmull-Rom curves. Seeds come from the pose, the stroke name and the frame, so the server and client render byte-identical paths and there is no hydration mismatch, and a new pose costs a few coordinates rather than an asset. Every colour is a theme token, so the figure follows light and dark mode and a host's palette. Poses map to things a run actually does, never to feelings: 'blocked' is a state, 'sad' is not, and the three result poses exist so confidence can be shown honestly rather than always looking pleased. The frame clock runs only while the pose is on the agent's turn, the figure is on screen (IntersectionObserver), and the tab is visible; otherwise nothing ticks.
how it moves, and when it doesn't
Motion Notes
Two kinds of motion, both stepped at 280ms rather than eased, so the figure reads as drawn rather than rendered: the line boil (strokes re-seed every second frame) and a few discrete prop movements — eyes scanning a page, a key press, the thought bubble filling in. Both stop completely when the pose is the person's turn (`idle`, `asking`, `blocked`, `done` and the result poses), which is the system's rule that stillness means it's your turn. `still` holds the lines on the agent's turn too. Under `prefers-reduced-motion` nothing animates and each pose remains a distinct static drawing, so no information is lost.