Skip to content
Join UI
Data Displaystable

Agent Hive

A honeycomb of frosted glass with one tinted tile that travels to whatever you pick, an action, and the queue of runs it dispatches.

View sourceon GitHub (opens in a new tab)Open full preview

AgentHive is the console pattern every AI product ends up drawing: choose an engine, send it something, watch the work come back. The comb is glass — frosted hexagons with a lit top edge and a shaded bottom one, over a wash of the selected model's own colour that is masked to the cells, so the light lives inside the material rather than behind it. The selection is not a state each cell paints for itself but a single tinted tile that travels: it stretches along its own direction of movement, its reflection slides to the trailing edge, the cells it passes are shoved aside and drawn back a beat later, and a marker smears along a rail above the comb to keep the reading. Everything is driven off one shared spring, so nothing lags the shape making it. The comb assembles from the middle outwards on mount, a cell thickens and lifts under the cursor, and the light swells while work is in flight. Below it the action takes the selected accent and pushes a ring out for as long as something is running. The queue is yours: onGenerate hands you the model that was picked, you push a row, and the component types out any run that arrives after mount while leaving the rows that were already there alone.

Preview

Agent Hive
Pick, then generate

The comb is frosted glass over a wash of the selected model's colour, and one tinted tile slides between the cells — stretching along its own direction of travel, with the marker smearing along the rail above. Press the action: a run drops in and types itself out, and the one below it is finished text the moment it does.

Prism Flash

Drafts and rewrites

  1. Rewrite the onboarding copy for the empty dashboardDone
  2. Trace the failing checkout webhook back to its retryFailed
size="sm"

A tighter comb and every run state at once. Nothing types here — these rows were on screen at mount, and history does not retype itself.

Swift

  1. Regenerate the API referenceWorking
  2. Diff the schema against stagingQueued

Installation

Install Agent Hive with the shadcn CLI. The registry item resolves its own dependencies and writes the file to components/joinui/agent-hive.tsx.

pnpm dlx shadcn@latest add @joinui/agent-hive

Usage

example.tsx
import { AgentHive, type AgentHiveRun } from "@/components/joinui/agent-hive"
import { Asterisk, Atom, Gem, Sparkles } from "lucide-react"

const models = [
  { id: "aster", label: "Aster 3", icon: <Asterisk />, accent: "#c2410c" },
  { id: "prism", label: "Prism Flash", icon: <Sparkles />, accent: "#4338ca" },
  { id: "nimbus", label: "Nimbus 2", icon: <Atom />, accent: "#0f766e" },
  { id: "quartz", label: "Quartz Mini", icon: <Gem />, accent: "#15803d" },
]

export function Example() {
  const [runs, setRuns] = React.useState<AgentHiveRun[]>([])

  return (
    <AgentHive
      models={models}
      runs={runs}
      onGenerate={(model) =>
        setRuns((current) => [
          {
            id: crypto.randomUUID(),
            prompt: "Identify code optimizations and performance improvements",
            modelId: model.id,
            state: "working",
          },
          ...current,
        ])
      }
    />
  )
}

Props

AgentHive

Props for AgentHive
PropTypeDefaultDescription
models (required)AgentHiveModel[]—The models to lay into the comb. They fill it from the middle outwards, so the order is the order they radiate, not a row-by-row layout.
valuestring—Controlled selection, by model id. Leave unset to let the component hold it and read the selection through onValueChange.
defaultValuestring—Where an uncontrolled comb starts. Defaults to the first enabled model. Pass value="" to start with nothing selected.
onValueChange(id: string) => void—Fires with the model id when the selection moves.
runsAgentHiveRun[][]The queue below the action, newest first. It is state you own, so it can come from anywhere — a websocket, a poll, a reducer.
onGenerate(model: AgentHiveModel) => void—Fires with the selected model when the action is pressed. Push a run onto runs from here.
actionLabelstring"Generate"Copy on the action.
busyboolean—Overrides the busy state, which is otherwise true whenever a run is working. Busy is what sets the rings leaving the action, the swell in the light behind the comb, and aria-busy on the frame — so a dispatch that has not reached the queue yet can still say so.
disabledbooleanfalseDisables the whole comb and the action.
combnumber[][3, 4, 3]Widths of the comb's rows, top to bottom. Rows are centred on each other, which is where the half-cell offset comes from. More models than cells and the comb grows by alternating its own widest and narrowest row.
size"sm" | "md""md"Cell, action, row and type scale.
railbooleantrueThe rail above the comb and the marker riding it. The marker tracks the selected cell's horizontal centre and smears along the rail as it goes, keyed to horizontal velocity alone — so a hop between two cells in the same column moves it nowhere and does not stretch it.
maxRunsnumber3Rows drawn before the queue fades out. The overflow row dissolves into a mask rather than being cut off.
typingbooleantrueType the newest run out a character at a time. Only ever the newest: one run is being written at a time, so the moment another arrives the row below it is finished text. Runs present at mount are history and never type at all.
typeSpeednumber26Milliseconds per character. Floored at 4.
emptyLabelstring"Nothing queued."Shown in place of the queue while there is nothing in it.
labelstring"Model"Accessible name of the comb.
variant"card" | "plain""card"Plain drops the surrounding rule, background and padding so the hive can sit inside your own container.
classNamestring—Merged onto the root through `cn` — this is where the width goes, since the size only sets a max.

