Skip to content
Tier

Drift change

Pro

A change made outside the app (drift): what changed and where, how severe in words, who made it (a person, an agent, or "Not attributed", drawn loudest), when it was made (or that it isn't known) and when the app found it, and its diff behind Show the change. The actions are the app's: Review, Adopt and Revert, each with a line saying what it undoes. Revert asks first, in place, says whether the reverted change can be had again, and says when the environment is production. While an action runs the others stay locked, saying why; once it has finished they stay locked, waiting for the change to update, until the app's status (or a new change) arrives, and the outcome is announced when it does. A failure says so, and a failed revert says that what it did isn't known.

Focus follows the work: to Keep it when Revert asks, back to Revert after Keep it, and to the row's heading once the change is settled and its actions go. It moves only while focus is in the row: focus on the page or elsewhere is left alone, and a move still waiting when the person goes elsewhere is dropped.

Markdown
Version 5.2.0· other versions

This page is for 5.2.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 and 4.3.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/drift-change

Brings in: agent-avatar, bidi, button, change-diff, chat, entity-chip, environment-band, heading-level, heading-scope, locale, locked, relative-time, team, tokens, use-announcer, use-focus-within, work, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { DriftChange, type DetectedChange } from "@/components/candor/drift-change"

const found: DetectedChange = {
  id: "d1",
  summary: "The approval step on refunds over $500 was turned off",
  entity: { kind: "field", name: "refunds.approval", label: "Refund approval" },
  environment: { name: "Shop", production: true, hue: 1 },
  severity: "high",
  // No person or agent is on record for it, so the row says so, loudest.
  author: { kind: "unattributed" },
  detectedAt: new Date(Date.now() - 40 * 60_000).toISOString(),
  files: [
    {
      path: "rules/refunds.json",
      change: "update",
      language: "json",
      hunks: [{ lines: [{ kind: "context", text: '"over": 500,' }, { kind: "remove", text: '"approval": "required"' }, { kind: "add", text: '"approval": "none"' }] }],
    },
  ],
}

export default function Example() {
  const [change, setChange] = React.useState(found)
  const settle = (status: "adopted" | "reverted") => async () => {
    await new Promise((r) => setTimeout(r, 500))
    setChange((c) => ({ ...c, status }))
  }
  return <DriftChange className="max-w-2xl" change={change} onReview={() => {}} onAdopt={settle("adopted")} onRevert={settle("reverted")} />
}

<DriftChange>

PropTypeDefaultDescription
changerequiredDetectedChange—The change the app found.
onReview((change: DetectedChange) => void) | undefined—Shows Review; called when it's pressed, e.g. to open the change in full.
onAdopt((change: DetectedChange) => void | Promise<void>) | undefined—Shows Adopt. Called once pressed; the row waits on a returned promise, then on `status` "adopted".
onRevert((change: DetectedChange) => void | Promise<void>) | undefined—Shows Revert. Called once the person confirms; the row waits on a returned promise, then on `status` "reverted".
onOpenEntity((entity: EntityRef) => void) | undefined—Makes the changed thing's chip a button that opens it.
readOnlystring | undefined—Why this person can't adopt or revert changes, e.g. "Only admins can adopt or revert." Both stay, locked, saying it.
revertUndostring | undefined—Whether a reverted change can be had again, in the app's words, shown in Revert's confirmation, e.g. "The change is kept in the audit log for 30 days and can be reapplied from there." Left out, the confirmation says it isn't kept.
classNamestring | undefined—Classes, merged with Candor's.
headingLevelHeadingLevel | undefined—The level of the change's heading. Defaults to one below the nearest `HeadingScope`, else 2.

Strings

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

KeyEnglish
labelChanged outside the app
lowLow severity
mediumMedium severity
highHigh severity
productionProduction
nonProductionNon-production
byPersonBy {name}
byAgentBy {name}
unattributedNot attributed
unattributedNoteNo person or agent is recorded for this change.
changedAtChanged {when}
changedUnknownWhen it changed isn’t known
detectedAtfound {when}
showDiffShow the change
hideDiffHide the change
reviewReview
reviewNoteOpens the change. Nothing changes.
adoptAdopt
adoptNoteUndoes nothing in {name}: the change stays, and the app records it as expected.
adoptingAdopting…
revertRevert
revertNoteUndoes this change in {name}, putting back what the app last recorded.
revertingReverting…
revertTitleRevert this change in {name}?
revertProduction{name} is production: the revert reaches real users at once.
revertItRevert the change
keepKeep it
waitActionWait for the change to be updated.
waitUpdateWaiting for the change to update.
revertUndoThe reverted change isn’t kept here: to have it again, it must be made again.
adoptFailedAdopting failed. Try again.
revertFailedReverting failed. What it changed before it stopped isn’t known: check {name} before trying again.
adoptedAdopted: the change stays, recorded as expected.
revertedReverted: what the app last recorded is back.

Types

ChangeAuthor
Who made a change, as the app knows it: a person, an agent, or no one on record.
| { kind: "person"; name: string } | { kind: "agent"; agent: AgentIdentity } | { kind: "unattributed" }
DetectedChange
A change the app found in an environment that it didn't make itself (drift), as the app's server reports it.
{ id: string; /** What changed, in the app's words, e.g. "The approval step on refunds was turned off". */ summary: string; /** The thing that changed, shown as a chip. */ entity?: EntityRef | undefined; /** Where it changed. */ environment: Pick<Environment, "name" | "detail" | "production" | "hue">; /** How much it matters, drawn in words: low, medium or high. */ severity: "low" | "medium" | "high"; /** Who made it. An unattributed change is drawn loudest. */ author: ChangeAuthor; /** When it was made, as an ISO time. Left out when the app doesn't know, and the row says so. */ changedAt?: string | undefined; /** When the app found it, as an ISO time. */ detectedAt: string; /** The change itself, drawn with ChangeDiff behind Show the change. */ files?: FileDiff[] | undefined; /** What became of it: still open (the default), adopted, or reverted. A settled change shows the outcome, not the actions. */ status?: "open" | "adopted" | "reverted" | undefined; }

Types from other items

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

EntityRef from @/lib/candor/chat
{ kind: EntityKind; /** The identifier, e.g. "orders.status" or "billing.invoice.due_date". */ name: string; /** A friendlier label when there is one, e.g. "Region". */ label?: string | undefined; /** Where it lives, when more than one place is in scope, e.g. "Shop staging". */ location?: string | undefined; }
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; }
Environment from @/lib/candor/team
Where agents act: a CRM instance, a database, a repository, a project. The app names them; LocaleProvider's `nouns` sets what they're called ("project", "workspace").
{ id: string; /** The name shown everywhere, e.g. "Shop". */ name: string; /** What tells same-named ones apart, e.g. "dev-2" or "staging". */ detail?: string | undefined; /** Changes here reach real users, so components warn and ask for more care. */ production: boolean; /** A chart slot (1–5) so each environment keeps one colour everywhere it appears. */ hue?: 1 | 2 | 3 | 4 | 5 | undefined; connection: "connected" | "expired" | "not-connected"; }
FileDiff from @/lib/candor/work
{ path: string; /** What the file is, e.g. a field or a layout. */ entity?: EntityRef | undefined; change: "create" | "update" | "delete"; /** e.g. "xml", "ts", "python"; shown as a label. */ language?: string | undefined; hunks: DiffHunk[]; }
EntityKind from @/lib/candor/chat
What kind of thing an entity is: "record", "table", "workflow"… Any string; the app registers each kind's icon and noun (see EntityKindsProvider), and presets cover the kinds of the systems an app commonly works with.
string
AgentRole from @/lib/candor/chat
What an agent does: "assistant", "coder", "reviewer"… The app registers each role's glyph and hue (AgentRolesProvider).
string
DiffHunk from @/lib/candor/work
{ header?: string | undefined; lines: DiffLine[] }