# Checkpoints

> Restore points across an agent run: a compact rail of auto and saved checkpoints with relative times, a current marker and inline-confirmed Restore.

- **Slug:** `checkpoints` · **Category:** agent · **Status:** experimental
- **Import:** `import { Checkpoint, Checkpoints } from "@kura-ui/react/checkpoints";`
- **Docs:** https://kuro.abstergo.fyi/components/checkpoints
- **Registry item:** https://kuro.abstergo.fyi/r/checkpoints.json
- **Tags:** agent, checkpoint, restore, rewind, undo, history, snapshot

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/checkpoints.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add checkpoints
# npm package
npm install @kura-ui/react
```

## When to use

- A coding agent snapshots the workspace before edits (Claude Code rewind, Cursor checkpoints) and the human should be able to roll files back.
- Showing where the workspace is after a rewind: checkpoints newer than the current one read as undone and can be restored forward.
- Letting people save a manual checkpoint before a risky step (onCreate).

## When not to use

- The agent's plan or steps — use task-list or agent-timeline.
- Reviewing what changed in files — use diff-view or file-changes.
- Git history or commits — this is for harness snapshots, not a commit log.

## Props

| Prop | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `checkpoints` | `Checkpoint[]` | — | yes | { id, label, at, filesChanged?, kind: 'auto' \| 'manual', current? }, newest first. Mark exactly one current. |
| `title` | `React.ReactNode` | `"Checkpoints"` | no | Header title; the count follows it. |
| `onRestore` | `(id: string) => void \| Promise<void>` | — | no | Restore handler, called after the inline confirm. Return a promise to show a spinner until it settles. Without it no Restore buttons render. |
| `onCreate` | `() => void` | — | no | Shows 'Save checkpoint' in the header. Add a kind:'manual' checkpoint marked current. |
| `confirm` | `boolean` | `true` | no | Ask inline before restoring. false calls onRestore directly. |
| `empty` | `React.ReactNode` | — | no | Replaces the default empty state ('No checkpoints yet'). |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **Checkpoints** (`data-slot="checkpoints"`) — Root panel (a size container).
- **Header** (`data-slot="checkpoints-header"`) — Title, count and the optional checkpoints-create button.
- **Item** (`data-slot="checkpoints-item"`) — <li> with `data-kind`, `data-current`, `data-ahead` (newer than current = undone) and `data-state='confirming'`. Node on a hairline rail: hollow ring = auto, diamond = saved, ink dot with halo = current.
- **Label** (`data-slot="checkpoints-label"`) — What the agent had just done. Truncates.
- **Current** (`data-slot="checkpoints-current"`) — 'Current' pill.
- **Files** (`data-slot="checkpoints-files"`) — '3 files' in mono (hidden when narrow).
- **Restore** (`data-slot="checkpoints-restore"`) — Ghost button that swaps in for the relative time on hover/focus (always visible on touch); aria-expanded when confirming.
- **Confirm** (`data-slot="checkpoints-confirm"`) — Inline confirm under the row: what will be undone (or re-applied), Cancel and Restore.
- **Empty** (`data-slot="checkpoints-empty"`) — Empty state.

## Accessibility

- Role: `list (ol)`
- An ordered list labelled by the title. Times are <time datetime> with the absolute time as a tooltip.
- Restore buttons are named 'Restore to <label>'. Opening the confirm moves focus to its Restore button; Escape or Cancel closes it and returns focus to the trigger.
- Undone checkpoints carry visually hidden '(undone)'. A polite status region announces 'Restored to …'.
- Relative times render after mount only, so server and client markup match.
- Reduced motion: Rows fade in without rising; the confirm opens with a fade instead of a height transition.

| Keys | Action |
| --- | --- |
| Tab | Reach each Restore button (it becomes visible on focus) |
| Enter / Space | Open the inline confirm, then confirm |
| Escape | Cancel the confirm |

## Notes for agents

- Pass checkpoints newest first and mark exactly one current; the component uses `at` to work out which are undone.
- Restore only rewinds files in your harness; keep the conversation and say so in your UI copy if that differs.
- Make onRestore async when restoring takes time so the confirm shows a spinner and blocks double restores.

## Examples

### Three checkpoints (minimal)

```tsx
"use client";

