Elements · Messages
Regenerate with
Fork the same turn to a different model instead of rolling the same dice.
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 initThen 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.
npx shadcn@latest add "@assistant-ui/elements-regenerate-menu"Props-driven: no runtime or provider required.
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
"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
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
},
};Standalone, the menu is fully controlled: open state, the current pick, and what happens on a pick are all yours.
Hold the open and current state
"use client";
import { useState } from "react";
import { RegenerateMenu, type RegenerateOption } from "@/components/assistant-ui/elements/regenerate-menu";
const OPTIONS: RegenerateOption[] = [
{ id: "opus", label: "Try again with Opus 5", detail: "slower" },
{ id: "sonnet", label: "Try again with Sonnet 5", detail: "balanced" },
];
export function Answer() {
const [open, setOpen] = useState(false);
const [model, setModel] = useState("sonnet");
return (
<RegenerateMenu
options={OPTIONS}
open={open}
currentId={model}
onOpenChange={setOpen}
onPick={(id) => {
setModel(id);
setOpen(false);
regenerate(id);
}}
/>
);
}Handle outside interaction yourself
The component has no built-in outside-click or Escape handling; it is a bare conditional render behind open. Wrap it in your own popover primitive, or listen for outside clicks yourself, if you need the menu to close on its own.
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>Nothing requires the menu to be open for a pick to happen; call onPick directly, for example from a keyboard shortcut bound to the current model's id.
onPick(currentId);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 / method | Type | Description |
|---|---|---|
aui.message.reload(config?) | (config?: { runConfig?: RunConfig }) => void | Starts 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. |
RegenerateMenu
| Prop | Type | Default | Description |
|---|---|---|---|
options | readonly RegenerateOption[] | required | The alternatives offered. |
open | boolean | required | Whether the dropdown is shown. |
currentId | string | required | Id shown as "current". A non-matching id marks no row current. |
onOpenChange | (open: boolean) => void | Called when the trigger is pressed, with the next open value. | |
onPick | (id: string) => void | Called when a row is pressed. Does not close the menu itself. | |
className | string | Merged onto the root. |
RegenerateOption
| Field | Type | Description |
|---|---|---|
id | string | |
label | string | |
detail | string | Shown for every option except the current one. |
All other div props are forwarded to the root.