Skip to content
Elements

Job progress

Work measured in minutes: weighted stages, an ETA, and a way out.

Verify the fix on CIabout 4 min
cloneinstallbuildtest

Fetching the branch

Current stage: clone
fig. 01 ยท plays once, replay from the corner

Installation

Install manually
npx shadcn@latest add "@assistant-ui/elements-job-progress"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

Job progress keeps a long-running task legible while it runs, then keeps the final receipt in the same place. Give stages relative weights, update the current stage and its share, and pass an outcome when the job settles.

Getting started

This element has no assistant-ui primitive of its own, so render it from a tool. The loading branch starts from the stage information available to the renderer. When the tool resolves, pass its final status as outcome so the card becomes a receipt rather than inferring success from a stage count.

Render a live job and its receipt

app/toolkit.tsx
"use client";

import { defineToolkit } from "@assistant-ui/react";
import { JobProgress } from "@/components/assistant-ui/elements/job-progress";

export const toolkit = defineToolkit({
  run_ci_job: {
    type: "backend",
    render: ({ args, result, addResult }) => {
      if (!result) {
        return (
          <JobProgress
            title={args.target}
            stages={args.stages ?? []}
            stageIndex={0}
            stageProgress={0}
            eta="starting"
            onCancel={() =>
              addResult({
                stageIndex: 0,
                stageProgress: 0,
                eta: "cancelled",
                outcome: {
                  status: "cancelled",
                  summary: "Cancelled before CI started.",
                },
                elapsedMs: 0,
              })
            }
          />
        );
      }

      return (
        <JobProgress
          title={args.target}
          stages={args.stages ?? []}
          stageIndex={result.stageIndex}
          stageProgress={result.stageProgress}
          eta={result.eta}
          outcome={result.outcome}
          elapsedMs={result.elapsedMs}
        />
      );
    },
  },
});

Anatomy

<div data-slot="job-progress" data-state="running">
  <div>
    <span>status icon</span>
    <span>title</span>
    <span>ETA, status word, or elapsed time</span>
    <button>cancel</button>
  </div>

  <span role="progressbar">weighted progress or a completed receipt</span>

  <div>
    <div>one monospace label per stage</div>
    <p>the current stage description</p>
  </div>

  <p>optional outcome summary</p>
  <span role="status">current stage or final outcome</span>
</div>

stageIndex is floored and clamped into 0 to stages.length. Reaching stages.length means the job finished successfully when no outcome is present. stageProgress is clamped into 0 to 1 and contributes only the current stage's weighted share. A current stage description appears below the labels. A hidden status region announces each current stage and final outcome.

An outcome wins over numeric progress. success uses a green check and done; partial uses amber, failed uses red, and cancelled uses a muted circle slash. A success outcome fills the bar to 100 percent; partial, failed, and cancelled keep the bar and aria-valuenow at the progress the job actually reached. When elapsedMs is present on a finished job or receipt, the terminal word becomes a semantic <time> such as 1m 12s.

Examples

A partial receipt

<JobProgress
  title="Verify the fix on CI"
  stages={stages}
  stageIndex={3}
  stageProgress={0.4}
  eta="stopped"
  outcome={{
    status: "partial",
    summary: "Build passed. Tests stopped after the preview environment timed out.",
  }}
  elapsedMs={72_000}
/>

Restyle the root

className is merged onto the root. The ETA and stage labels use the shared mono surface, and the cancel button uses the shared ghostButton surface from surfaces.tsx.

<JobProgress
  title="Verify the fix on CI"
  stages={stages}
  stageIndex={1}
  stageProgress={0.6}
  eta="about 3 min"
  className="max-w-none gap-4"
/>

API reference

Render props

PropTypeDescription
args{ target: string; stages: { name: string; weight: number; description?: string }[] }The tool arguments. Stages are available while the tool is still running.
result{ stageIndex: number; stageProgress: number; eta: string; outcome?: { status: "success" | "partial" | "failed" | "cancelled"; summary?: string }; elapsedMs?: number } | undefinedThe one-shot runner result. Its outcome turns the final card into a receipt.
addResult(result: TResult) => voidSupplies a result from the renderer, such as a cancellation receipt.

run_ci_job result

FieldTypeDescription
stageIndexnumberThe current or final stage. stages.length means success when no outcome is set.
stageProgressnumberProgress through that stage, from 0 to 1.
etastringThe running label displayed before a receipt or finished state.
outcome{ status: "success" | "partial" | "failed" | "cancelled"; summary?: string }Optional final receipt status and summary.
elapsedMsnumberOptional terminal duration in milliseconds.

Runtime outcome fields

FieldTypeDescription
status"success" | "partial" | "failed" | "cancelled"The final receipt state.
summarystringOptional text shown below the stages.