import * as React from "react";
import { type Checkpoint, Checkpoints } from "@kura-ui/react/checkpoints";

const sec = (s: number) => Date.now() - s * 1000;

export function CheckpointsMinimal() {
  const [checkpoints] = React.useState<Checkpoint[]>(() => [
    {
      id: "c3",
      label: "Patched src/middleware.ts",
      at: sec(70),
      filesChanged: 1,
      kind: "auto",
      current: true,
    },
    {
      id: "c2",
      label: "Replaced fetchUser() in 9 files",
      at: sec(260),
      filesChanged: 9,
      kind: "auto",
    },
    { id: "c1", label: "Run started on kura/auth-client", at: sec(690), kind: "auto" },
  ]);
  return <Checkpoints className="w-full max-w-md" checkpoints={checkpoints} onRestore={() => {}} />;
}
```

### Live, with save and restore (full)

A new automatic checkpoint lands while you watch; Restore confirms inline and resolves after a simulated 900ms; Save checkpoint adds a manual one.

```tsx
"use client";

import * as React from "react";
import { type Checkpoint, Checkpoints } from "@kura-ui/react/checkpoints";

function storyline(): Checkpoint[] {
  return [
    {
      id: "tests",
      label: "Updated login and session tests",
      at: sec(150),
      filesChanged: 3,
      kind: "auto",
      current: true,
    },
    { id: "mw", label: "Patched src/middleware.ts", at: sec(290), filesChanged: 1, kind: "auto" },
    { id: "manual", label: "Before middleware changes", at: sec(370), kind: "manual" },
    {
      id: "client",
      label: "Replaced fetchUser() with auth.getSession()",
      at: sec(430),
      filesChanged: 9,
      kind: "auto",
    },
    { id: "start", label: "Run started on kura/auth-client", at: sec(720), kind: "auto" },
  ];
}

const sec = (s: number) => Date.now() - s * 1000;

export function CheckpointsFull() {
  const [checkpoints, setCheckpoints] = React.useState<Checkpoint[]>(storyline);

  // The agent keeps working: a new automatic checkpoint lands a moment after mount.
  React.useEffect(() => {
    const t = setTimeout(() => {
      setCheckpoints((list) => [
        {
          id: "fix",
          label: "Fixed expired-session refresh in src/auth/client.ts",
          at: Date.now(),
          filesChanged: 1,
          kind: "auto",
          current: true,
        },
        ...list.map((c) => ({ ...c, current: false })),
      ]);
    }, 1800);
    return () => clearTimeout(t);
  }, []);

  const restore = (id: string) =>
    new Promise<void>((resolve) =>
      setTimeout(() => {
        setCheckpoints((list) => list.map((c) => ({ ...c, current: c.id === id })));
        resolve();
      }, 900),
    );

  const save = () =>
    setCheckpoints((list) => {
      const current = list.find((c) => c.current);
      return [
        {
          id: `manual-${list.length}`,
          label: current ? `Saved after “${current.label}”` : "Saved checkpoint",
          at: Date.now(),
          kind: "manual" as const,
          current: true,
        },
        ...list.map((c) => ({ ...c, current: false })),
      ];
    });

  return (
    <Checkpoints
      className="w-full max-w-lg"
      checkpoints={checkpoints}
      onRestore={restore}
      onCreate={save}
    />
  );
}
```

### After a rewind (state)

The current checkpoint is in the middle; newer ones read as undone and can be restored forward.

```tsx
"use client";

import * as React from "react";
import { type Checkpoint, Checkpoints } from "@kura-ui/react/checkpoints";