AgentHiveModel

Shape of a single entry in the `models` array.

Props for AgentHiveModel
PropTypeDefaultDescription
id (required)string—Stable identity, and the value reported by onValueChange.
label (required)string—The model's name. Printed under the comb, and the accessible name of its cell.
descriptionstring—Second line under the comb while the model holds the selection.
iconReact.ReactNode—Drawn inside the cell. Sized by the component, so pass a bare icon element — or a brand mark as inline SVG.
accentstring—Any CSS colour. Tints the glass tile while the model holds the selection, lights the wash behind the comb, colours its glyph while it does not, carries into the action, and marks every run the model produced. Omit it and the component falls back to its ink tokens.
disabledboolean—Draws the cell at half strength and takes it out of both the tab order and the arrow-key walk.

AgentHiveRun

Shape of a single entry in the `runs` array.

Props for AgentHiveRun
PropTypeDefaultDescription
id (required)string—Stable identity for the rendered row, and what the component keys typing off — reuse an id and the row is treated as the same run.
prompt (required)string—The line of work. Typed out when the run arrives after mount.
state"queued" | "working" | "done" | "failed""queued"Picks the pip and the word beside it: grey for queued, amber and pulsing for working, green for done, red for failed.
statusstring—Overrides the status word — “Retrying”, “Queued · 3rd”, an elapsed time — without changing the hue.
modelIdstring—Ties the run to a model, which marks the row with that model's cell in miniature. Leave it out and the marker stays neutral.

Dependencies

npm packages

  • motion

Registry items

  • utils

Installed automatically by the CLI when they are missing.

Accessibility

  • The comb is a real radio group: `role="radiogroup"` on the frame, `role="radio"` with `aria-checked` on every occupied cell, and a roving tabindex — so a nine-model hive costs one tab stop and the arrows move inside it.
  • Cells are glyph-only, so each carries its model name as `aria-label`, and the name is also printed under the comb where a sighted user can read it. The rail and the fill are decoration on top of that, never the only cue.
  • Empty comb, the travelling tile and the rail are all `aria-hidden` and non-interactive; they are scenery, and they are not announced or focusable.
  • The focus ring is drawn as a hexagon rather than left as the default outline, which would trace the cell's bounding box instead of the cell.
  • The selected cell is the only filled one, so the selection survives a monochrome rendering, a forced-colours mode and any accent a consumer picks — fill and position carry it, not hue and not translucency.
  • The material is decoration and degrades to a flat tint: a browser without `backdrop-filter` still gets a frosted wash, a lit edge and the tinted tile, because none of the three depends on the blur.
  • Run states never rest on hue alone — every row prints its status in words next to the pip, and `status` overrides that word rather than removing it.
  • The queue is an ordered list with `aria-live="polite"`, so a run arriving or landing is announced without stealing focus.
  • A typing row renders its prompt twice: the full text, invisible but in the accessibility tree, under the prefix typed so far, which is `aria-hidden`. Assistive technology reads the whole line at once instead of a character at a time, and the row reserves its final height so nothing below it reflows while it types.
  • The only continuous motion is the ring leaving the action, the pip on a working run and the swell in the light behind the comb — all three tied to the queue, and all three stopping when it does.
  • `prefers-reduced-motion` is honoured structurally rather than by shortening durations: the mount wave, the typewriter, the caret, the ring, the smear, the wake and every spring are not started at all, and the tile and the marker are positioned rather than animated. What is left is a still component that still works.
  • Both themes are covered by the tokens rather than by `dark:` variants, so the component keeps its contrast inside a forced-theme subtree.

Keyboard interactions

Keyboard interactions for Agent Hive
KeyBehaviour
→↓Selects the next model in the comb, skipping disabled ones.
←↑Selects the previous model in the comb.
HomeSelects the first model.
EndSelects the last model.
TabMoves from the comb to the action, and on out of the component.
EnterSpaceFires the action.

Customization

Reshape the comb

The comb prop is the row widths, top to bottom, and rows centre on each other — so [3, 4, 3] is a hexagon of hexagons, [1, 2, 1] is a diamond, and a single-element array is a straight row. Models always fill from the middle outwards, so the shape stays balanced whatever the count.

tsx
{/* The default: ten cells, four of them models. */}
<AgentHive models={models} comb={[3, 4, 3]} />

{/* A diamond for four. */}
<AgentHive models={models} comb={[1, 2, 1]} size="sm" />

{/* One row, no scenery. */}
<AgentHive models={models} comb={[4]} />

Wire it to a real backend

