{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "batch-jobs",
  "title": "Batch Jobs",
  "description": "A batch inference jobs table — middle-truncated copyable job ids, a stacked done/failed/pending bar per job, spend so far, age and ETA derived from an injected clock, an expandable failed-request preview, a one-shot cancel, partial-result downloads, and four data states.",
  "dependencies": [
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "https://ui.zyeon.ai/r/use-copy-to-clipboard.json",
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/batch-jobs.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  AlertCircle,\n  Ban,\n  Check,\n  ChevronRight,\n  CircleAlert,\n  CircleCheck,\n  CircleDashed,\n  CircleX,\n  Copy,\n  CopyX,\n  Download,\n  Hourglass,\n  Layers,\n  LoaderCircle,\n  RefreshCcw,\n  X,\n  type LucideIcon,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useCopyToClipboard } from \"@/hooks/use-copy-to-clipboard\"\nimport type {\n  BatchJobFailure,\n  BatchJobStatus,\n  BatchJobsItem,\n  BatchJobsStatus,\n  Instant,\n} from \"./batch-jobs.contract\"\n\n/** Columns in the ready table. Only used for the detail row's colSpan. */\nconst COLUMN_COUNT = 6\n\n/**\n * Seven statuses, seven SHAPES plus a written word. Colour alone disappears in a\n * monochrome theme and for a colour-blind reader — and \"cancelled\" vs \"failed\"\n * vs \"expired\" is exactly the distinction a hue would blur, while being exactly\n * the distinction that decides whether you re-run the batch or download it.\n */\nconst STATUS_META: Record<BatchJobStatus, { word: string; Icon: LucideIcon; iconClassName: string }> = {\n  validating: { word: \"Validating\", Icon: CircleDashed, iconClassName: \"text-muted-foreground\" },\n  running: {\n    word: \"Running\",\n    Icon: LoaderCircle,\n    iconClassName: \"animate-spin text-primary motion-reduce:animate-none\",\n  },\n  cancelling: {\n    word: \"Cancelling\",\n    Icon: Ban,\n    iconClassName: \"animate-pulse text-muted-foreground motion-reduce:animate-none\",\n  },\n  completed: { word: \"Completed\", Icon: CircleCheck, iconClassName: \"text-primary\" },\n  cancelled: { word: \"Cancelled\", Icon: CircleX, iconClassName: \"text-muted-foreground\" },\n  failed: { word: \"Failed\", Icon: CircleAlert, iconClassName: \"text-destructive\" },\n  expired: { word: \"Expired\", Icon: Hourglass, iconClassName: \"text-muted-foreground\" },\n}\n\n/** Work is still moving: counters change, and money is still being spent. */\nfunction isActive(status: BatchJobStatus): boolean {\n  return status === \"validating\" || status === \"running\" || status === \"cancelling\"\n}\n\n/** The job will not move again. Its results — even partial ones — are final. */\nfunction isTerminal(status: BatchJobStatus): boolean {\n  return !isActive(status)\n}\n\nconst countFormatter = new Intl.NumberFormat(\"en-US\")\nconst usdFormatter = new Intl.NumberFormat(\"en-US\", {\n  style: \"currency\",\n  currency: \"USD\",\n  minimumFractionDigits: 2,\n  maximumFractionDigits: 2,\n})\n/**\n * Explicit `timeZone` on purpose: an absolute timestamp formatted in the ambient\n * zone renders one string on the server and another in the browser, which is a\n * hydration mismatch that only shows up for readers outside the server's zone.\n */\nconst absoluteFormatter = new Intl.DateTimeFormat(\"en-US\", {\n  month: \"short\",\n  day: \"numeric\",\n  hour: \"2-digit\",\n  minute: \"2-digit\",\n  hour12: false,\n  timeZone: \"UTC\",\n})\n\n/**\n * Money, with the sub-cent case spelled out. A batch that has processed nine\n * requests genuinely costs $0.004, and rounding that to \"$0.00\" reads as \"free\"\n * — the one reading a spend table must never produce.\n */\nfunction defaultFormatCost(usd: number): string {\n  if (!Number.isFinite(usd) || usd < 0) return \"—\"\n  if (usd === 0) return usdFormatter.format(0)\n  if (usd < 0.01) return `<${usdFormatter.format(0.01)}`\n  return usdFormatter.format(usd)\n}\n\n/**\n * A COARSE duration, unlike a trace panel's millisecond column: batch jobs live\n * for hours, and \"3 h 12 m ago\" is the resolution a human acts on. Seconds only\n * survive under a minute, where they are the whole story (\"ETA 40 s\").\n */\nfunction defaultFormatDuration(ms: number): string {\n  if (!Number.isFinite(ms) || ms < 0) return \"—\"\n  const seconds = Math.floor(ms / 1000)\n  if (seconds < 60) return `${seconds}s`\n  const minutes = Math.floor(seconds / 60)\n  if (minutes < 60) return `${minutes}m`\n  const hours = Math.floor(minutes / 60)\n  if (hours < 24) {\n    const rest = minutes % 60\n    return rest === 0 ? `${hours}h` : `${hours}h ${rest}m`\n  }\n  const days = Math.floor(hours / 24)\n  const restHours = hours % 24\n  return restHours === 0 ? `${days}d` : `${days}d ${restHours}h`\n}\n\nfunction defaultFormatAbsolute(epochMs: number): string {\n  if (!Number.isFinite(epochMs)) return \"—\"\n  return `${absoluteFormatter.format(epochMs)} UTC`\n}\n\nfunction toEpoch(value: Instant): number {\n  if (typeof value === \"number\") return value\n  // Date.parse, not `new Date(...)`: a pure call, safe to run during render.\n  if (typeof value === \"string\") return Date.parse(value)\n  return value.getTime()\n}\n\n/** An unparseable instant collapses to `undefined` once, here, so no branch below has to think about NaN. */\nfunction toEpochOrUndefined(value: Instant | undefined): number | undefined {\n  if (value === undefined) return undefined\n  const epoch = toEpoch(value)\n  return Number.isFinite(epoch) ? epoch : undefined\n}\n\n/**\n * Middle truncation, never a CSS ellipsis. Provider ids share a long prefix\n * (`batch_req_01JQ…`) and differ in the TAIL, so cutting the end — which is what\n * `text-overflow: ellipsis` does — turns every row into the same string.\n */\nfunction truncateMiddle(value: string, head: number, tail: number): string {\n  if (value.length <= head + tail + 1) return value\n  return `${value.slice(0, head)}…${value.slice(value.length - tail)}`\n}\n\n/** A count from a flaky worker: NaN, -1 and 3.7 all have to become an integer >= 0. */\nfunction clampCount(value: number): number {\n  if (!Number.isFinite(value) || value <= 0) return 0\n  return Math.floor(value)\n}\n\ninterface JobCounts {\n  /** The DENOMINATOR actually used — never smaller than what has already landed. */\n  total: number\n  /** What the job SAID it contained. Kept so a widened denominator can explain itself. */\n  declared: number\n  done: number\n  failed: number\n  pending: number\n  donePct: number\n  failedPct: number\n  processedPct: number\n  /** True when `done + failed` exceeded the declared `total` and it had to widen. */\n  overCounted: boolean\n}\n\n/**\n * The bar's arithmetic, in one pure place. A provider that reports 2,807\n * processed requests for a 2,800-request file (retries counted twice) must widen\n * the DENOMINATOR, not paint a segment past the end of the track — an overflowing\n * bar is a rendering bug the reader blames on their own data. The widened figure\n * then says what it grew from, so the reader isn't left auditing their own file.\n */\nfunction resolveCounts(job: BatchJobsItem): JobCounts {\n  const done = clampCount(job.done)\n  const failed = clampCount(job.failed)\n  const declared = clampCount(job.total)\n  const processed = done + failed\n  const total = Math.max(declared, processed)\n  const pending = Math.max(0, total - processed)\n  const ratio = (value: number) => (total === 0 ? 0 : (value / total) * 100)\n  return {\n    total,\n    declared,\n    done,\n    failed,\n    pending,\n    donePct: ratio(done),\n    failedPct: ratio(failed),\n    processedPct: total === 0 ? 0 : Math.round((processed / total) * 100),\n    overCounted: processed > declared,\n  }\n}\n\n/**\n * Whether a horizontally scrollable box is ACTUALLY scrolling right now. A scroll\n * container that a keyboard cannot reach is a WCAG failure, but adding a\n * permanent tab stop for a table that fits is noise — so the region role and the\n * tab stop appear only while there is something to scroll to.\n */\nfunction useHorizontalOverflow(node: HTMLElement | null): boolean {\n  // useSyncExternalStore, not useEffect + setState: the browser's layout IS the\n  // external store here. Subscribing to it instead of mirroring it into state\n  // removes the extra render pass — and the server snapshot is a flat `false`,\n  // so hydration can never disagree about an attribute nobody can measure yet.\n  const subscribe = React.useCallback(\n    (onStoreChange: () => void) => {\n      if (!node || typeof ResizeObserver === \"undefined\") return () => undefined\n      const observer = new ResizeObserver(onStoreChange)\n      observer.observe(node)\n      // The inner table too: adding a job changes the CONTENT width without\n      // resizing the box that holds it.\n      const inner = node.firstElementChild\n      if (inner) observer.observe(inner)\n      return () => observer.disconnect()\n    },\n    [node],\n  )\n  const getSnapshot = React.useCallback(() => (node ? node.scrollWidth - node.clientWidth > 1 : false), [node])\n  return React.useSyncExternalStore(subscribe, getSnapshot, () => false)\n}\n\n/* -------------------------------------------------------------------- pieces */\n\nfunction StatusMark({ status }: { status: BatchJobStatus }) {\n  const { Icon, iconClassName } = STATUS_META[status]\n  return (\n    <span className=\"flex size-4 shrink-0 items-center justify-center\">\n      <Icon aria-hidden=\"true\" className={cn(\"size-4\", iconClassName)} />\n    </span>\n  )\n}\n\n/**\n * The id IS the button. A separate copy icon next to a `title=` tooltip would\n * need two controls and a hover to reveal the full value; here the visible text\n * is middle-truncated, the accessible name carries the id in full, and the click\n * writes the full id — never the truncated preview.\n */\nfunction JobIdButton({\n  headChars,\n  jobId,\n  onAnnounce,\n  tailChars,\n}: {\n  headChars: number\n  jobId: string\n  onAnnounce: (message: string) => void\n  tailChars: number\n}) {\n  const { copied, copy, error } = useCopyToClipboard()\n  return (\n    <button\n      aria-label={`Copy job id ${jobId}`}\n      className=\"inline-flex min-w-0 max-w-full cursor-pointer items-center gap-1.5 rounded-md px-1 py-0.5 text-left font-mono text-xs transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\"\n      data-copy-id=\"\"\n      onClick={() => {\n        void copy(jobId).then(ok => {\n          onAnnounce(ok ? `Job id ${jobId} copied to clipboard` : `Couldn't copy job id ${jobId}`)\n        })\n      }}\n      type=\"button\"\n    >\n      <span aria-hidden=\"true\" className=\"min-w-0 truncate text-foreground\">\n        {truncateMiddle(jobId, headChars, tailChars)}\n      </span>\n      {/* Every outcome is visible: a silent no-op on a blocked clipboard reads as\n          a broken control, so a failure flips to a destructive icon. */}\n      {error ? (\n        <CopyX aria-hidden=\"true\" className=\"size-3 shrink-0 text-destructive\" />\n      ) : copied ? (\n        <Check aria-hidden=\"true\" className=\"size-3 shrink-0 text-primary\" />\n      ) : (\n        <Copy aria-hidden=\"true\" className=\"size-3 shrink-0 text-muted-foreground\" />\n      )}\n    </button>\n  )\n}\n\n/**\n * Done / failed / pending in ONE track, because the three numbers are parts of\n * the same whole and stacking them is the only way the eye reads \"mostly done,\n * a sliver failed\" without arithmetic. The visible figures are aria-hidden and\n * the meaning is carried by `aria-valuetext`: \"1,842 · 12 · 2,800\" is a riddle\n * when read out loud.\n */\nfunction RequestBar({ counts }: { counts: JobCounts }) {\n  // A widened denominator explains itself. Silently showing \"of 2,807\" for a file\n  // the reader submitted with 2,800 lines in it reads as a bug in the table.\n  const overCountNote = counts.overCounted\n    ? `, ${countFormatter.format(counts.total - counts.declared)} more than the ${countFormatter.format(counts.declared)} submitted`\n    : \"\"\n  const valueText =\n    counts.total === 0\n      ? \"No requests in this job\"\n      : `${countFormatter.format(counts.done)} of ${countFormatter.format(counts.total)} requests done, ` +\n        `${countFormatter.format(counts.failed)} failed, ${countFormatter.format(counts.pending)} pending${overCountNote}`\n\n  return (\n    <div className=\"flex min-w-0 flex-col gap-1.5\">\n      <div\n        // A progressbar with no accessible name is announced as a bare \"progress\n        // bar\"; the row it sits in supplies the job, so the column word is the name.\n        aria-label=\"Requests\"\n        aria-valuemax={100}\n        aria-valuemin={0}\n        aria-valuenow={counts.processedPct}\n        aria-valuetext={valueText}\n        className=\"flex h-1.5 w-full min-w-0 overflow-hidden rounded-full bg-muted\"\n        data-request-bar=\"\"\n        role=\"progressbar\"\n      >\n        <div\n          className=\"h-full bg-primary transition-[width] duration-500 motion-reduce:transition-none\"\n          style={{ width: `${counts.donePct}%` }}\n        />\n        <div\n          className=\"h-full bg-destructive transition-[width] duration-500 motion-reduce:transition-none\"\n          style={{ width: `${counts.failedPct}%` }}\n        />\n      </div>\n      <span aria-hidden=\"true\" className=\"flex flex-wrap items-baseline gap-x-1.5 text-xs tabular-nums\">\n        <span className=\"font-medium text-foreground\">{countFormatter.format(counts.done)}</span>\n        <span className=\"text-muted-foreground\">done</span>\n        <span className={cn(\"font-medium\", counts.failed > 0 ? \"text-destructive\" : \"text-muted-foreground\")}>\n          {countFormatter.format(counts.failed)}\n        </span>\n        <span className=\"text-muted-foreground\">failed</span>\n        <span className=\"text-muted-foreground\">of {countFormatter.format(counts.total)}</span>\n        {counts.overCounted && (\n          <span className=\"text-muted-foreground\" data-over-counted=\"\">\n            ({countFormatter.format(counts.declared)} submitted)\n          </span>\n        )}\n      </span>\n    </div>\n  )\n}\n\nfunction FailureSample({ failure }: { failure: BatchJobFailure }) {\n  return (\n    <li className=\"flex min-w-0 flex-col gap-1 rounded-md border border-destructive/40 bg-destructive/5 p-2\">\n      <span className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 font-mono text-[11px]\">\n        <span className=\"min-w-0 wrap-anywhere text-foreground\">{failure.customId}</span>\n        {failure.code && (\n          <span className=\"shrink-0 rounded border px-1 py-px text-muted-foreground\">{failure.code}</span>\n        )}\n        {failure.httpStatus !== undefined && (\n          <span className=\"shrink-0 text-muted-foreground\">HTTP {failure.httpStatus}</span>\n        )}\n      </span>\n      {/* No max-height and no line clamp: the tail of a provider error is usually\n          the part that names the cause. */}\n      <span className=\"min-w-0 whitespace-pre-wrap wrap-anywhere text-xs text-destructive\">{failure.message}</span>\n    </li>\n  )\n}\n\nfunction SkeletonRow() {\n  return (\n    <div aria-hidden=\"true\" className=\"flex items-center gap-3 border-t p-3\">\n      <div className=\"h-3 w-40 animate-pulse rounded bg-muted motion-reduce:animate-none\" />\n      <div className=\"h-1.5 flex-1 animate-pulse rounded-full bg-muted motion-reduce:animate-none\" />\n      <div className=\"h-3 w-16 shrink-0 animate-pulse rounded bg-muted motion-reduce:animate-none\" />\n      <div className=\"h-3 w-12 shrink-0 animate-pulse rounded bg-muted motion-reduce:animate-none\" />\n    </div>\n  )\n}\n\n/* ---------------------------------------------------------------- component */\n\nexport interface BatchJobsProps extends React.HTMLAttributes<HTMLElement> {\n  /** The jobs to render, in the order you want them. The table never sorts behind your back. */\n  items: BatchJobsItem[]\n  /** Envelope state — whether there is a list at all. Not a job's own `status`. */\n  status: BatchJobsStatus\n  /**\n   * \"Now\", injected. Turns `createdAt` into an age (\"3h 12m ago\") and `eta` into\n   * a remaining time (\"ETA 22m\"). Omit it and both render as absolute UTC\n   * instants instead — the table owns no clock, which is what keeps SSR, a poll\n   * replay and a screenshot identical. Tick it once per second in your app if you\n   * want the numbers to run.\n   */\n  now?: Instant\n  /**\n   * Job ids whose failure panel starts open. Read once, on mount: later changes\n   * are ignored so a poll that re-sends the same list can't slam a panel shut\n   * while it is being read. Nothing auto-opens — a table that expands every\n   * failing row is a table you have to collapse before you can scan it.\n   */\n  defaultOpenIds?: readonly string[]\n  /** Called at most once per job per status when the human asks to stop it. */\n  onCancel?: (jobId: string) => void\n  /** Called when the human asks for the (possibly partial) result file. */\n  onDownload?: (jobId: string) => void\n  /** Renders \"Try again\" in the `status=\"error\"` branch; omit it to hide the affordance. */\n  onRetry?: () => void\n  /** Failed requests drawn in an open panel before an explicit \"N more\" note. Clamped to >= 1. */\n  failureSampleLimit?: number\n  /** Leading characters kept when middle-truncating a job id. Clamped to >= 4. */\n  idHeadChars?: number\n  /** Trailing characters kept when middle-truncating a job id. Clamped to >= 3. */\n  idTailChars?: number\n  /** Override the money wording (another currency, more precision, a credits unit). */\n  formatCost?: (usd: number) => string\n  /** Override the age / ETA wording. Receives a positive duration in ms. */\n  formatDuration?: (ms: number) => string\n  /** Override the absolute-instant wording used when `now` is not supplied. */\n  formatAbsolute?: (epochMs: number) => string\n  /** Replaces the default `status=\"empty\"` body. */\n  emptyState?: React.ReactNode\n  /** Message shown in the `status=\"error\"` branch. */\n  errorMessage?: string\n  /** Hides the aggregate header line (job count, active count, requests, spend). */\n  showSummary?: boolean\n  /** Accessible name for the region, and the header title. */\n  label?: string\n}\n\n/**\n * A table of batch inference jobs: copyable ids, a stacked done/failed/pending\n * bar per job, spend so far, age and ETA derived from an injected clock, an\n * expandable preview of failed requests, and actions that follow the job's own\n * lifecycle — cancel while it runs, download while there is anything to download.\n */\nexport const BatchJobs = React.forwardRef<HTMLElement, BatchJobsProps>(\n  (\n    {\n      items,\n      status,\n      now,\n      defaultOpenIds,\n      onCancel,\n      onDownload,\n      onRetry,\n      failureSampleLimit = 3,\n      idHeadChars = 14,\n      idTailChars = 6,\n      formatCost = defaultFormatCost,\n      formatDuration = defaultFormatDuration,\n      formatAbsolute = defaultFormatAbsolute,\n      emptyState,\n      errorMessage = \"Couldn't reach the batch API.\",\n      showSummary = true,\n      label = \"Batch jobs\",\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const uid = React.useId()\n    const [scrollNode, setScrollNode] = React.useState<HTMLDivElement | null>(null)\n    const scrollable = useHorizontalOverflow(scrollNode)\n\n    // Uncontrolled default, read once. A later `defaultOpenIds` identity change\n    // (an inline array literal in a polling parent) must not reopen or close\n    // anything the reader has since touched.\n    const [openIds, setOpenIds] = React.useState<Record<string, boolean>>(() => {\n      const seed: Record<string, boolean> = {}\n      for (const id of defaultOpenIds ?? []) seed[id] = true\n      return seed\n    })\n\n    /**\n     * Which job has an unanswered cancel request, keyed to the status it was\n     * fired FROM. That key is what makes the marker expire by itself: the moment\n     * the consumer moves the job on (`running` → `cancelling`), it stops matching\n     * and the pending affordance lifts — no effect, no timer, no reset callback.\n     */\n    const [cancelRequests, setCancelRequests] = React.useState<Record<string, BatchJobStatus>>({})\n    /**\n     * The one-shot lock. It lives in a REF, not in state: five clicks dispatched\n     * inside a single task all read the same stale state, so a state-only guard\n     * would let four of them through and fire four cancels for one job.\n     * Written only ever from an event handler.\n     */\n    const firedRef = React.useRef<Record<string, BatchJobStatus>>({})\n    const [announcement, setAnnouncement] = React.useState(\"\")\n\n    const sampleLimit = Math.max(1, Math.floor(Number.isFinite(failureSampleLimit) ? failureSampleLimit : 3))\n    const headChars = Math.max(4, Math.floor(Number.isFinite(idHeadChars) ? idHeadChars : 14))\n    const tailChars = Math.max(3, Math.floor(Number.isFinite(idTailChars) ? idTailChars : 6))\n\n    const nowMs = toEpochOrUndefined(now)\n\n    const totals = React.useMemo(() => {\n      let requests = 0\n      let failed = 0\n      let cost = 0\n      let active = 0\n      for (const job of items) {\n        const counts = resolveCounts(job)\n        requests += counts.total\n        failed += counts.failed\n        cost += Number.isFinite(job.costUsd) && job.costUsd > 0 ? job.costUsd : 0\n        if (isActive(job.status)) active += 1\n      }\n      return { active, cost, failed, requests }\n    }, [items])\n\n    const requestCancel = (job: BatchJobsItem) => {\n      if (firedRef.current[job.id] === job.status) return\n      firedRef.current[job.id] = job.status\n      setCancelRequests(current => ({ ...current, [job.id]: job.status }))\n      setAnnouncement(`Cancel requested for job ${job.id}`)\n      onCancel?.(job.id)\n    }\n\n    const requestDownload = (job: BatchJobsItem) => {\n      setAnnouncement(`Preparing the result file for job ${job.id}`)\n      onDownload?.(job.id)\n    }\n\n    const rootClass = cn(\"w-full min-w-0 rounded-lg border bg-card text-sm\", className)\n\n    /* ------------------------------------------------------------ envelopes */\n\n    if (status === \"loading\") {\n      return (\n        <section aria-busy=\"true\" aria-label={label} className={rootClass} ref={ref} {...props}>\n          <span className=\"sr-only\" role=\"status\">\n            Loading batch jobs\n          </span>\n          <div aria-hidden=\"true\" className=\"flex items-center gap-2 p-3\">\n            <div className=\"h-3 w-24 animate-pulse rounded bg-muted motion-reduce:animate-none\" />\n            <div className=\"ml-auto h-3 w-32 animate-pulse rounded bg-muted motion-reduce:animate-none\" />\n          </div>\n          <SkeletonRow />\n          <SkeletonRow />\n          <SkeletonRow />\n        </section>\n      )\n    }\n\n    if (status === \"error\") {\n      return (\n        <section aria-label={label} className={rootClass} ref={ref} {...props}>\n          <div className=\"flex flex-col items-start gap-2 p-4\" role=\"alert\">\n            <p className=\"flex items-center gap-2 font-medium\">\n              <AlertCircle aria-hidden=\"true\" className=\"size-4 shrink-0 text-destructive\" />\n              Couldn&apos;t load your batch jobs\n            </p>\n            <p className=\"min-w-0 whitespace-pre-wrap wrap-anywhere text-muted-foreground\">{errorMessage}</p>\n            {onRetry && (\n              <button\n                className=\"inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\"\n                data-retry=\"\"\n                onClick={onRetry}\n                type=\"button\"\n              >\n                <RefreshCcw aria-hidden=\"true\" className=\"size-3.5\" />\n                Try again\n              </button>\n            )}\n          </div>\n        </section>\n      )\n    }\n\n    if (status === \"empty\" || items.length === 0) {\n      return (\n        <section aria-label={label} className={rootClass} ref={ref} {...props}>\n          {emptyState ?? (\n            <div className=\"flex flex-col items-start gap-1 p-4\">\n              <p className=\"flex items-center gap-2 font-medium\">\n                <Layers aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n                No batch jobs yet\n              </p>\n              <p className=\"text-muted-foreground\">\n                Submitted batches show up here with their progress, spend and result files.\n              </p>\n            </div>\n          )}\n        </section>\n      )\n    }\n\n    /* ---------------------------------------------------------------- ready */\n\n    const summaryParts = [\n      `${countFormatter.format(items.length)} ${items.length === 1 ? \"job\" : \"jobs\"}`,\n      `${countFormatter.format(totals.active)} active`,\n      `${countFormatter.format(totals.requests)} requests`,\n      `${formatCost(totals.cost)} spent`,\n    ]\n    if (totals.failed > 0) summaryParts.push(`${countFormatter.format(totals.failed)} failed requests`)\n    const summaryText = summaryParts.join(\" · \")\n\n    return (\n      <section aria-label={label} className={rootClass} ref={ref} {...props}>\n        {/* One persistent live region for the whole table: it is mounted for the\n            entire ready branch, so an action's outcome is announced from an\n            element the screen reader is already watching. */}\n        <p className=\"sr-only\" role=\"status\">\n          {announcement}\n        </p>\n\n        {showSummary && (\n          <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1 border-b p-3\">\n            <span className=\"flex items-center gap-1.5 font-medium\">\n              <Layers aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n              {label}\n            </span>\n            <span className=\"min-w-0 wrap-anywhere text-xs text-muted-foreground tabular-nums\" data-summary=\"\">\n              {summaryText}\n            </span>\n          </div>\n        )}\n\n        <div\n          aria-label={scrollable ? `${label} table, scrollable` : undefined}\n          className=\"w-full min-w-0 overflow-x-auto\"\n          data-scroll-region=\"\"\n          ref={setScrollNode}\n          role={scrollable ? \"region\" : undefined}\n          tabIndex={scrollable ? 0 : undefined}\n        >\n          <table className=\"w-full min-w-[38rem] border-collapse text-left\">\n            <caption className=\"sr-only\">\n              {label} — {summaryText}\n            </caption>\n            <thead>\n              <tr className=\"border-b text-xs text-muted-foreground\">\n                <th className=\"p-3 font-medium\" scope=\"col\">\n                  Job\n                </th>\n                <th className=\"w-[13rem] p-3 font-medium\" scope=\"col\">\n                  Requests\n                </th>\n                <th className=\"p-3 font-medium\" scope=\"col\">\n                  Status\n                </th>\n                <th className=\"p-3 text-right font-medium\" scope=\"col\">\n                  Cost\n                </th>\n                <th className=\"p-3 font-medium\" scope=\"col\">\n                  Created\n                </th>\n                <th className=\"p-3 text-right font-medium\" scope=\"col\">\n                  <span className=\"sr-only\">Actions</span>\n                </th>\n              </tr>\n            </thead>\n            <tbody>\n              {items.map(job => {\n                const counts = resolveCounts(job)\n                const meta = STATUS_META[job.status]\n                const samples = job.failures ?? []\n                const hasDetail = counts.failed > 0 || samples.length > 0 || Boolean(job.error)\n                const isOpen = hasDetail && Boolean(openIds[job.id])\n                const detailId = `${uid}-${job.id}-failures`\n\n                const createdMs = toEpochOrUndefined(job.createdAt)\n                const createdText =\n                  createdMs === undefined\n                    ? \"—\"\n                    : nowMs === undefined\n                      ? formatAbsolute(createdMs)\n                      : `${formatDuration(Math.max(0, nowMs - createdMs))} ago`\n\n                // A terminal job's ETA is noise, and an overdue ETA must never\n                // render as a negative or as a fake \"0s\".\n                const etaMs = toEpochOrUndefined(job.eta)\n                const etaText =\n                  etaMs === undefined || isTerminal(job.status)\n                    ? null\n                    : nowMs === undefined\n                      ? `ETA ${formatAbsolute(etaMs)}`\n                      : etaMs > nowMs\n                        ? `ETA ${formatDuration(etaMs - nowMs)}`\n                        : \"ETA any moment\"\n\n                const cancelPending = cancelRequests[job.id] === job.status\n                const canCancel = Boolean(onCancel) && (job.status === \"validating\" || job.status === \"running\")\n                const partial = job.status !== \"completed\"\n                const canDownload = Boolean(onDownload) && isTerminal(job.status) && counts.done > 0\n\n                return (\n                  <React.Fragment key={job.id}>\n                    <tr className=\"border-t align-top\" data-job-row=\"\" data-status={job.status}>\n                      <td className=\"p-3\">\n                        <div className=\"flex min-w-0 items-start gap-1\">\n                          {hasDetail ? (\n                            <button\n                              aria-controls={isOpen ? detailId : undefined}\n                              aria-expanded={isOpen}\n                              className=\"mt-0.5 shrink-0 cursor-pointer rounded-md p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\"\n                              data-disclosure=\"\"\n                              onClick={() =>\n                                setOpenIds(current => ({ ...current, [job.id]: !current[job.id] }))\n                              }\n                              type=\"button\"\n                            >\n                              <ChevronRight\n                                aria-hidden=\"true\"\n                                className={cn(\n                                  \"size-4 transition-transform duration-150 motion-reduce:transition-none\",\n                                  isOpen && \"rotate-90\",\n                                )}\n                              />\n                              <span className=\"sr-only\">\n                                Failed requests in job {job.id}\n                                {counts.failed > 0 ? `, ${countFormatter.format(counts.failed)} of them` : \"\"}\n                              </span>\n                            </button>\n                          ) : (\n                            <span aria-hidden=\"true\" className=\"mt-0.5 size-5 shrink-0\" />\n                          )}\n                          <div className=\"flex min-w-0 flex-col gap-0.5\">\n                            <JobIdButton\n                              headChars={headChars}\n                              jobId={job.id}\n                              onAnnounce={setAnnouncement}\n                              tailChars={tailChars}\n                            />\n                            {(job.model || job.endpoint) && (\n                              <span className=\"min-w-0 wrap-anywhere px-1 text-xs text-muted-foreground\">\n                                {[job.model, job.endpoint].filter(Boolean).join(\" · \")}\n                              </span>\n                            )}\n                          </div>\n                        </div>\n                      </td>\n\n                      <td className=\"p-3\">\n                        <RequestBar counts={counts} />\n                      </td>\n\n                      <td className=\"p-3\">\n                        <span className=\"flex items-center gap-1.5 whitespace-nowrap text-xs\">\n                          <StatusMark status={job.status} />\n                          {meta.word}\n                        </span>\n                      </td>\n\n                      <td className=\"p-3 text-right\">\n                        <span className=\"block whitespace-nowrap tabular-nums\" data-cost=\"\">\n                          {formatCost(job.costUsd)}\n                        </span>\n                        {isActive(job.status) && (\n                          <span className=\"block text-xs text-muted-foreground\">so far</span>\n                        )}\n                      </td>\n\n                      <td className=\"p-3\">\n                        <span className=\"block whitespace-nowrap text-xs text-muted-foreground tabular-nums\">\n                          {createdText}\n                        </span>\n                        {etaText && (\n                          <span className=\"block whitespace-nowrap text-xs tabular-nums\" data-eta=\"\">\n                            {etaText}\n                          </span>\n                        )}\n                      </td>\n\n                      <td className=\"p-3 text-right\">\n                        <div className=\"flex flex-wrap items-center justify-end gap-1.5\">\n                          {canCancel && (\n                            // aria-disabled, never the native attribute: `disabled`\n                            // blurs the button the instant it flips, dropping focus\n                            // to <body> mid-action and removing the tab stop.\n                            <button\n                              aria-disabled={cancelPending || undefined}\n                              aria-label={`Cancel job ${job.id}`}\n                              className={cn(\n                                \"inline-flex cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border px-2 py-1 text-xs transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\",\n                                cancelPending && \"cursor-default opacity-60 hover:bg-transparent\",\n                              )}\n                              data-cancel=\"\"\n                              onClick={() => requestCancel(job)}\n                              type=\"button\"\n                            >\n                              {cancelPending ? (\n                                <LoaderCircle\n                                  aria-hidden=\"true\"\n                                  className=\"size-3.5 animate-spin motion-reduce:animate-none\"\n                                />\n                              ) : (\n                                <X aria-hidden=\"true\" className=\"size-3.5\" />\n                              )}\n                              {cancelPending ? \"Cancelling…\" : \"Cancel\"}\n                            </button>\n                          )}\n\n                          {job.status === \"cancelling\" && (\n                            <span className=\"inline-flex items-center gap-1.5 whitespace-nowrap text-xs text-muted-foreground\">\n                              <LoaderCircle\n                                aria-hidden=\"true\"\n                                className=\"size-3.5 animate-spin motion-reduce:animate-none\"\n                              />\n                              Stopping\n                            </span>\n                          )}\n\n                          {canDownload && (\n                            <button\n                              aria-label={\n                                partial\n                                  ? `Download partial results for job ${job.id}, ${countFormatter.format(counts.done)} of ${countFormatter.format(counts.total)} requests`\n                                  : `Download results for job ${job.id}`\n                              }\n                              className=\"inline-flex cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\"\n                              data-download=\"\"\n                              onClick={() => requestDownload(job)}\n                              type=\"button\"\n                            >\n                              <Download aria-hidden=\"true\" className=\"size-3.5\" />\n                              {partial ? \"Partial\" : \"Download\"}\n                            </button>\n                          )}\n                        </div>\n                      </td>\n                    </tr>\n\n                    {isOpen && (\n                      // Unmounted when closed, never a zero-height collapse: a\n                      // hidden panel that keeps its buttons in the tab order is an\n                      // invisible keyboard trap.\n                      <tr className=\"bg-muted/30\" data-failures-row=\"\" id={detailId}>\n                        <td className=\"px-3 pb-3\" colSpan={COLUMN_COUNT}>\n                          <div className=\"flex min-w-0 flex-col gap-2\">\n                            {job.error && (\n                              <div className=\"flex min-w-0 flex-col gap-1\">\n                                <p className=\"font-mono text-[11px] uppercase tracking-wide text-muted-foreground\">\n                                  Job error\n                                </p>\n                                <p className=\"min-w-0 whitespace-pre-wrap wrap-anywhere rounded-md border border-destructive/40 bg-destructive/5 p-2 font-mono text-xs text-destructive\">\n                                  {job.error}\n                                </p>\n                              </div>\n                            )}\n\n                            {samples.length > 0 ? (\n                              <>\n                                <p className=\"font-mono text-[11px] uppercase tracking-wide text-muted-foreground\">\n                                  Failed requests\n                                </p>\n                                <ul className=\"flex min-w-0 flex-col gap-1.5\">\n                                  {samples.slice(0, sampleLimit).map(failure => (\n                                    <FailureSample failure={failure} key={failure.customId} />\n                                  ))}\n                                </ul>\n                                {/* Never a silent cut: a sample is labelled as a\n                                    sample, with the number it was taken from. */}\n                                <p className=\"text-xs text-muted-foreground tabular-nums\" data-sample-note=\"\">\n                                  Showing {countFormatter.format(Math.min(sampleLimit, samples.length))} of{\" \"}\n                                  {countFormatter.format(Math.max(counts.failed, samples.length))} failed requests\n                                  {counts.failed > samples.length || samples.length > sampleLimit\n                                    ? \" — the error file has the rest.\"\n                                    : \".\"}\n                                </p>\n                              </>\n                            ) : counts.failed > 0 ? (\n                              <p className=\"text-xs text-muted-foreground\">\n                                {countFormatter.format(counts.failed)} requests failed, but no error samples came\n                                with this job — download the error file to see them.\n                              </p>\n                            ) : null}\n                          </div>\n                        </td>\n                      </tr>\n                    )}\n                  </React.Fragment>\n                )\n              })}\n            </tbody>\n          </table>\n        </div>\n      </section>\n    )\n  },\n)\n\nBatchJobs.displayName = \"BatchJobs\"\n\nexport default BatchJobs\n",
      "type": "registry:ui"
    },
    {
      "path": "src/registry/ui/batch-jobs.contract.ts",
      "content": "import { z } from \"zod\"\n\n/**\n * Lifecycle of ONE batch inference job — a file of N requests handed to a\n * provider's batch endpoint and processed offline over minutes to hours.\n *\n * Seven states, because the interesting ones are the three most tables drop:\n * - `cancelling` is not `cancelled`. The provider accepted the stop but in-flight\n *   requests are still landing, so the counters keep moving while the cancel\n *   affordance is already gone.\n * - `expired` is not `failed`. The completion window elapsed with the job only\n *   partly processed — nothing broke, and whatever finished is still downloadable\n *   and still billed.\n * - `cancelled` and `expired` therefore both carry PARTIAL results. A table that\n *   collapses them into \"failed\" tells you to re-run work you already paid for.\n *\n * Every value here is DATA. The component starts no clock, derives no state from\n * a timer and never mutates the array it is handed: it renders the same on the\n * server, in a replayed poll and in a screenshot fixture.\n */\nexport const BATCH_JOB_STATUSES = [\n  \"validating\",\n  \"running\",\n  \"cancelling\",\n  \"completed\",\n  \"cancelled\",\n  \"failed\",\n  \"expired\",\n] as const\nexport const batchJobStatusSchema = z.enum(BATCH_JOB_STATUSES)\nexport type BatchJobStatus = z.infer<typeof batchJobStatusSchema>\n\n/** ISO string, epoch ms, or a Date — whatever your transport already speaks. */\nexport const instantSchema = z.union([z.string(), z.number(), z.date()])\nexport type Instant = z.infer<typeof instantSchema>\n\n/**\n * ONE failed request inside a batch — a SAMPLE, not the whole error file. A batch\n * with 128 failures usually fails for two or three reasons; three samples answer\n * \"is this my prompt or their rate limiter?\" without downloading 40 MB of JSONL.\n */\nexport const batchJobFailureSchema = z.object({\n  /** The `custom_id` you attached to the request line, so you can find it in your own file. */\n  customId: z.string(),\n  /** Provider error code, e.g. `rate_limit_exceeded`, `context_length_exceeded`. */\n  code: z.string().optional(),\n  /** HTTP status of the individual request, when the provider reports one. */\n  httpStatus: z.number().int().optional(),\n  /** Human-readable failure text. Rendered in full: wrapped, never clipped. */\n  message: z.string(),\n})\nexport type BatchJobFailure = z.infer<typeof batchJobFailureSchema>\n\nexport const batchJobsItemSchema = z.object({\n  /**\n   * Provider job id — long, opaque, and the one string you paste into a support\n   * ticket. Rendered MIDDLE-truncated (`batch_68f2a1…9c04`) so the tail that\n   * actually distinguishes two ids survives, and copied in full.\n   */\n  id: z.string(),\n  status: batchJobStatusSchema,\n  /** Requests in the submitted file. */\n  total: z.number().int().nonnegative(),\n  /** Requests that came back with a result. */\n  done: z.number().int().nonnegative(),\n  /** Requests that came back with an error. `total - done - failed` is still pending. */\n  failed: z.number().int().nonnegative(),\n  /**\n   * Money spent SO FAR, in USD. On a running job this keeps climbing; it is not\n   * an estimate of the final bill, and a cancelled or expired job still shows\n   * what its finished requests cost.\n   */\n  costUsd: z.number().nonnegative(),\n  createdAt: instantSchema,\n  /**\n   * When the job is expected to finish — an INSTANT, not a pre-baked countdown\n   * string, so the remaining time is recomputed against the injected `now` and\n   * can never go stale mid-render. Ignored for terminal jobs: a completed job's\n   * ETA is noise.\n   */\n  eta: instantSchema.optional(),\n  /** Model the batch runs against, e.g. `claude-sonnet-4`. A scan aid, not a link. */\n  model: z.string().optional(),\n  /** Endpoint the requests target, e.g. `/v1/messages`. */\n  endpoint: z.string().optional(),\n  /**\n   * JOB-level failure text for `status: \"failed\"` — the file never validated, the\n   * account hit a spend cap. Different from per-request failures, and rendered in\n   * full: no max-height, no line clamp.\n   */\n  error: z.string().optional(),\n  /**\n   * A few failed requests, for the expandable preview. The count on screen is\n   * always \"X of `failed`\", so a sample is never mistaken for the total.\n   */\n  failures: z.array(batchJobFailureSchema).optional(),\n})\nexport type BatchJobsItem = z.infer<typeof batchJobsItemSchema>\n\n/**\n * The TABLE's own render state — \"is there a list of jobs to show at all\".\n * Independent of any job's status: the envelope can be `ready` while every job in\n * it has `failed`, and `status: \"error\"` here means the jobs endpoint itself did\n * not answer, which reads (and recovers) differently from a job failing.\n */\nexport const batchJobsStatusSchema = z.enum([\"loading\", \"empty\", \"error\", \"ready\"])\nexport type BatchJobsStatus = z.infer<typeof batchJobsStatusSchema>\n\n/** The envelope a data layer / mock factory hands over; the demo spreads it into the props. */\nexport const batchJobsSchema = z.object({\n  status: batchJobsStatusSchema,\n  items: z.array(batchJobsItemSchema),\n})\nexport type BatchJobsData = z.infer<typeof batchJobsSchema>\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