function storyline(): Checkpoint[] {
  return [
    {
      id: "tests",
      label: "Updated login and session tests",
      at: sec(150),
      filesChanged: 3,
      kind: "auto",
      current: true,
    },
    { id: "mw", label: "Patched src/middleware.ts", at: sec(290), filesChanged: 1, kind: "auto" },
    { id: "manual", label: "Before middleware changes", at: sec(370), kind: "manual" },
    {
      id: "client",
      label: "Replaced fetchUser() with auth.getSession()",
      at: sec(430),
      filesChanged: 9,
      kind: "auto",
    },
    { id: "start", label: "Run started on kura/auth-client", at: sec(720), kind: "auto" },
  ];
}

const sec = (s: number) => Date.now() - s * 1000;

export function CheckpointsRestored() {
  const [checkpoints, setCheckpoints] = React.useState<Checkpoint[]>(() =>
    storyline().map((c) => ({ ...c, current: c.id === "manual" })),
  );
  return (
    <Checkpoints
      className="w-full max-w-lg"
      checkpoints={checkpoints}
      onRestore={(id) => setCheckpoints((l) => l.map((c) => ({ ...c, current: c.id === id })))}
    />
  );
}
```

### Empty (state)

```tsx
import { Checkpoints } from "@kura-ui/react/checkpoints";

export function CheckpointsEmpty() {
  return <Checkpoints className="w-full max-w-md" checkpoints={[]} />;
}
```

### With run-header (composition)

Restoring rewinds the run's step count and pauses it; Resume returns to the latest checkpoint.

```tsx
"use client";

import * as React from "react";
import { RunHeader } from "@kura-ui/react/run-header";
import { type Checkpoint, Checkpoints } from "@kura-ui/react/checkpoints";

const sec = (s: number) => Date.now() - s * 1000;

function storyline(): Checkpoint[] {
  return [
    {
      id: "tests",
      label: "Updated login and session tests",
      at: sec(150),
      filesChanged: 3,
      kind: "auto",
      current: true,
    },
    { id: "mw", label: "Patched src/middleware.ts", at: sec(290), filesChanged: 1, kind: "auto" },
    { id: "manual", label: "Before middleware changes", at: sec(370), kind: "manual" },
    {
      id: "client",
      label: "Replaced fetchUser() with auth.getSession()",
      at: sec(430),
      filesChanged: 9,
      kind: "auto",
    },
    { id: "start", label: "Run started on kura/auth-client", at: sec(720), kind: "auto" },
  ];
}

const STEP_OF: Record<string, number> = { tests: 5, mw: 4, manual: 3, client: 3, start: 0 };

export function CheckpointsComposition() {
  const [startedAt] = React.useState(() => sec(744));
  const [checkpoints, setCheckpoints] = React.useState<Checkpoint[]>(storyline);
  const current = checkpoints.find((c) => c.current)?.id ?? "tests";
  const rewound = current !== "tests";
  return (
    <div className="flex w-full max-w-xl flex-col gap-2">
      <RunHeader
        variant="bar"
        title="Migrate session API to @acme/auth"
        status={rewound ? "paused" : "running"}
        startedAt={startedAt}
        progress={{ done: STEP_OF[current] ?? 0, total: 7 }}
        cost={1.46}
        budget={5}
        onPause={() => {}}
        onResume={() => setCheckpoints((l) => l.map((c) => ({ ...c, current: c.id === "tests" })))}
        onStop={() => {}}
        shortcuts={false}
      />
      <Checkpoints
        checkpoints={checkpoints}
        onRestore={(id) => setCheckpoints((l) => l.map((c) => ({ ...c, current: c.id === id })))}
      />
    </div>
  );
}
```

## Dependencies

- npm: `lucide-react`
- Kura registry items: `kura-theme`, `button`, `utils`
- Related: `run-header`, `diff-view`, `file-changes`, `task-list`
