Sandbox Status
A sandbox lifecycle card — provisioning through reclaimed, CPU / memory meters, an uptime clock that freezes on teardown, and restart / tear-down behind a confirm and a one-shot lock.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/sandbox-status.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "SandboxStatus" component that reports the
life of ONE remote sandbox (a VM / container an agent runs code in) and offers
only the actions that state allows. Use lucide-react for icons and shadcn/ui
Badge, Button and AlertDialog. The host owns every transition; this component
never changes `state` by itself.
Contract
- forwardRef<HTMLElement>, extends React.HTMLAttributes<HTMLElement> minus
children. `variant="card"` renders a <div>, `variant="chip"` a <span>; the
rest props and className land on whichever one renders.
- state: "provisioning" | "ready" | "executing" | "idle" | "expired" | "stopped".
- name?: string (default "sandbox"), image?: string, region?: string.
- startedAt?: number — epoch of the create call. Omitted means no clock at all.
- endedAt?: number — epoch of death, read only in a terminal state.
- reclaimAt?: number — epoch an idle machine gets reclaimed.
- resources?: { id, label, used, limit?, unit?, tone? }[] — CPU / memory / disk.
- pressureAt?: number (default 0.9) — used/limit ratio that flags a meter.
- variant?: "card" | "chip" (default "card").
- confirm?: boolean (default true) — route restart / teardown through a dialog.
- busy?: SandboxAction | null — controlled in-flight action; omit for the
component's own one-shot lock.
- onRestart / onTeardown / onRecreate?: () => void.
- labels?: Partial<Labels>, confirmCopy?: per-action { title, body, action,
destructive } overrides, announce?: boolean (default true).
Behavior — one table, three phases
- Keep a single Record<state, { phase, tone, dot, actions }>. `phase` is the
coarser question the layout asks: "booting" (provisioning), "up" (ready,
executing, idle), "gone" (expired, stopped). Every branch below tests the
phase, never the state, so a seventh state is one table row.
- Actions per state: provisioning → [teardown, labelled "Cancel"]; ready /
executing / idle → [restart, teardown]; expired / stopped → [recreate].
- An action whose handler is missing is NOT rendered. Never a dead button.
- expired and stopped are both terminal but not the same event: expired is the
platform reclaiming a machine and takes the loud token, stopped is the user's
own decision and stays muted. Each gets its own one-line explanation.
Behavior — clocks
- Keep `now` as state, null until the first client tick: server and hydrating
frames must agree on every digit and Date.now() does not agree across them.
Render a dash for that one frame instead of a number that will change.
- Run a self-correcting timeout, not setInterval: after each tick schedule the
next at 1000 - ((now - startedAt) mod 1000), floored at ~50ms so a startedAt
in the future (clock skew) cannot spin. setInterval drifts and a throttled tab
makes it skip a whole second. Clear the timeout on unmount and whenever the
clock is no longer needed.
- The clock only runs while the machine is alive, so reaching a terminal state
parks `now` on its last reading and the duration freezes there — no epoch is
stamped during render, and nothing keeps counting for a machine that is gone.
- A card that MOUNTS already dead never ran the ticker: it needs `endedAt`.
Without it, drop the clock line rather than invent a duration that starts from
the moment the component happened to render.
- Prefix follows the phase: "booting 0:07", "up 12:04", "ran for 41:12".
- reclaimAt renders "reclaimed in 4:31" only in phase "up" — during a boot the
machine has not qualified for the idle list yet. Ceil the seconds so 0:01
survives its whole second, clamp at zero and swap in "reclaiming now"; the
last minute turns destructive. The host still owns the flip to "expired".
Behavior — resource meters
- One row per resource: label, "1.75 / 2 GB", and a bar. Fill width is
clamp(used/limit, 0, 1).
- ratio >= pressureAt → "near limit"; ratio > 1 → "over limit". Both flip the
fill to the destructive token AND print the words: the warning must never live
in color alone.
- limit missing / zero / non-finite = "no quota published": keep the reading,
drop the bar. role="meter" needs a real min/max pair, and a made-up
denominator is worse than no bar.
- Meters only exist in phase "up". While booting, the same array is reduced to
the shape being reserved ("Allocating · 4 vCPU · 8 GB") above an INDETERMINATE
bar — a provisioning API answers "not yet", never a percentage. In phase
"gone" the block is replaced by the released-resources line.
Behavior — actions, confirm, one-shot lock
- restart and teardown never fire on the first click: open an AlertDialog whose
copy depends on the action AND the state it fires from. Tearing down a booting
machine is a cancel; restarting a machine that is executing kills the run
mid-flight. A single per-action sentence would lie in one of those cases.
- Keep the pending action in state alongside an `open` flag so the panel can
animate out with its own copy instead of flashing empty.
- Confirming fires the callback once and latches that action. The latch clears
when the host answers — i.e. when `state` or `startedAt` changes — and never
on a timer: a machine that never answers must keep looking busy rather than
silently re-arm a destructive button.
- While latched, the fired button shows a spinner and its -ing label; all
buttons take aria-disabled + pointer-events-none, NOT the disabled attribute:
a disabled button drops focus to the body the instant it is pressed, so a
keyboard user loses their place while the restart is still in flight. Guard
the click in the handler instead.
- Use a plain <Button variant="destructive">, not AlertDialogAction: closing the
dialog and arming the lock happen in the same tick.
Rendering & styling
- Semantic tokens only: bg-card, text-card-foreground, bg-muted,
text-muted-foreground, text-destructive, border, ring, and var(--chart-1..5)
for state tones and meter fills. No hex, no rgb(), no oklch().
- The root publishes the winning state tone as a CSS variable (--sandbox-tone)
and every child paints with that one variable through color-mix, so the badge,
the dot and the boot bar can never drift apart.
- Status dot per state: solid (ready), hollow ring (idle / terminal), spinning
arc (provisioning), ping halo (executing). The halo is motion-reduce:hidden
and the arc motion-reduce:animate-none — both keep a visible, distinct shape
when animation is off. The boot sweep ships as hoisted @keyframes and degrades
to a dimmed full-width bar; meter widths use motion-reduce:transition-none.
- Name and image truncate with a title attribute; they never wrap the header.
- One polite sr-only role="status" region, permanently mounted (several screen
readers skip a live region inserted in the same frame as its text). It carries
"<name>: <state>" only — no digits, or a ticking clock re-announces the
machine once a second. The visible badge is aria-hidden while the region is
on, so the state is not read twice in browse mode. announce={false} in lists.
- Meters are role="meter" with aria-valuemin/max/now clamped to 0..100 and the
true figure (including any overshoot) in aria-valuetext. The boot bar is an
indeterminate role="progressbar" with no aria-valuenow.
Customization levers
- Lifecycle set: add or remove a state by editing the one Record — its phase
decides the layout, its tone the color, its actions the footer.
- Tones: swap var(--chart-*) per state; expired defaults to var(--destructive)
because it costs the user files, stopped to var(--muted-foreground) because
they asked for it.
- pressureAt: 0.9 suits a soft cap; drop it to 0.75 for hosts that throttle
early, or pass a per-resource `tone` to keep a bar on brand at any load.
- Density: drop `region` / `image`, drop the meter block entirely (omit
resources), or keep only the busiest resource for a compact card.
- confirm={false} when the surrounding app already owns a confirmation step;
confirmCopy to rewrite any dialog; labels for full i18n including the -ing
strings.
- busy: hand the lock to your data layer (mutation.isPending) when a request can
outlive the state change, or pass null to keep the buttons live at all times.
- Chip variant: the tail is uptime plus the busiest meter — cut it to the dot
and the name for a dense toolbar, or reuse the chip as the trigger of your own
popover with the full card inside.Concepts
- Phase over state — six lifecycle states collapse into three layout questions (is there a box yet, is there one now, is it gone); every branch tests the phase, so adding a state is a table row instead of a new rendering path.
- One-shot lock — a destroy button that can be pressed twice destroys twice, so firing an action latches it until the host answers with a changed
state; nothing times the latch out, because a machine that never answers must keep looking busy rather than silently re-arm. - Confirm copy follows the state — the same button means "cancel this boot" while provisioning and "kill the command running right now" while executing, so the dialog sentence is derived from the action and the state instead of being one fixed string per button.
- A frozen clock — the uptime ticker only runs while the machine is alive, so a teardown parks the reading where it was; a dead machine's duration is a fact to display, not a number to keep incrementing.
- Reclaim countdown — an idle deadline is rendered as time remaining (ceiled, clamped at zero, loud in the last minute) so the user can act before the platform does, while the actual expiry still arrives as a state change from the host.
- Pressure band, in words — crossing
pressureAtor the quota itself repaints the bar and prints "near limit" / "over limit", because a meter whose only warning is its hue is invisible to a third of the people reading it.
Commit Card
One agent commit as a card — a Conventional Commits badge parsed out of the raw git message, the files it touched behind an announced cap, an agent and timestamp line, a two-step one-shot revert, and a grouped rail for a whole run.
Fallback Notice
An inline "answered by a backup model" notice — the failed primary and the model that answered as a chip chain, reason-class copy, and a one-shot retry-with-primary button that suppresses itself when a retry cannot help.