# Drift change

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.

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

## Install

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

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/drift-change`.
It 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 packages: lucide-react@^1.67.0.

## Example

```tsx
"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>

Props:

- `change: DetectedChange`: 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.
- `readOnly?: string | undefined`: Why this person can't adopt or revert changes, e.g. "Only admins can adopt or revert." Both stay, locked, saying it.
- `revertUndo?: string | 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.
- `className?: string | undefined`: Classes, merged with Candor's.
- `headingLevel?: HeadingLevel | undefined`: The level of the change's heading. Defaults to one below the nearest `HeadingScope`, else 2.

## Strings (`driftChange`)

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

- `label`: Changed outside the app
- `low`: Low severity
- `medium`: Medium severity
- `high`: High severity
- `production`: Production
- `nonProduction`: Non-production
- `byPerson`: By {name}
- `byAgent`: By {name}
- `unattributed`: Not attributed
- `unattributedNote`: No person or agent is recorded for this change.
- `changedAt`: Changed {when}
- `changedUnknown`: When it changed isn’t known
- `detectedAt`: found {when}
- `showDiff`: Show the change
- `hideDiff`: Hide the change
- `review`: Review
- `reviewNote`: Opens the change. Nothing changes.
- `adopt`: Adopt
- `adoptNote`: Undoes nothing in {name}: the change stays, and the app records it as expected.
- `adopting`: Adopting…
- `revert`: Revert
- `revertNote`: Undoes this change in {name}, putting back what the app last recorded.
- `reverting`: Reverting…
- `revertTitle`: Revert this change in {name}?
- `revertProduction`: {name} is production: the revert reaches real users at once.
- `revertIt`: Revert the change
- `keep`: Keep it
- `waitAction`: Wait for the change to be updated.
- `waitUpdate`: Waiting for the change to update.
- `revertUndo`: The reverted change isn’t kept here: to have it again, it must be made again.
- `adoptFailed`: Adopting failed. Try again.
- `revertFailed`: Reverting failed. What it changed before it stopped isn’t known: check {name} before trying again.
- `adopted`: Adopted: the change stays, recorded as expected.
- `reverted`: Reverted: 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.

```ts
| { 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.

```ts
{
  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

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

```ts
{
  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

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;
}
```

### Environment

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

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").

```ts
{
  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

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

```ts
{
  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

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

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.

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

### DiffHunk

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

```ts
{ header?: string | undefined; lines: DiffLine[] }
```

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