# Document progress queue

Per-document progress for a batch an agent works through: each document queued, working (with the stage it's at, and a bar only when the pipeline measured how far that stage is), done (with what it came to), or failed (with the reason, and Retry). A summary over the list counts each state and fills a bar with the documents finished out of the batch. That is all it claims: no time left and no overall percentage, because documents differ and only the counts are measured. While the app is still adding documents (`adding`), the summary says "so far" and shows no bar, since the total isn't known.

The list filters by state when there is more than one, so the failed ones are a click away, and `onRetry` with several failed adds "Retry 3 failed". A finished document says how long it took when the app gives both times; nothing is shown when it doesn't.

- Registry name: `document-progress-queue` (`@candor/document-progress-queue`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/document-progress-queue

## Install

```bash
npx candor-ui add document-progress-queue
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/document-progress-queue`.
It brings in: bidi, button, format-elapsed, heading-level, heading-scope, locale, locked, progress, status, status-dot, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { DocumentProgressQueue, type DocumentProgress } from "@/components/candor/document-progress-queue"

const at = (s: number) => new Date(Date.UTC(2026, 10, 10, 9, 0, s)).toISOString()

/* What the app's pipeline reports for each document. A stage's bar appears only where the pipeline counted pages. */
const batch: DocumentProgress[] = [
  { id: "d-1", name: "March statement.pdf", state: "done", result: "18 fields extracted", startedAt: at(0), finishedAt: at(42) },
  { id: "d-2", name: "Invoice INV-2207.pdf", state: "done", result: "11 fields extracted, 2 to check", startedAt: at(3), finishedAt: at(31) },
  { id: "d-3", name: "Invoice INV-2211.pdf", state: "working", stage: "Reading pages", progress: { done: 3, total: 12 } },
  { id: "d-4", name: "Supplier contract.pdf", state: "working", stage: "Extracting fields" },
  { id: "d-5", name: "Receipt 88.png", state: "failed", error: "The scan is too blurry to read." },
  { id: "d-6", name: "Invoice INV-2190.pdf", state: "failed", error: "The file is password-protected." },
  { id: "d-7", name: "Invoice INV-2215.pdf", state: "queued" },
  { id: "d-8", name: "Invoice INV-2219.pdf", state: "queued" },
]

export default function Example() {
  const [documents, setDocuments] = React.useState(batch)
  const [retrying, setRetrying] = React.useState<string[]>([])
  return (
    <DocumentProgressQueue
      title="Invoices from the March upload"
      className="max-w-2xl"
      documents={documents}
      retrying={retrying}
      onRetry={(ids) => {
        setRetrying((r) => [...r, ...ids])
        // Your app asks its pipeline to run them again; they come back queued.
        window.setTimeout(() => {
          setDocuments((ds) => ds.map((d) => (ids.includes(d.id) ? { id: d.id, name: d.name, state: "queued" } : d)))
          setRetrying((r) => r.filter((id) => !ids.includes(id)))
        }, 800)
      }}
      onOpen={() => {}}
    />
  )
}
```

## <DocumentProgressQueue>

Props:

- `documents: DocumentProgress[]`: The batch's documents, in the order to show them.
- `adding?: boolean | undefined` (default `false`): The app is still adding documents to the batch: the summary counts "so far" and shows no bar.
- `onRetry?: ((ids: string[]) => void) | undefined`: Called with the ids of failed documents to try again: one from its row, or, with two or more that can be retried, every one of them that isn't being retried already. Without it there is no Retry.
- `retrying?: readonly string[] | undefined`: Ids the app is retrying now: their Retry is locked with the reason until their state changes.
- `onOpen?: ((id: string) => void) | undefined`: Called with a done document's id, to open what it came to. Without it, done documents have no Open.
- `title?: string | undefined`: A heading over the queue, also its accessible name. Defaults to "Documents in this batch" for the name, with no heading shown.
- `headingLevel?: HeadingLevel | undefined`: The heading's level when `title` is set. Defaults to one below the nearest `HeadingScope`, else 2.
- `className?: string | undefined`

## Strings (`documentProgressQueue`)

The fixed words, English by default. Override any of them under `documentProgressQueue` in `<LocaleProvider strings={{ documentProgressQueue: { … } }}>`; the table is exported as `documentProgressQueueStrings`.

- `label`: Documents in this batch
- `documents`: { one: "{count} document", other: "{count} documents" }
- `documentsSoFar`: { one: "{count} document so far", other: "{count} documents so far" }
- `countDone`: {done} done
- `countWorking`: {working} working
- `countQueued`: {queued} queued
- `countFailed`: {failed} failed
- `announceFailed`: { one: "{count} document failed.", other: "{count} documents failed." }
- `announceFinished`: The batch has finished: {done} done, {failed} failed.
- `finished`: {finished} of {total} finished
- `queued`: Queued
- `working`: Working
- `done`: Done
- `failed`: Failed
- `stageStep`: {stage}: {done} of {total}
- `progressName`: {name}: {step}
- `took`: Took {time}
- `noReason`: No reason was given.
- `retry`: Retry
- `retryName`: Retry {name}
- `retryFailed`: { one: "Retry {count} failed", other: "Retry {count} failed" }
- `retrying`: Retrying this document.
- `retryingAll`: The failed documents are being retried.
- `open`: Open
- `openName`: Open {name}
- `show`: Show
- `all`: All
- `option`: {state} {count}
- `empty`: No documents in this batch.
- `noneShown`: No documents in this state.

## Types

### DocumentProgressState

Where one document is: waiting its turn, being worked on, finished, or stopped by a failure.

```ts
"queued" | "working" | "done" | "failed"
```

### DocumentProgress

One document in the batch, as the app's pipeline reports it.

```ts
{
  /** Stable for this document in the batch. */
  id: string;
  /** The document's name, as the app shows it ("March statement.pdf"). */
  name: string;
  state: DocumentProgressState;
  /** What is happening to it now, in the app's words ("Reading pages", "Extracting fields"). Shown while working. */
  stage?: string | undefined;
  /**
   * How far the current stage is, as the pipeline measured it: 3 of 12 pages. Shown as a bar while working. Left out,
   * there is no bar and no percentage: nothing is estimated.
   */
  progress?: {
    done: number;
    total: number;
  } | undefined;
  /** Why it failed, in the app's words. Shown on a failed document; without it, it says no reason was given. */
  error?: string | undefined;
  /** What it came to, in the app's words ("18 fields extracted"). Shown on a done document. */
  result?: string | undefined;
  /** When work on it started (ISO). With `finishedAt`, a finished document says how long it took. */
  startedAt?: string | undefined;
  /** When it finished, done or failed (ISO). */
  finishedAt?: string | undefined;
  /** Why it can't be retried, when it can't (a file that can't be read again). Locks its Retry with the reason. */
  cannotRetry?: string | undefined;
}
```

## Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

### HeadingLevel

Import: `import type { HeadingLevel } from "@/lib/candor/heading-level"` (item `heading-level`, https://candor.themethodic.com/docs/heading-level.md)

A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.

```ts
1 | 2 | 3 | 4 | 5 | 6
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
