# Upgrading to 5.0

How to take a Candor UI 4.x project to 5.0: the commands, then every change 5.0 makes with what breaks, where it shows, what to do and the codemod that does it, or the edit you make by hand. The same list is in each item's registry entry and is what `candor-ui update --to 5.0.0` prints. The release's notes are in the changelog: https://candor.themethodic.com/changelog#5-0-0.

## The commands

### 1. Look first

From the app's folder (the one with `components.json`), with your work committed. This prints what 5.0 changes for the items in `candor-lock.json`, with the merges the update would make, and writes nothing.

```bash
npx candor-ui@3 update --to 5.0.0 --dry-run
```

### 2. Update

Updates every item, merging your changes into Candor's as `update` always has. If `candor-lock.json` still records the layout CLI 2.0.0 nested (`candor/candor/`), CLI 3 stops and names `npx candor-ui@2 update --fold`, which folds it first.

```bash
npx candor-ui@3 update --to 5.0.0
```

### 3. Run the codemods

Runs, in this order, those of 5.0's codemods (`types-data`, `props`, `strings-keys`, `headings`) that an entry on one of your installed items names, and lists the ones it leaves out (all of them run where there's no `candor-lock.json`); `npx candor-ui@3 migrate <codemod>` runs one anyway. They change your own files only: Candor's installed files come with the update, and in one you've changed, what a codemod finds is listed. Each says what it changed and lists what it couldn't under "Left for you". `headings` changes nothing: it lists each heading item whose level 5.0 changes. Add `--dry-run` to look first.

```bash
npx candor-ui@3 migrate 5.0.0
```

### 4. Finish by hand, then check

Make the changes `migrate` lists under "Left for you": Tree's and EnvironmentBand's `onSelect`, which passes the id; a `headingLevel` where your page or a test needs 4.x's level (or a `<HeadingScope>` round the section, except round the free `PermissionPrompt`, which reads none and takes `headingLevel={3}`); two different separators to choose between (and a `Formatter` you build yourself, a test double for `useFormat`, adds `separator` and `join`); a removed sentence key to translate again; and an English export used outside a component. Then the ones below marked "By hand" that no codemod lists, which the type check finds. Then type-check, build and run your tests: one that uses `toBeDisabled()` on a control that waits checks `aria-disabled="true"` now.

```bash
npx tsc --noEmit && npm run build
```

## Before you start

