Meet Frontfriend.Your brand. Your rules. Your agents, connected.Explore what’s new

Checkpoint

A conversation checkpoint that restores the thread from a saved turn.

Interactive preview

The shared preview uses React Base. The source below uses Radix UI. Each example runs with local data.

Installation

Complete project setup with react/radix, then install these components:

pnpm exec frontfriend download message marker button bubble -f react --primitive radix

Restore keeps the checkpoint message. Continue adds a new turn after it. The marker remains visible, and the status text announces the result. In an application, restore the matching application state as well as the messages.

Usage

Complete example

import { Message, MessageContent } from "@/components/ui/message";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import { Marker, MarkerContent } from "@/components/ui/marker";
import { Button } from "@/components/ui/button";
import * as React from "react";
export default function AiCheckpointExample() {
  const [messages, setMessages] = React.useState([
    { id: "user-1", role: "user", text: "Build the page layout." },
    { id: "assistant-1", role: "assistant", text: "The page layout is ready." },
    {
      id: "user-2",
      role: "user",
      text: "Add a footer with the project links.",
    },
    {
      id: "assistant-2",
      role: "assistant",
      text: "I added the footer and its links.",
    },
  ]);
  const [status, setStatus] = React.useState("");
  const continueRef = React.useRef<HTMLButtonElement>(null);
  return (
    <div
      style={{
        width: "100%",
        maxWidth: 560,
        minWidth: 0,
        marginInline: "auto",
        display: "flex",
        flexDirection: "column",
        gap: 20,
        paddingBlock: 16,
        fontSize: "var(--text-sm)",
        lineHeight: 1.5,
      }}
    >
      {messages.map((message) => (
        <React.Fragment key={message.id}>
          <Message align={message.role === "user" ? "end" : "start"}>
            <MessageContent>
              {message.role === "user" ? (
                <Bubble align="end" variant="muted">
                  <BubbleContent>{message.text}</BubbleContent>
                </Bubble>
              ) : (
                <p>{message.text}</p>
              )}
            </MessageContent>
          </Message>
          {message.id === "assistant-1" && (
            <Marker variant="separator">
              <MarkerContent>
                <span
                  style={{
                    display: "inline-flex",
                    gap: 8,
                    alignItems: "center",
                    flexWrap: "wrap",
                  }}
                >
                  {"Layout saved"}
                  {messages.length > 2 && (
                    <Button
                      size="xs"
                      variant="outline"
                      aria-label={`Restore to ${"Layout saved"}`}
                      onClick={() => {
                        setMessages((previous) => previous.slice(0, 2));
                        setStatus("Restored. Later turns removed.");
                        continueRef.current?.focus();
                      }}
                    >
                      Restore
                    </Button>
                  )}
                </span>
              </MarkerContent>
            </Marker>
          )}
        </React.Fragment>
      ))}
      <div>
        <Button
          ref={continueRef}
          variant="outline"
          size="sm"
          onClick={() => {
            setMessages((previous) => [
              ...previous,
              {
                id: crypto.randomUUID(),
                role: "user",
                text: "Continue from this layout.",
              },
              {
                id: crypto.randomUUID(),
                role: "assistant",
                text: "I will keep the saved layout and add the next change.",
              },
            ]);
            setStatus("Added a new exchange.");
          }}
        >
          Continue
        </Button>
      </div>
      <p
        role="status"
        style={{
          position: "absolute",
          width: 1,
          height: 1,
          padding: 0,
          margin: -1,
          overflow: "hidden",
          clipPath: "inset(50%)",
          whiteSpace: "nowrap",
          border: 0,
        }}
      >
        {status}
      </p>
    </div>
  );
}

Components and API

This composition has no separate component API. Use the APIs for its installed components.

ComponentTarget reference
message Radix UI API
marker Radix UI API
button Radix UI API
bubble Radix UI API