onGenerate hands you the model and gets out of the way. Push a working row, resolve it, and let the states carry the rest — the halo, the pulsing pip and aria-busy all follow from the run's state without a second prop.

tsx
async function generate(model: AgentHiveModel) {
  const id = crypto.randomUUID()
  const prompt = draft.trim()

  setRuns((current) => [{ id, prompt, modelId: model.id, state: "working" }, ...current])

  try {
    await dispatch({ model: model.id, prompt })
    setRuns((current) =>
      current.map((run) => (run.id === id ? { ...run, state: "done" } : run))
    )
  } catch (error) {
    setRuns((current) =>
      current.map((run) =>
        run.id === id ? { ...run, state: "failed", status: "Retrying" } : run
      )
    )
  }
}

Bring your own marks

An icon is any node, so the cells can carry real brand marks as inline SVG rather than an icon-set approximation. The component sizes them and leaves the fill alone unless an accent is set, in which case the glyph takes the accent and the action does too.

tsx
const models = [
  {
    id: "orbit",
    label: "Orbit",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.75}>
        <circle cx="12" cy="12" r="4" />
        <ellipse cx="12" cy="12" rx="10" ry="4.5" transform="rotate(-28 12 12)" />
      </svg>
    ),
    accent: "#7c3aed",
  },
]

Give the glass something to refract

Frosting only has something to say when there is something behind it. On a flat panel the hive supplies its own light — the wash of the selected accent — but dropped onto a photograph, a gradient or a mesh, the cells sample it: the blur and the saturation boost are real, so the picture moves under the comb rather than beside it. Plain removes the frame so the surface can be yours.

tsx
<div className="relative overflow-hidden rounded-3xl p-6">
  <img src="/aurora.jpg" alt="" className="absolute inset-0 size-full object-cover" />
  <AgentHive variant="plain" models={models} runs={runs} className="relative" />
</div>

Just the picker, or just the queue

The three bands are independent. Drop runs and the queue collapses to its empty line; set maxRuns to keep a long queue from taking the page over. Plain removes the frame so the hive can sit inside a card of your own.

tsx
{/* A picker in a settings panel. */}
<div className="rounded-xl border border-border p-6">
  <AgentHive
    variant="plain"
    models={models}
    comb={[2, 3, 2]}
    size="sm"
    actionLabel="Set default"
    emptyLabel="No recent runs."
  />
</div>

{/* A busy console: six rows in flight, three of them on screen. */}
<AgentHive models={models} runs={runs} maxRuns={3} />

Slow the typewriter down, or turn it off

Typing is per run and happens once: a row that was on screen at mount is history and renders whole, and a row that arrives afterwards types itself out. Reduced motion turns it off on its own, so typing={false} is for when a queue is filling faster than anything can be read.

tsx
{/* Deliberate, terminal-paced. */}
<AgentHive models={models} runs={runs} typeSpeed={45} />

{/* Straight to the text. */}
<AgentHive models={models} runs={runs} typing={false} />

Slow the whole thing down, or take the motion out

The comb answers a pointer, a press and the tile going past it, and all three are gestures rather than decorations — so the component never moves on its own except while the queue is working. If a hive sits somewhere the motion would compete with the page, `rail={false}` drops the index above the comb and the busy states carry on regardless; anyone who has asked their system for reduced motion already gets a still drawing without you doing anything.

tsx
{/* No rail, and a comb that only moves when it is touched. */}
<AgentHive models={models} rail={false} runs={runs} />

{/* The busy signal, driven by you rather than read off the queue. */}
<AgentHive models={models} busy={isDispatching} runs={runs} />

Tune the material

The glass is four numbers, and all four are custom properties rather than props, because they are a decision about the surface the component was installed onto rather than about any one hive. The frosting is a wash of the theme's own ink, which is what lets a single value serve both themes; the glow is the one exception, since the same 45% that reads as light on paper reads as a whisper on charcoal, so a product that lives in one theme should say so. The glyph on the tinted tile is near-white by default — if your accents are pale, redeclare it once instead of threading a second colour through every model.

css
/* app/globals.css */
:root {
  /* The frosting: at rest, empty, and under the cursor. */
  --agent-hive-glass: color-mix(in oklab, var(--foreground) 7%, transparent);
  --agent-hive-glass-empty: color-mix(in oklab, var(--foreground) 4%, transparent);
  --agent-hive-glass-lit: color-mix(in oklab, var(--foreground) 11%, transparent);
  /* How brightly the selected accent lights the comb from behind. */
  --agent-hive-glow: 0.45;
  /* The glyph on the tinted tile. */
  --agent-hive-glyph: oklch(0.985 0.002 90);
}

.dark {
  --agent-hive-glow: 0.55;
}
  • A colour-coded step tracker for orders, deployments and onboarding, still at rest and animated only when a step advances.

  • A coil of pills that keeps one item in focus and lets the rest twist away, turned either by a timer or by the page scroll.

  • A run log for agent tool calls: the step in flight opens itself, writes its output a line at a time, then folds away and goes still.