- **`update --to 5.0.0` prints 5.0.0's text.** It reads the 5.0.0 snapshot, which stays as it was released, so the entries 5.1.0 corrected (`heading-default`, `strings-separator`, `strings-sentences`, `locked-not-disabled` and `props-active-id-nav`, and the migration lines' `npx candor-ui@3`) print there as they were. This page has the corrected text, and `update` to 5.1.0 or later prints it.
- `migrate` parses your source with the project's TypeScript or, on TypeScript 7 (which has no JavaScript API) or with none, with the TypeScript 5 that CLI 3 brings: there is nothing to install for the run.
- **`@candor-ui/react` 5.0.0** has the same type names as before (they were already `*Data`); it drops `statusLabel` and follows the heading, separator and naming changes below for its free components. Update it with your package manager.
- **A removed string key still compiles.** `StringOverrides` takes any key, so the type check doesn't find an override of a key 5.0 removes: it just isn't read. `migrate strings-keys` moves it or lists it, for a `strings` object it can see (on `LocaleProvider` or `CandorProvider`, in a `CandorConfig`, or typed `StringOverrides`).
- **Stored strings are data, not source.** No codemod reaches translations kept in a database or a CMS: move a stored `*Separator` override to `locale.separator`, and translate a removed sentence key's replacement, once, where it is stored.
- **4.x stays installable.** `https://candor.themethodic.com/r/v/4.5.0/<name>.json` serves every item as 4.5.0 shipped it, with the same licence check, and `npx candor-ui add <item> --version 4.5.0` installs from there.
- **A 3.x project goes to 4.0 first**, with CLI 2: `npx candor-ui@2 update --to 4.0.0`, then `npx candor-ui@2 migrate 4.0.0` ([Upgrading to 4.0](https://candor.themethodic.com/upgrading-to-4)). CLI 3 doesn't change a 3.x project; it names those commands. `@2` stays on npm for 3.x and 4.x.

## What changes (14)

### types-data

The six data types are named for what they hold: `Message` and `ToolCall` (lib/candor/chat), `ApprovalRequest` and `ResultTable` (lib/candor/work), `Finding` (lib/candor/data) and `RunReceipt` (lib/candor/ops) are `MessageData`, `ToolCallData`, `ApprovalRequestData`, `ResultTableData`, `FindingData` and `RunReceiptData`, as `@candor-ui/react` already named them. The old names aren't kept.

- **On:** `chat`, `work`, `data`, `ops`
- **What breaks:** An import of an old name from lib/candor fails the type check (`has no exported member 'Message'`). Imports from `@candor-ui/react` don't change.
- **Shows in:** the type check, imports
- **What to do:** `npx candor-ui@3 migrate types-data` renames each import and its uses in your files; an import you aliased (`Message as M`) keeps its local name. It lists a re-export from a file of yours, and a file that already has its own `MessageData`, for you to change.
- **Codemod:** `npx candor-ui@3 migrate types-data`, which `migrate 5.0.0` runs.

### heading-default

Every item that draws a heading takes `headingLevel` (1 to 6) and, without it, draws its top heading one level below the nearest `HeadingScope`, else at `<h2>`, so a page's outline skips no level. Dialogs, sheets, drawers, alert dialogs, settings sections, dashboard tiles and artifact panels set a scope over their content. `PageHeader` and `EmailLayout` stay at `<h1>`; the free `PermissionPrompt` and `PageState` default to `<h2>` and read no scope.

- **On:** `artifact-panel`, `app-layout`, `dock-window`, `settings-layout`, `mcp-connect`, `model-key`, `stop-all`, `entity-disambiguation`, `permission-prompt`, `thread-header`, `form-error-summary`, `dialog`, `sheet`, `drawer`, `alert-dialog`, `data-table`, `reorderable-list`, `board`, `details-list`, `reconcile-table`, `column-mapper`, `finding`, `narrative-block`, `portfolio-card`, `report-sheet`, `monitor-card`, `run-log`, `run-estimate`, `failure-clusters`, `review-queue`, `dashboard-tile`, `approval-request`, `result-card`, `plan`, `environment-promotion`, `blast-radius`, `agent-screen`, `checkpoint-list`, `clarifying-questions`, `change-set-review`, `document-progress-queue`, `file-list`, `proposal`, `workflow-editor`, `comment-thread`, `drift-change`, `ship-review`, `run-recovery`, `knowledge-sources`, `memory-manager`, `prose-diff`, `retrieval-inspector`, `version-history`, `away-digest`, `escalation`, `extraction-review`, `api-keys`, `channel-setup`, `data-region`, `data-retention`, `device-sessions`, `environment-connect`, `environment-list`, `integration-connect`, `schedule-editor`, `schedule-list`, `sso-setup`, `webhooks`, `account-create`, `consent-ceremony`, `invite-chooser`, `setup-checklist`, `sign-in`, `terms-consent`, `billing-details`, `invoice-history`, `plan-gate`
- **What breaks:** Where you don't pass `headingLevel`, a heading can be a different level than in 4.x: most that were `<h3>` or `<h4>` are `<h2>` outside a scope. It looks the same; a test or an outline check that expects 4.x's level sees the change.
- **Shows in:** what is drawn
- **What to do:** `npx candor-ui@3 migrate headings` lists each use without `headingLevel`, with the level it drew in 4.x, and changes nothing. Where your page or a test needs that level, pass `headingLevel={n}`; or wrap a section in `<HeadingScope level={n}>` (lib/candor/heading-scope, Pro) so the items in it sit one level below. The free `PermissionPrompt` draws `<h2>` by default since 5.0 and reads no `HeadingScope`: pass `headingLevel={3}` to keep 4.x's `<h3>`.
- **Codemod:** `npx candor-ui@3 migrate headings`, which `migrate 5.0.0` runs.

### heading-level-type

`headingLevel` is typed `HeadingLevel` (1 to 6) on every item, where nine props took `2 | 3 | 4 | 5 | 6`. lib/candor/heading-level exports the type, and `layout/heading` still does.

- **On:** `environment-list`, `dashboard-tile`, `board`, `data-table`, `reorderable-list`, `monitor-card`, `narrative-block`, `portfolio-card`
- **What breaks:** Nothing you pass stops type-checking. A type of yours read from the prop (`ComponentProps<typeof DataTable>["headingLevel"]`) now includes 1.
- **Shows in:** the type check
- **What to do:** Nothing for a level you pass. Where you type a level of your own, import `HeadingLevel` from lib/candor/heading-level.
- **Codemod:** None needed for a level you pass. By hand: import `HeadingLevel` where you type a level of your own.

### props-active-id-nav

TabBar's and NavRail's `current`, AppSidebar's `activeNav` and SettingsLayout's `active` are `activeId`, the name Candor's other lists use for the id of the item that's selected.

- **On:** `tab-bar`, `nav-rail`, `app-sidebar`, `settings-layout`
- **What breaks:** The old prop fails the type check; in code that isn't type-checked, nothing shows as current.
- **Shows in:** the type check, what is drawn
- **What to do:** `npx candor-ui@3 migrate props` renames them on Candor's components; the value is the same. Stepper's, PlanPicker's and InlineSuggestion's `current` mean something else and stay.
- **Codemod:** `npx candor-ui@3 migrate props`, which `migrate 5.0.0` runs.

### props-label-name

SettingsLayout's and DocumentViewer's `title`, which is their accessible name and isn't drawn, is `label`; `title` is Candor's word for a heading that is drawn.

- **On:** `settings-layout`, `document-viewer`
- **What breaks:** `title` fails the type check; in code that isn't type-checked, the settings region or the viewer has no name for a screen reader.
- **Shows in:** the type check, behaviour
- **What to do:** `npx candor-ui@3 migrate props` renames it on both.
- **Codemod:** `npx candor-ui@3 migrate props`, which `migrate 5.0.0` runs.

### props-popup-class

PersonMenu's and ScopeSwitcher's `layerClass` prop, the classes for the menu's popup, is `popupClassName`; `layerClass` is the name of `foundation/layer`'s constant.

- **On:** `person-menu`, `scope-switcher`
- **What breaks:** `layerClass` fails the type check; in code that isn't type-checked, the popup loses your classes.
- **Shows in:** the type check, styling
- **What to do:** `npx candor-ui@3 migrate props` renames it on both.
- **Codemod:** `npx candor-ui@3 migrate props`, which `migrate 5.0.0` runs.

### on-select-id

Tree's and EnvironmentBand's `onSelect` passes the id of what was chosen, as RunTree, SessionList, Plan and Candor's other lists do, not the node or the environment.

- **On:** `tree`, `environment-band`
- **What breaks:** A handler that takes the object fails the type check; in code that isn't type-checked, it reads properties of a string.
- **Shows in:** the type check, behaviour
- **What to do:** Write `onSelect={(id) => …}` and look the node or the environment up by id where the handler needs it. `npx candor-ui@3 migrate props` lists each `onSelect` on them with its file and line, and the type check finds the rest.
- **Codemod:** By hand: no codemod can make this change safely.

### strings-compat-exports

The English exports kept for compatibility are removed: `statusLabel` (lib/candor/status), `environmentLabel` (lib/candor/team), `approvalStateLabel` (approval-request), `reviewLevels` (review-ladder) and `standardCards` (standard-cards). SessionList's `statusLabel` prop is a different thing and stays.

- **On:** `status`, `team`, `approval-request`, `review-ladder`, `standard-cards`
- **What breaks:** An import of one fails the type check and the build.
- **Shows in:** the type check, imports
- **What to do:** `npx candor-ui@3 migrate strings-keys` rewrites `standardCards` used inside a component to `const standardCards = useStandardCards()` at its top, and lists the others with what to use: `useStatusLabel()`, EnvironmentBand's strings (`production`, `nonProduction`) joined by `useFormat().join`, `useStrings("approvalRequest", approvalRequestStrings)` and `useStrings("reviewLadder", reviewLadderStrings)`. Each follows the locale, where the export was English.
- **Codemod:** `npx candor-ui@3 migrate strings-keys`, which `migrate 5.0.0` runs.

### chart-strings

BarChart's `byCategory`, LineChart's `over` and `chartValues` and Heatmap's `heatmap` strings, which nothing has read since 4.2, are removed.

- **On:** `bar-chart`, `line-chart`, `heatmap`
- **What breaks:** An override of one is left in place and isn't read, as it hasn't been since 4.2, so nothing draws differently. It still compiles: `StringOverrides` takes any key.
- **Shows in:** what is drawn
- **What to do:** `npx candor-ui@3 migrate strings-keys` deletes the overrides.
- **Codemod:** `npx candor-ui@3 migrate strings-keys`, which `migrate 5.0.0` runs.

### strings-separator

The " · " between parts is one string, `locale.separator`, read through `useFormat().separator` and `useFormat().join`, where eleven items each had a key for it (`message.metaSeparator`, `composer.hintSeparator`, `reasoning.summarySeparator` …) and about forty typed it. SubscoreCard's `separator` and TableScrubberMap's `kindSeparator`, which joined lists, are removed: a list follows the locale's list format (`useFormat().list`). Strings that held the separator at their start or in their middle no longer do (`integrationConnect.connectedSince` is `since {when}`, `membersTable.invitedBy` is `Invited by {name}`, `columnMapper.suggested` is `Suggested`, with `percentSure` beside it, and the others the changelog lists): Candor joins the parts. Four that held it between parts are removed: `reconcileTable.summary` (now `openCount`, `resolvedCount` and `matchedCount`), `evalScorecard.passedFailed` (`passedCount`, `failedCount`), `reviewQueue.progress` (`position`, `gradedCount`) and `dataTable.aggregateLine` (nothing to translate).

- **On:** `locale`, `message`, `composer`, `permission-prompt`, `reasoning`, `payload`, `search-trace`, `change-set-review`, `task-tray`, `workflow-editor`, `review-queue`, `subscore-card`, `table-scrubber-map`, `integration-connect`, `subscription-status`, `members-table`, `away-digest`, `escalation`, `blast-radius`, `environment-promotion`, `clarifying-questions`, `composer-scope-picker`, `app-sidebar`, `inbox`, `session-list`, `agent-roster`, `reconcile-table`, `eval-scorecard`, `data-table`, `column-mapper`
- **What breaks:** An override of a removed key isn't read any more: the separator is whatever `locale.separator` says. The override still compiles, since `StringOverrides` takes any key, so `migrate strings-keys` is what finds it. An object you build as a `Formatter` yourself (a test double for `useFormat`) fails the type check: it has no `separator` or `join`.
- **Shows in:** what is drawn, the type check
- **What to do:** `npx candor-ui@3 migrate strings-keys` moves an override of one of the eleven keys to `strings.locale.separator` and removes the old keys; where two give different values, it leaves them and lists them for you to choose one. It lists an override of `subscoreCard.separator` or `tableScrubberMap.kindSeparator`, which has nothing to move to, and of `reconcileTable.summary`, `evalScorecard.passedFailed`, `reviewQueue.progress` or `dataTable.aggregateLine`, with the keys that say it now. An override of a string that no longer holds the separator, written with its own " · ", shows it twice: take the mark out. The codemod lists one that still has it. `clarifyingQuestions.before` ("Before I plan") is new, the words `clarifyingQuestions.progress` held before its separator: a translated app translates it, since no codemod lists it. `Formatter` has two new members, `separator` and `join`: an object you build as a `Formatter` yourself (a test double for `useFormat`) adds them, or it fails the type check.
- **Codemod:** `npx candor-ui@3 migrate strings-keys`, which `migrate 5.0.0` runs.

### strings-sentences

Sentences that were built by joining strings and numbers in code are one string each, with `{placeholders}`, so a translation can put the words in its own order; numbers go through the locale's formatter. The keys they replace are removed: `scheduleEditor.announceNext` (say it with `announceRule` and `announceRuleAlone`) and the others the changelog lists by item. Some keys stay but no longer fill a placeholder, since Candor joins that part on with `locale.separator`: `environmentPromotion.requestedBy`, `promoted` and `declined`, and `escalation.sentLine` and `acknowledged` (`{when}`); `blastRadius.group` and `awayDigest.group` (`{count}`); `taskTray.started` (`{status}`); and `membersTable.invitedBy` (`{expires}`, now `expiresIn`). Five sentences have new keys that replace none, so nothing is removed: `modelKey.keyFor`, `progressButton.measuredFailed` and `failedWithFiles`, `runTree.verdictNote` and `presence.hereStale`.

- **On:** `schedule-editor`, `data-retention`, `environment-connect`, `subscription-status`, `invite-chooser`, `dual-listbox`, `subscore-card`, `freshness-stamp`, `memory-manager`, `blast-radius`, `change-set-review`, `document-progress-queue`, `drift-change`, `environment-promotion`, `log-stream`, `plan`, `ship-review`, `workflow-editor`, `escalation`, `away-digest`, `task-tray`, `members-table`, `composer`, `composer-scope-picker`, `search-trace`, `app-sidebar`, `inbox`, `session-list`, `model-key`, `progress-button`, `run-tree`, `presence`
- **What breaks:** An override of a removed key isn't read any more: the item says the sentence in English until you translate its new key. Nothing fails to compile, since `StringOverrides` takes any key, so `migrate strings-keys` is what lists it. An override of a kept key that still has its dropped placeholder shows it as written (`{when}` on screen), with its own " · " beside Candor's. A translated app also says the five new keys' sentences in English (ModelKey's field label for one provider, ProgressButton's description and announcement with failed files, RunTree's verdict note, Presence's stale summary) until you translate them; no codemod lists those, since no key was removed.
- **Shows in:** what is drawn
- **What to do:** `npx candor-ui@3 migrate strings-keys` lists each override of a removed key with the key that says the sentence now and its placeholders. Translate that key: your words can't be moved for you, since the sentence is different. It also lists an override of a kept key that still has its dropped placeholder or a " · ": take them out. Translate the five new keys too, where your app is translated.
- **Codemod:** `npx candor-ui@3 migrate strings-keys`, which `migrate 5.0.0` runs.

### card-deck-backspace

CardDeck no longer dismisses a card on Backspace; Delete still does, and the hint names Delete and the arrow keys. A dismissal is announced only once the app has answered: on the redraw after `onDismiss`, or when the promise `onDismiss` returns settles; a card still in `items` then was refused, and isn't announced when it leaves later.

- **On:** `card-deck`
- **What breaks:** Backspace on a card does nothing. An app that takes the card out later, after an `await`, without returning that promise from `onDismiss`, has its dismissal go unannounced, where 4.x announced it when the card left.
- **Shows in:** behaviour
- **What to do:** Where your own help names Backspace for CardDeck, name Delete. Where `onDismiss` saves before it takes the card out, return the promise (an `async` handler does).
- **Codemod:** None needed: it changes what a key does, not your code.

### locked-not-disabled

A control that waits on something (a save on its way, a condition not yet met) stays focusable and is `aria-disabled`, with the reason, through `Locked`, where it was natively `disabled`; a press does nothing until it can be used. A ToolCall or SearchTrace row that has finished with nothing behind it, which can never open, is text rather than a button.

- **On:** `session-expired`, `webhooks`, `api-keys`, `data-controls`, `schedule-editor`, `account-create`, `profile-settings`, `terms-consent`, `invite-members`, `invite-chooser`, `step-walker`, `sign-in`, `step-up-dialog`, `tool-call`, `search-trace`, `permission-prompt`, `composer`, `message-branches`, `approval-request`, `ship-review`, `clarifying-questions`, `steer-composer`, `restore-dialog`, `plan`, `type-to-confirm`, `source-ingest`, `memory-manager`, `memory-mode`, `data-import`, `import-review`, `billing-details`, `plan-picker`
- **What breaks:** A test that checks such a control with `toBeDisabled()` fails, since it isn't `disabled`, and one that finds a finished ToolCall or SearchTrace row with no details as a button finds none. For a person nothing is pressed that wasn't before, and the control can be reached and says why it waits. A translated app says those reasons in English until it translates their keys (50 `locked…` keys, on 31 of these items: RestoreDialog's reasons were keys already, or are the app's).
- **Shows in:** behaviour, what is drawn
- **What to do:** In a test, check `aria-disabled="true"` (`toHaveAttribute("aria-disabled", "true")`) in place of `toBeDisabled()`, and find a finished ToolCall or SearchTrace row with no details by its text. Nothing changes in the app's code. A translated app translates the reasons the waiting controls give, which are new string keys and say their English words until it does: `accountCreate.lockedCreating`, `apiKeys.lockedCreating`, `approvalDecision.lockedRecording`, `approvalRequest.lockedEditChecking`, `approvalRequest.lockedEditInvalid`, `approvalRequest.lockedRecording`, `billingDetails.lockedSaving`, `clarifyingQuestions.lockedSending`, `clarifyingQuestions.lockedUnanswered`, `composer.lockedEmpty`, `composer.lockedHeld`, `composer.lockedSending`, `composer.lockedStreaming`, `composer.lockedUnavailable`, `dataControls.lockedPreparing`, `dataImport.lockedReading`, `importReview.lockedLoading`, `inviteChooser.lockedChoose`, `inviteChooser.lockedJoining`, `inviteMembers.lockedSending`, `memoryManager.lockedNoMemory`, `memoryMode.lockedSaving`, `messageBranches.lockedFirst`, `messageBranches.lockedLast`, `permissionPrompt.lockedBusy`, `permissionPrompt.lockedInvalid`, `permissionPrompt.lockedUnanswered`, `plan.lockedAllSkipped`, `plan.lockedChoose`, `planPicker.lockedChoose`, `planPicker.lockedUnchanged`, `profileSettings.lockedClean`, `profileSettings.lockedSaving`, `scheduleEditor.lockedSaving`, `searchTrace.lockedNoDetail`, `sessionExpired.lockedSigningIn`, `shipReview.lockedShipping`, `signIn.lockedBusy`, `signIn.lockedNeedsCode`, `signIn.lockedNeedsEmail`, `sourceIngest.lockedNoLink`, `sourceIngest.lockedNoText`, `steerComposer.lockedEmpty`, `stepUpDialog.lockedChecking`, `stepWalker.lockedAhead`, `termsConsent.lockedRecording`, `toolCall.lockedNoDetail`, `typeToConfirm.lockedWorking`, `webhooks.lockedAdding` and `webhooks.lockedTesting`.
- **Codemod:** None needed in the app's code. By hand: a test that uses `toBeDisabled()` on one of these controls checks `aria-disabled="true"`, and a translated app translates the new `locked…` keys listed above.

### cli-3

CLI 3.0.0 (`npx candor-ui@3`) takes a project to 5.0, with `migrate 5.0.0`, and works with Candor 4.0 and later. It no longer takes a 3.x project, folds the layout CLI 2.0.0 nested (`update --fold`), or records items installed before the lockfile (`update --from`); `npx candor-ui@2` stays on npm for those.

- **On:** every item that installs at the default paths: components, hooks, libs, primitives and presets
- **What breaks:** `npx candor-ui@2` doesn't have 5.0's codemods. `npx candor-ui@3` stops on a 3.x project, and on a lockfile that records CLI 2.0.0's nested layout, naming the `@2` command to run.
- **Shows in:** behaviour
- **What to do:** Use `npx candor-ui@3` from 5.0. A 3.x project goes to 4.0 first with `npx candor-ui@2 update --to 4.0.0` and `npx candor-ui@2 migrate 4.0.0`; a lockfile with the nested layout is folded with `npx candor-ui@2 update --fold` first.
- **Codemod:** None needed: run the commands above with `npx candor-ui@3`.
