Skip to content
Tier

Change list

Pro

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.

Markdown
Version 5.4.0· other versions

This page is for 5.4.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0, 4.2.0, 4.3.0, 4.4.0, 4.5.0, 5.0.0, 5.1.0 and 5.2.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/change-list

Brings in: agent-avatar, bidi, chat, data, heading-level, heading-scope, locale, locked, page-state, relative-time, tokens, use-announcer, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"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>

PropTypeDefaultDescription
entriesrequiredChangeListEntry[]—The changes, in any order: they are drawn newest first.
lastReviewedAtstring | 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.
compareChangeListCompare | 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.
defaultCompareChangeListCompare | 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.
stateListState | 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.
headingLevelHeadingLevel | undefined—The level of the heading. Defaults to one below the nearest `HeadingScope`, else 2.
classNamestring | undefined—Classes for the list, merged with Candor's.

Strings

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

KeyEnglish
labelChanges
compareShow
sinceSince you last reviewed
everythingEverything
noReviewNo review recorded yet
byAgentAgent: {name}
byPersonPerson: {name}
linesAdded{ one: "{count} line added", other: "{count} lines added" }
linesRemoved{ one: "{count} line removed", other: "{count} lines removed" }
whenUnknownTime not known
emptyNo changes yet.
nothingSinceNothing has changed since you last reviewed.
showing{ one: "{count} change shown", other: "{count} changes shown" }
loadingLoading changes
errorChanges couldn’t be loaded.

Motion

MomentFamilyWhat it does
tinttintA 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.

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`.
{ kind: "agent"; agent: AgentIdentity } | { kind: "person"; name: string }
ChangeListEntry
One change in a ChangeList.
{ 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.
"since" | "all"

Types from other items

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

ListState from @/lib/candor/data
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.
ChartState
HeadingLevel from @/lib/candor/heading-level
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.
1 | 2 | 3 | 4 | 5 | 6
AgentIdentity from @/lib/candor/chat
{ id: string; /** The displayed name, e.g. "Architect agent". Never derived from model output. */ name: string; role: AgentRole; specialty?: string | undefined; }
ChartState from @/lib/candor/data
{ 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 from @/lib/candor/chat
What an agent does: "assistant", "coder", "reviewer"… The app registers each role's glyph and hue (AgentRolesProvider).
string