Restore dialog
ProRestore a checkpoint, honestly. The person chooses code, conversation or both (nothing is chosen for them, and Restore stays locked until they do), and the dialog says what a restore doesn't undo: the effects of shell commands the agent ran (listing them when the app knows), changes made outside the agent, and effects that left the workspace such as sent messages and deployments. onRestore restores and then checks; the dialog shows that check with RestoreResult, which never says "Restored" without a check that matched.
Preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/restore-dialog
Brings in: button, checkpoint-list, dialog, locale, locked, radio-group, relative-time, status-dot, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import { Button } from "@/components/ui/button"
import { RestoreDialog, RestoreResult, type RestoreOutcome, type RestoreTarget } from "@/components/restore-dialog"
const checkpoint = { label: "Added tests for burst traffic", createdAt: new Date(Date.now() - 9 * 60_000), captured: "both" as const }
// The app restores, then compares each file with the checkpoint's snapshot and reports what it found.
async function restore(target: RestoreTarget): Promise<RestoreOutcome> {
await new Promise((r) => setTimeout(r, 1200))
return {
target,
checkedAt: new Date(),
checks: [
{ id: "1", kind: "file", label: "src/server/limiter.ts", status: "matches" },
{ id: "2", kind: "file", label: "src/server/routes.ts", status: "differs", detail: "A formatter on save re-wrapped 3 lines after the restore." },
{ id: "3", kind: "file", label: "package-lock.json", status: "not-checked" },
...(target !== "code" ? [{ id: "4", label: "Conversation", area: "conversation" as const, status: "matches" as const }] : []),
],
}
}
const earlier: RestoreOutcome = {
target: "code",
checks: [
{ id: "a", kind: "file", label: "src/server/limiter.ts", status: "matches" },
{ id: "b", kind: "file", label: "src/server/limiter.test.ts", status: "matches" },
],
}
export default function Example() {
return (
<div className="flex max-w-xl flex-col items-start gap-4">
<RestoreDialog
checkpoint={checkpoint}
onRestore={restore}
commandsRun={["npm install ioredis@5", "npm run db:migrate", "rm -rf .cache/limiter"]}
outsideChanges={["Priya Shah edited src/config/limits.ts"]}
externalEffects={["Pushed branch rate-limiter to origin", "Deployed a preview build"]}
trigger={<Button variant="outline">Restore this checkpoint…</Button>}
/>
<RestoreResult className="w-full" result={earlier} />
</div>
)
}<RestoreDialog>
| Prop | Type | Default | Description |
|---|---|---|---|
| checkpointrequired | Pick<Checkpoint, "label" | "createdAt" | "captured"> | — | The checkpoint to restore. What it captured decides which choices are available. |
| onRestorerequired | (target: RestoreTarget) => Promise<RestoreOutcome> | — | Restore what the person chose, then check the result against the checkpoint and report it. A rejection is shown as an error. |
| commandsRun | string[] | undefined | — | Shell commands the agent ran after this checkpoint, as text. Left out, the dialog says it isn't recorded; [] says none ran. |
| outsideChanges | string[] | undefined | — | Changes made outside the agent since the checkpoint, when the app recorded them ("Ana edited README.md"). |
| externalEffects | string[] | undefined | — | Effects that left the workspace since the checkpoint, when the app recorded them ("Deployed to preview"). |
| locked | Partial<Record<RestoreTarget, string | undefined>> | undefined | — | Choices the app doesn't allow here, with why ("Only the session owner can rewind the conversation."). |
| open | boolean | undefined | — | Whether the dialog is open, when the app controls it. |
| onOpenChange | ((open: boolean) => void) | undefined | — | Called when the dialog opens or closes. |
| trigger | React.ReactElement | undefined | — | The control that opens the dialog, when the app doesn't control `open`. |
<RestoreResult>
What a restore actually did, as the app checked it. "Restored" shows only when every check the app reports matched the checkpoint. Items that weren't checked are shown as not checked, a mismatch is flagged with what differs, and a restore that didn't finish says what happened. Used inside RestoreDialog, or alone after a restore started elsewhere.
| Prop | Type | Default | Description |
|---|---|---|---|
| resultrequired | RestoreOutcome | — | The app's report: what it checked after restoring, and any error. |
| className | string | undefined | — | Extra classes for the outer element. |
Strings
The fixed words, in English by default. Change any of them under restoreDialog in <LocaleProvider strings={{ … }}>; the table is exported as restoreDialogStrings.
| Key | English |
|---|---|
| title | Restore to “{label}”? |
| saved | Checkpoint from {time}. |
| choose | What to restore |
| both | Code and conversation |
| bothHint | Put the files back and rewind the chat to this point. |
| code | Code only |
| codeHint | Put the files back. The chat stays as it is. |
| conversation | Conversation only |
| conversationHint | Rewind the chat to this point. The files stay as they are now. |
| noFiles | This checkpoint didn’t capture files. |
| noConversation | This checkpoint didn’t capture the conversation. |
| notUndone | What a restore doesn’t undo |
| shell | Shell commands the agent ran |
| shellDefault | Their effects stay: installed packages, generated or deleted files, database and service changes. |
| shellListed | { one: "{count} command ran after this checkpoint:", other: "{count} commands ran after this checkpoint:" } |
| shellNone | No commands ran after this checkpoint. |
| shellUnknown | Which commands ran since then isn’t recorded. |
| outside | Changes made outside the agent |
| outsideDefault | Edits by people or other tools since this checkpoint aren’t tracked. A code restore may overwrite them where they touch the same files. |
| external | Effects outside this workspace |
| externalDefault | Sent messages, deployments, pushed commits and calls to other services can’t be taken back. |
| recorded | Recorded since this checkpoint: |
| cancel | Cancel |
| restore | Restore |
| chooseFirst | Choose what to restore first. |
| restoring | Restoring and checking… |
| close | Close |
| tryAgain | Try again |
| noReason | No reason was given. |
| result | Restore result |
| matched | Restored |
| matchedBody | { one: "Checked: {count} item matches the checkpoint.", other: "Checked: all {count} items match the checkpoint." } |
| differs | Doesn’t match the checkpoint |
| differsBody | { one: "{count} item differs from the checkpoint after the restore.", other: "{count} items differ from the checkpoint after the restore." } |
| partial | Partly checked |
| partialBody | { one: "{matched} matched. {count} item wasn’t checked, so whether it was put back isn’t known.", other: "{matched} matched. {count} items weren’t checked, so whether they were put back isn’t known." } |
| areaNotChecked | Nothing checked {area}, so whether it was put back isn’t known. |
| codeArea | the code |
| conversationArea | the conversation |
| unchecked | Restore ran, but wasn’t checked |
| uncheckedBody | Nothing was checked afterwards, so whether it matches the checkpoint isn’t known. |
| failed | The restore didn’t finish |
| failedBody | Some items may have changed before it stopped. |
| statusMatches | Matches |
| statusDiffers | Differs |
| statusNotChecked | Not checked |
| noDetail | The app didn’t say what differs. |
| checkedAt | Checked {time} |
Types
- RestoreTarget
- What a restore puts back: the code (files), the conversation, or both.
"both" | "code" | "conversation"- RestoreCheckStatus
- What the app found when it checked one file or area after the restore.
"matches" | "differs" | "not-checked"- RestoreCheck
- One file or area the app checked (or didn't) against the checkpoint after restoring.
{ id: string /** The file path ("src/server/limiter.ts") or area ("Conversation", "Lockfile"). */ label: string /** "file" shows the label as a path. Defaults to "area". */ kind?: "file" | "area" | undefined /** * Which part of the restore this check covers. A file is code unless you say otherwise. "Restored" needs a check * for every part restored: with "both", one covering the code and one cover- RestoreOutcome
- What the app reports after a restore: each check it made, and an error if the restore didn't finish.
{ /** What was restored. */ target: RestoreTarget /** What the app checked afterwards. Empty means nothing was checked, and the result says so. */ checks: RestoreCheck[] /** Set when the restore didn't finish: what happened, in plain words. */ error?: string | undefined /** When the checks were made. */ checkedAt?: Date | string | undefined }