# Change list

Changes, newest first: what changed, who made it (an agent through its mark and the name the app gives it, or a person), when, and how many lines were added and removed when that's known. A choice above the list shows "Since you last reviewed" or "Everything". The first needs the app's record of the person's last review (`lastReviewedAt`): without one it stays in the list, locked, saying "No review recorded yet", and is never guessed. A change whose date can't be read is kept under "Since you last reviewed", as it can't be shown to be older.

Each title is text, as the agent or the app wrote it; `onOpen` makes it a button that opens the change. Choosing says how many changes are shown, once, in a polite announcement.

Its strings are overridden under `changeList` in LocaleProvider's `strings`.

- Registry name: `change-list` (`@candor/change-list`)
- 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/change-list

## Install

```bash
npx candor-ui add change-list
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/change-list`.
It brings in: agent-avatar, bidi, chat, data, heading-level, heading-scope, locale, locked, page-state, relative-time, tokens, use-announcer, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { ChangeList, type ChangeListCompare, type ChangeListEntry } from "@/components/candor/change-list"

const ago = (hours: number) => new Date(Date.now() - hours * 3_600_000).toISOString()
const agent = { id: "dev", name: "Dev agent", role: "coder" }

const entries: ChangeListEntry[] = [
  { id: "c4", title: "Show the region on the customer header", by: { kind: "agent", agent }, at: ago(1), added: 14, removed: 3 },
  { id: "c3", title: "Add a region column to customers", by: { kind: "agent", agent }, at: ago(5), added: 6, removed: 0 },
  { id: "c2", title: "Retire the partner tier field", by: { kind: "person", name: "Priya" }, at: ago(30), added: 0, removed: 22 },
  { id: "c1", title: "Rename the billing settings page", by: { kind: "person", name: "Maya" }, at: ago(72) },
]

export default function Example() {
  const [compare, setCompare] = React.useState<ChangeListCompare>("since")
  const [recorded, setRecorded] = React.useState(true)
  return (
    <div className="flex max-w-xl flex-col gap-3">
      <label className="flex items-center gap-2 text-sm">
        <input type="checkbox" checked={recorded} onChange={(e) => setRecorded(e.target.checked)} />
        The app has a record of the last review
      </label>
      {/* Without `lastReviewedAt`, "Since you last reviewed" is locked and says why; the app's record is never guessed. */}
      <ChangeList
        entries={entries}
        lastReviewedAt={recorded ? ago(12) : null}
        compare={compare}
        onCompareChange={setCompare}
        onOpen={() => {}}
      />
    </div>
  )
}
```

## <ChangeList>

Props:

- `entries: ChangeListEntry[]`: The changes, in any order: they are drawn newest first.
- `lastReviewedAt?: string | null | undefined`: When the person last reviewed, as an ISO date, from the app's own record. Without it (or with a date that can't be read) "Since you last reviewed" is locked and says why.
- `compare?: ChangeListCompare | null | undefined`: The compare choice, when the app holds it: pressed in the list and changed only by the app (`onCompareChange` says what the person chose). `null` presses neither and shows every change.
- `defaultCompare?: ChangeListCompare | null | undefined`: The choice at first, when the list holds it: given this and no `compare`, the list changes it when the person chooses, starting here, and still calls `onCompareChange`. `null` starts with neither pressed. With neither prop the list holds no choice: it shows every change, presses neither, and calls `onCompareChange`.
- `onCompareChange?: ((compare: ChangeListCompare) => void) | undefined`: Called with the choice the person makes.
- `onOpen?: ((id: string) => void) | undefined`: Makes each title a button; called with the change's id.
- `state?: ListState | undefined`: What the list is showing: its rows (ready, the default), loading, empty or error with the reason, or partial with a note over the rows. Loading, empty and error draw in place of the rows, in this component's own words.
- `onRetry?: (() => void) | undefined`: Shows Try again when `state` is an error; called when it's pressed. Pass it only when it loads the list again.
- `headingLevel?: HeadingLevel | undefined`: The level of the heading. Defaults to one below the nearest `HeadingScope`, else 2.
- `className?: string | undefined`: Classes for the list, merged with Candor's.

## Strings (`changeList`)

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

- `label`: Changes
- `compare`: Show
- `since`: Since you last reviewed
- `everything`: Everything
- `noReview`: No review recorded yet
- `byAgent`: Agent: {name}
- `byPerson`: Person: {name}
- `linesAdded`: { one: "{count} line added", other: "{count} lines added" }
- `linesRemoved`: { one: "{count} line removed", other: "{count} lines removed" }
- `whenUnknown`: Time not known
- `empty`: No changes yet.
- `nothingSince`: Nothing has changed since you last reviewed.
- `showing`: { one: "{count} change shown", other: "{count} changes shown" }
- `loading`: Loading changes
- `error`: Changes couldn’t be loaded.

## Types

### ChangeListAuthor

Who made a change: an agent, drawn with its mark and the name the app gives it, or a person by name. The same shape as Digest's `DigestActor`.

```ts
{ kind: "agent"; agent: AgentIdentity } | { kind: "person"; name: string }
```

### ChangeListEntry

One change in a ChangeList.

```ts
{
  id: string;
  /** What changed, in a line. Often the agent's words: drawn as text, never as HTML or a link. */
  title: string;
  by: ChangeListAuthor;
  /** When it was made, as an ISO date. */
  at: string;
  /** Lines added, when known. Left out (or `null`, or not a finite number), none is drawn: never 0 for unknown. */
  added?: number | null | undefined;
  /** Lines removed, when known. Left out (or `null`), none is drawn. */
  removed?: number | null | undefined;
}
```

### ChangeListCompare

ChangeList's compare choice: the changes since the person last reviewed, or every change.

```ts
"since" | "all"
```

## Types from other items

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

### ListState

Import: `import type { ListState } from "@/lib/candor/data"` (item `data`, https://candor.themethodic.com/docs/data.md)

What a list or table is showing, the same shape as `ChartState` and as page-state's `ListState` (which the free tier ships, so it declares its own rather than import this file): its rows (ready), the shape of rows still coming (loading), nothing and why (empty), the rows it has with a note on what's missing (partial), or why it couldn't load (error). Lists without a `state` show their rows.

```ts
ChartState
```

### 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
```

### AgentIdentity

Import: `import type { AgentIdentity } from "@/lib/candor/chat"` (item `chat`, https://candor.themethodic.com/docs/chat.md)

```ts
{
  id: string;
  /** The displayed name, e.g. "Architect agent". Never derived from model output. */
  name: string;
  role: AgentRole;
  specialty?: string | undefined;
}
```

### ChartState

Import: `import type { ChartState } from "@/lib/candor/data"` (item `data`, https://candor.themethodic.com/docs/data.md)

```ts
{
  kind: "ready";
} | {
  kind: "loading";
}
/** Nothing to plot, and why. */
 | {
  kind: "empty";
  reason: string;
}
/** Some of the range is missing. */
 | {
  kind: "partial";
  note: string;
} | {
  kind: "error";
  reason: string;
}
```

### AgentRole

Import: `import type { AgentRole } from "@/lib/candor/chat"` (item `chat`, https://candor.themethodic.com/docs/chat.md)

What an agent does: "assistant", "coder", "reviewer"… The app registers each role's glyph and hue (AgentRolesProvider).

```ts
string
```

## Motion

- **tint** (tint): A compare choice's text colour changes when the pointer is over it, and its background and ring change when the person chooses it; a ring fades in on keyboard focus. A locked choice doesn't change on hover.

Set with `--cd-motion-change-list-<moment>-*` in your brand file; the families are in the design reference.

## 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`.
