{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "run-list",
  "type": "registry:component",
  "title": "Run List",
  "description": "Keep an eye on every background agent from one calm list, with live clocks, counts by status and a click to see the last few steps or sign off the work.",
  "categories": [
    "component",
    "Mission Control"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/badge.json",
    "https://coss.com/ui/r/button.json",
    "https://coss.com/ui/r/tabs.json"
  ],
  "files": [
    {
      "path": "registry/devl/run-list.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon, ChevronDownIcon, CircleAlertIcon, CircleCheckIcon, CircleXIcon, ExternalLinkIcon, RotateCwIcon, SquareIcon, XIcon } from \"lucide-react\";\nimport { AnimatePresence, LayoutGroup, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Tabs, TabsList, TabsTab } from \"@/components/ui/tabs\";\nimport { cn } from \"@/lib/utils\";\n\n// Stays false until mounted so the server render and the first client render match.\nfunction useSafeReducedMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nexport type RunStatus = \"running\" | \"review\" | \"failed\" | \"done\";\n\nexport interface RunListItem {\n  id: string;\n  title: string;\n  agent: string;\n  repo?: string;\n  status: RunStatus;\n  elapsed: number;\n  steps: string[];\n  outcome?: Exclude<RunStatus, \"running\">;\n}\n\nexport interface RunListProps {\n  tasks: RunListItem[];\n  autoplay?: boolean;\n  title?: string;\n  onOpen?: (task: RunListItem) => void;\n  className?: string;\n}\n\ntype Filter = \"all\" | RunStatus;\n\nconst order: Record<RunStatus, number> = { review: 0, failed: 1, running: 2, done: 3 };\n\nconst statusMeta: Record<RunStatus, { label: string; badge: \"info\" | \"warning\" | \"error\" | \"success\" }> = {\n  running: { label: \"Running\", badge: \"info\" },\n  review: { label: \"Needs review\", badge: \"warning\" },\n  failed: { label: \"Failed\", badge: \"error\" },\n  done: { label: \"Done\", badge: \"success\" },\n};\n\nconst filters: { value: Filter; label: string }[] = [\n  { value: \"all\", label: \"All\" },\n  { value: \"running\", label: \"Running\" },\n  { value: \"review\", label: \"Review\" },\n  { value: \"failed\", label: \"Failed\" },\n  { value: \"done\", label: \"Done\" },\n];\n\nconst outcomeStep: Record<Exclude<RunStatus, \"running\">, string> = {\n  review: \"Opened a pull request for review\",\n  done: \"Finished, all checks green\",\n  failed: \"Stopped: tests failed after 3 tries\",\n};\n\nconst emptyCopy: Record<Filter, { title: string; body: string }> = {\n  all: { title: \"No runs yet\", body: \"Start an agent and its run shows up here with a live clock.\" },\n  running: { title: \"Nothing running\", body: \"Every agent is idle. New runs appear here as they start.\" },\n  review: { title: \"You're all caught up\", body: \"Nothing is waiting on your review.\" },\n  failed: { title: \"No failed runs\", body: \"Every run finished or is still going.\" },\n  done: { title: \"Nothing finished yet\", body: \"Completed runs land here with their last steps.\" },\n};\n\nconst press = \"transition-[scale,box-shadow,background-color] duration-150 ease-out active:scale-[0.97]\";\n\nconst nextOutcome: Exclude<RunStatus, \"running\">[] = [\"review\", \"done\", \"failed\", \"review\", \"done\"];\n\nfunction clock(seconds: number) {\n  const h = Math.floor(seconds / 3600);\n  const m = Math.floor((seconds % 3600) / 60);\n  const s = seconds % 60;\n  const mm = h ? String(m).padStart(2, \"0\") : String(m);\n  return `${h ? `${h}:` : \"\"}${mm}:${String(s).padStart(2, \"0\")}`;\n}\n\nexport function RunList({ tasks: initial, autoplay = true, title = \"Background agents\", onOpen, className }: RunListProps) {\n  const reduce = useSafeReducedMotion();\n  const listId = useId();\n  const [tasks, setTasks] = useState(initial);\n  const [filter, setFilter] = useState<Filter>(\"all\");\n  const [openId, setOpenId] = useState<string | null>(null);\n  const openRef = useRef(openId);\n  useEffect(() => {\n    openRef.current = openId;\n  }, [openId]);\n\n  useEffect(() => {\n    const tick = setInterval(() => {\n      setTasks((ts) => ts.map((t) => (t.status === \"running\" ? { ...t, elapsed: t.elapsed + 1 } : t)));\n    }, 1000);\n    return () => clearInterval(tick);\n  }, []);\n\n  useEffect(() => {\n    if (!autoplay) return;\n    let turn = 0;\n    const loop = setInterval(() => {\n      turn++;\n      setTasks((ts) => {\n        const free = ts.filter((t) => t.id !== openRef.current);\n        const running = free.filter((t) => t.status === \"running\");\n        if (running.length >= 2) {\n          const pick = running.reduce((a, b) => (b.elapsed > a.elapsed ? b : a));\n          const outcome = pick.outcome ?? \"done\";\n          return ts.map((t) =>\n            t.id === pick.id ? { ...t, status: outcome, steps: [...t.steps, outcomeStep[outcome]].slice(-4) } : t,\n          );\n        }\n        const idle = free.filter((t) => t.status === \"done\");\n        if (!idle.length) return ts;\n        const pick = idle[turn % idle.length];\n        return ts.map((t) =>\n          t.id === pick.id\n            ? {\n                ...t,\n                status: \"running\",\n                elapsed: 0,\n                outcome: nextOutcome[turn % nextOutcome.length],\n                steps: [\"Picked up a new run\", \"Reading the repository\"],\n              }\n            : t,\n        );\n      });\n    }, 6500);\n    return () => clearInterval(loop);\n  }, [autoplay]);\n\n  const counts = useMemo(() => {\n    const c: Record<Filter, number> = { all: tasks.length, running: 0, review: 0, failed: 0, done: 0 };\n    for (const t of tasks) c[t.status]++;\n    return c;\n  }, [tasks]);\n\n  const visible = useMemo(\n    () =>\n      tasks\n        .map((t, i) => ({ t, i }))\n        .filter(({ t }) => filter === \"all\" || t.status === filter)\n        .sort((a, b) => order[a.t.status] - order[b.t.status] || a.i - b.i)\n        .map(({ t }) => t),\n    [tasks, filter],\n  );\n\n  const act = (id: string, status: RunStatus, step: string) =>\n    setTasks((ts) =>\n      ts.map((t) =>\n        t.id === id\n          ? { ...t, status, elapsed: status === \"running\" ? 0 : t.elapsed, steps: [...t.steps, step].slice(-4) }\n          : t,\n      ),\n    );\n\n  const spring = reduce ? { duration: 0 } : { type: \"spring\" as const, stiffness: 420, damping: 36, mass: 0.8 };\n\n  return (\n    <div className={cn(\"w-full max-w-[460px] rounded-2xl border bg-card text-card-foreground shadow-xs/5\", className)}>\n      <div className=\"flex items-center justify-between gap-3 px-4 pt-4\">\n        <div>\n          <h3 className=\"font-medium text-sm\">{title}</h3>\n          <p className=\"flex items-center gap-1.5 text-muted-foreground text-xs\">\n            <span\n              aria-hidden\n              className={cn(\n                \"size-1.5 rounded-full transition-colors duration-300\",\n                counts.running ? \"bg-info\" : \"bg-muted-foreground/40\",\n              )}\n            />\n            <Rolling value={counts.running} /> running · <Rolling value={counts.review} /> waiting on you\n          </p>\n        </div>\n      </div>\n\n      <div className=\"px-4 pt-3\">\n        <FilterTabs\n          controls={listId}\n          counts={counts}\n          onChange={(f) => {\n            setFilter(f);\n            setOpenId(null);\n          }}\n          value={filter}\n        />\n      </div>\n\n      <LayoutGroup>\n        <motion.ul className=\"relative flex flex-col gap-1 p-2\" id={listId} layout transition={spring}>\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            {visible.map((task) => (\n              <RunRow\n                key={task.id}\n                onAct={act}\n                onOpen={onOpen}\n                onToggle={() => setOpenId((o) => (o === task.id ? null : task.id))}\n                open={openId === task.id}\n                reduce={!!reduce}\n                spring={spring}\n                task={task}\n              />\n            ))}\n          </AnimatePresence>\n          {visible.length === 0 && (\n            <motion.li\n              animate={{ opacity: 1 }}\n              className=\"flex flex-col items-center gap-1 px-6 py-8 text-center\"\n              initial={{ opacity: 0 }}\n              layout\n              transition={{ duration: reduce ? 0 : 0.2, ease: \"easeOut\" }}\n            >\n              <span className=\"font-medium text-sm\">{emptyCopy[filter].title}</span>\n              <span className=\"text-pretty text-muted-foreground text-xs\">{emptyCopy[filter].body}</span>\n            </motion.li>\n          )}\n        </motion.ul>\n      </LayoutGroup>\n    </div>\n  );\n}\n\nfunction RunRow({\n  task,\n  open,\n  onToggle,\n  onAct,\n  onOpen,\n  reduce,\n  spring,\n}: {\n  task: RunListItem;\n  open: boolean;\n  onToggle: () => void;\n  onAct: (id: string, status: RunStatus, step: string) => void;\n  onOpen?: (task: RunListItem) => void;\n  reduce: boolean;\n  spring: object;\n}) {\n  const meta = statusMeta[task.status];\n  const panelId = useId();\n  return (\n    <motion.li\n      animate={{ opacity: 1, scale: 1 }}\n      className={cn(\"rounded-lg transition-colors duration-150\", open ? \"bg-muted\" : \"hover:bg-muted/60\")}\n      exit={{ opacity: 0, scale: 0.96 }}\n      initial={{ opacity: 0, scale: 0.96 }}\n      layout\n      transition={spring}\n    >\n      <motion.button\n        aria-controls={panelId}\n        aria-expanded={open}\n        className=\"flex w-full cursor-pointer items-start gap-3 rounded-lg px-2.5 py-2.5 text-left outline-none transition-[scale] duration-150 ease-out focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.985]\"\n        layout=\"position\"\n        onClick={onToggle}\n        transition={spring}\n        type=\"button\"\n      >\n        <StatusIcon reduce={reduce} status={task.status} />\n        <span className=\"min-w-0 flex-1\">\n          <span className=\"block truncate font-medium text-sm\">{task.title}</span>\n          <span className=\"block truncate text-muted-foreground text-xs\">\n            {task.agent}\n            {task.repo && <span className=\"font-mono\"> · {task.repo}</span>}\n          </span>\n        </span>\n        <span className=\"flex shrink-0 flex-col items-end gap-1\">\n          <span\n            className={cn(\n              \"font-mono text-xs tabular-nums\",\n              task.status === \"running\" ? \"text-foreground\" : \"text-muted-foreground\",\n            )}\n          >\n            {clock(task.elapsed)}\n          </span>\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n              className=\"max-sm:hidden\"\n              exit={{ opacity: 0, y: -4, filter: \"blur(2px)\" }}\n              initial={{ opacity: 0, y: 4, filter: \"blur(2px)\" }}\n              key={task.status}\n              transition={{ duration: reduce ? 0 : 0.2, ease: [0.23, 1, 0.32, 1] }}\n            >\n              <Badge size=\"sm\" variant={meta.badge}>\n                {meta.label}\n              </Badge>\n            </motion.span>\n          </AnimatePresence>\n        </span>\n        <ChevronDownIcon\n          aria-hidden\n          className={cn(\n            \"mt-1 size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 ease-out motion-reduce:transition-none\",\n            open && \"rotate-180\",\n          )}\n        />\n      </motion.button>\n\n      <AnimatePresence initial={false}>\n        {open && (\n          <motion.div\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"overflow-hidden\"\n            exit={{ height: 0, opacity: 0 }}\n            id={panelId}\n            initial={{ height: 0, opacity: 0 }}\n            transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 380, damping: 34 }}\n          >\n            <div className=\"px-2.5 pb-3 pl-10\">\n              <ol className=\"relative flex flex-col gap-1.5 border-border border-l pl-3.5\">\n                {task.steps.slice(-3).map((step, i, all) => {\n                  const last = i === all.length - 1;\n                  return (\n                    <motion.li\n                      animate={{ opacity: 1, x: 0 }}\n                      className=\"relative text-xs\"\n                      initial={{ opacity: 0, x: reduce ? 0 : -4 }}\n                      key={`${step}-${i}`}\n                      transition={{ duration: 0.2, ease: [0.23, 1, 0.32, 1], delay: reduce ? 0 : 0.04 + i * 0.03 }}\n                    >\n                      <span\n                        className={cn(\n                          \"absolute top-1.5 -left-[18px] size-1.5 rounded-full ring-2 ring-muted\",\n                          last && task.status === \"running\" ? \"bg-info\" : \"bg-muted-foreground/50\",\n                        )}\n                      />\n                      <span className={last ? \"text-foreground\" : \"text-muted-foreground\"}>{step}</span>\n                      {last && task.status === \"running\" && !reduce && (\n                        <span className=\"ml-1 inline-flex gap-0.5 align-middle\">\n                          {[0, 1, 2].map((d) => (\n                            <motion.span\n                              animate={{ opacity: [0.25, 0.9, 0.25] }}\n                              className=\"size-1 rounded-full bg-info\"\n                              key={d}\n                              transition={{ duration: 1.6, repeat: Infinity, ease: \"easeInOut\", delay: d * 0.2 }}\n                            />\n                          ))}\n                        </span>\n                      )}\n                    </motion.li>\n                  );\n                })}\n              </ol>\n              <div className=\"mt-3 flex flex-wrap gap-1.5\">\n                {task.status === \"review\" && (\n                  <>\n                    <Button onClick={() => onAct(task.id, \"done\", \"Approved by you\")} className={press} size=\"xs\">\n                      <CheckIcon />\n                      Approve\n                    </Button>\n                    <Button\n                      onClick={() => onAct(task.id, \"failed\", \"Rejected by you\")}\n                      className={press} size=\"xs\"\n                      variant=\"destructive-outline\"\n                    >\n                      <XIcon />\n                      Reject\n                    </Button>\n                  </>\n                )}\n                {task.status === \"failed\" && (\n                  <Button onClick={() => onAct(task.id, \"running\", \"Retrying from the last checkpoint\")} className={press} size=\"xs\">\n                    <RotateCwIcon />\n                    Retry\n                  </Button>\n                )}\n                {task.status === \"running\" && (\n                  <Button onClick={() => onAct(task.id, \"failed\", \"Stopped by you\")} className={press} size=\"xs\" variant=\"outline\">\n                    <SquareIcon />\n                    Stop\n                  </Button>\n                )}\n                <Button onClick={() => onOpen?.(task)} className={press} size=\"xs\" variant=\"ghost\">\n                  <ExternalLinkIcon />\n                  Open\n                </Button>\n              </div>\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </motion.li>\n  );\n}\n\nfunction StatusIcon({ status, reduce }: { status: RunStatus; reduce: boolean }) {\n  return (\n    <span className=\"relative mt-0.5 flex size-4 shrink-0 items-center justify-center\">\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          animate={{ scale: 1, opacity: 1, filter: \"blur(0px)\" }}\n          className=\"flex\"\n          exit={{ scale: reduce ? 1 : 0.8, opacity: 0, filter: \"blur(2px)\" }}\n          initial={{ scale: reduce ? 1 : 0.8, opacity: 0, filter: \"blur(2px)\" }}\n          key={status}\n          transition={{ duration: reduce ? 0.1 : 0.2, ease: [0.23, 1, 0.32, 1] }}\n        >\n          {status === \"running\" && (\n            <svg aria-hidden className={cn(\"size-4 text-info\", !reduce && \"animate-spin\")} viewBox=\"0 0 16 16\">\n              <circle cx=\"8\" cy=\"8\" fill=\"none\" opacity=\"0.2\" r=\"6\" stroke=\"currentColor\" strokeWidth=\"2\" />\n              <path d=\"M8 2a6 6 0 0 1 6 6\" fill=\"none\" stroke=\"currentColor\" strokeLinecap=\"round\" strokeWidth=\"2\" />\n            </svg>\n          )}\n          {status === \"review\" && <CircleAlertIcon className=\"size-4 text-warning\" />}\n          {status === \"failed\" && <CircleXIcon className=\"size-4 text-destructive\" />}\n          {status === \"done\" && <CircleCheckIcon className=\"size-4 text-success\" />}\n        </motion.span>\n      </AnimatePresence>\n      <span className=\"sr-only\">{statusMeta[status].label}</span>\n    </span>\n  );\n}\n\nfunction FilterTabs({\n  value,\n  onChange,\n  counts,\n  controls,\n}: {\n  value: Filter;\n  onChange: (f: Filter) => void;\n  counts: Record<Filter, number>;\n  controls: string;\n}) {\n  return (\n    <Tabs onValueChange={(v) => onChange(v as Filter)} value={value}>\n      <TabsList aria-label=\"Filter tasks\" className=\"w-full text-muted-foreground\" size=\"sm\">\n        {filters.map((f) => (\n          <TabsTab\n            aria-controls={controls}\n            className=\"min-w-0 gap-1 px-1.5 text-xs sm:px-2 sm:text-xs\"\n            key={f.value}\n            value={f.value}\n          >\n            {f.label}\n            <Rolling\n              className={cn(\"transition-colors\", f.value === value ? \"text-muted-foreground\" : \"text-muted-foreground/56\")}\n              value={counts[f.value]}\n            />\n          </TabsTab>\n        ))}\n      </TabsList>\n    </Tabs>\n  );\n}\n\nconst roll = {\n  enter: (d: number) => ({ y: `${d * 100}%`, opacity: 0 }),\n  center: { y: \"0%\", opacity: 1 },\n  exit: (d: number) => ({ y: `${-d * 100}%`, opacity: 0 }),\n};\n\nfunction Rolling({ value, className }: { value: number; className?: string }) {\n  const reduce = useSafeReducedMotion();\n  const prev = useRef(value);\n  const dir = reduce ? 0 : value >= prev.current ? 1 : -1;\n  useEffect(() => {\n    prev.current = value;\n  }, [value]);\n  return (\n    <span className={cn(\"relative inline-flex overflow-hidden tabular-nums\", className)}>\n      <AnimatePresence custom={dir} initial={false} mode=\"popLayout\">\n        <motion.span\n          animate=\"center\"\n          custom={dir}\n          exit=\"exit\"\n          initial=\"enter\"\n          key={value}\n          transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 420, damping: 30 }}\n          variants={roll}\n        >\n          {value}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n"
    }
  ]
}