AI

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { Box, LoaderCircle, Play, Power, RotateCcw } from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  AlertDialog,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/sandbox-status.json

Prompt

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 pressureAt or 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.

On This Page