Elements

Elements · Messages

Regenerate with

Fork the same turn to a different model instead of rolling the same dice.

fig. 01

Installation

npx shadcn@latest add "@assistant-ui/elements-regenerate-menu"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

RegenerateMenu turns the regenerate action into a small picker: open it, see the alternatives with the current one marked, and pick one to try again. With a runtime picking an option starts a new run carrying your choice; standalone you own the open state and the pick.

Getting started

aui.message.reload(config) starts a new run for the current turn, and config.runConfig.custom reaches your model adapter untouched. assistant-ui itself has no built-in "model" field on a run; on a useLocalRuntime plus ChatModelAdapter runtime, this is exactly what ChatModelAdapter.run(options) receives as options.runConfig, so a custom adapter reads the pick back out for that one run only, without changing what later turns use.

Wire picks to reload

components/assistant-ui/elements/thread.aui.tsx
"use client";

import { useState } from "react";
import { useAui } from "@assistant-ui/react";
import { RegenerateMenu, type RegenerateOption } from "@/components/assistant-ui/elements/regenerate-menu";

const MODELS: RegenerateOption[] = [
  { id: "opus", label: "Try again with Opus 5", detail: "slower" },
  { id: "sonnet", label: "Try again with Sonnet 5", detail: "balanced" },
  { id: "haiku", label: "Try again with Haiku 4.5", detail: "fastest" },
];

function RegenerateWith({ currentModel }: { currentModel: string }) {
  const aui = useAui();
  const [open, setOpen] = useState(false);

  return (
    <RegenerateMenu
      options={MODELS}
      open={open}
      currentId={currentModel}
      onOpenChange={setOpen}
      onPick={(id) => {
        aui.message.reload({ runConfig: { custom: { model: id } } });
        setOpen(false);
      }}
    />
  );
}

Read the pick on the model side

lib/model-adapter.ts
import type { ChatModelAdapter } from "@assistant-ui/react";

export const adapter: ChatModelAdapter = {
  async run({ messages, runConfig, abortSignal }) {
    const model = (runConfig.custom?.model as string | undefined) ?? "sonnet";
    // call `model` for this run
  },
};

Anatomy

<div data-slot="regenerate-menu">
  <button aria-expanded aria-label="Regenerate with a different model" />
  {open && (
    <div>
      {/* one row per option: its label, and "current" in place of the detail for the active one */}
    </div>
  )}
</div>

No outside-click, Escape, or keyboard roving is built in; every row is a plain button. The active option (option.id === currentId) shows the literal text "current" in place of its own detail, so a detail string that happens to read "current" is indistinguishable from the real thing.

Examples

Regenerating with the same model

reload() with no config just retries the current turn as-is; the override in runConfig.custom is only needed when you actually want a different model:

<button onClick={() => aui.message.reload()}>Try again</button>

Restyle the trigger and menu

Both lanes take className on the root. The dropdown reads the shared floating surface from surfaces.tsx, the same token popovers and tooltips use elsewhere.

<RegenerateMenu className="gap-1" /* ... */ />

API reference

Message state

Selector / methodTypeDescription
aui.message.reload(config?)(config?: { runConfig?: RunConfig }) => voidStarts a new run for this turn. runConfig.custom reaches your ChatModelAdapter as options.runConfig.custom, untouched.
RunConfig{ custom?: Record<string, unknown> }The shape reload's runConfig accepts. assistant-ui has no built-in model field; the key you read back is your own convention.