Elements · Messages
Stopped run
You pressed stop. The half-written answer stays, and continuing is one tap away.
Installation
npx assistant-ui@latest add elements-stopped-runThe CLI reads react-native from your package.json and installs from the native registry tree. The element takes the same props as the React one; the React Native elements guide covers setup and what changes on a phone.
npx shadcn@latest add "@assistant-ui/elements-stopped-run"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-stopped-run"Props-driven: no runtime or provider required.
StoppedRun shows what a cancelled generation leaves behind: the words that arrived before the stop, a small reason badge, and a way to pick it back up or let it go. With a runtime the words and the reason come from the message that was cancelled; standalone you pass in whatever you captured.
Getting started
A cancelled message settles into status.type === "incomplete" with status.reason === "cancelled". Its content up to that point is still there; only the run stopped.
Detect a cancelled message and wire its actions
"use client";
import { useAui, useAuiState } from "@assistant-ui/react";
import { StoppedRun } from "@/components/assistant-ui/elements/stopped-run";
function CancelledNotice() {
const aui = useAui();
const status = useAuiState((s) =>
s.message.role === "assistant" ? s.message.status : undefined,
);
const words = useAuiState((s) =>
s.message.role === "assistant"
? s.message.content
.filter((part): part is { type: "text"; text: string } => part.type === "text")
.flatMap((part) => part.text.split(" "))
: [],
);
if (status?.type !== "incomplete" || status.reason !== "cancelled") return null;
return (
<StoppedRun
words={words}
reason="stopped by you"
onContinue={() => aui.message.reload()}
onDiscard={() => aui.message.delete()}
/>
);
}Understand what Continue really does
reload() starts a new run for the same turn; it does not resume the exact cut-off text, since assistant-ui has no built-in prefix-continuation primitive. A backend that supports literal continuation needs the partial text itself, for example passed through reload's runConfig.custom and read back inside your ChatModelAdapter.
Standalone, StoppedRun is presentational: you decide what counts as stopped and supply the words already streamed.
Freeze the words when a stream stops
"use client";
import { useState } from "react";
import { StoppedRun } from "@/components/assistant-ui/elements/stopped-run";
export function Answer() {
const [words, setWords] = useState<string[]>([]);
const [stopped, setStopped] = useState(false);
function onStop() {
controller.abort();
setStopped(true);
}
if (!stopped) return null;
return (
<StoppedRun
words={words}
reason="stopped by you"
onContinue={() => resume(words)}
onDiscard={() => setStopped(false)}
/>
);
}Wire Continue and Discard
Neither prop has a default behavior; both are no-ops until you pass a handler. onContinue is where you'd fetch the rest of the answer; onDiscard is where you'd drop it from your own list.
Anatomy
<div data-slot="stopped-run">
<p>{/* words joined by a space, with a blinking cursor after them */}</p>
<div>
<span>{/* the reason badge, e.g. "stopped by you" */}</span>
<button>Continue</button>
<button>Discard</button>
</div>
</div>The cursor is decorative (aria-hidden) and always renders while the component is mounted; there is no internal "still streaming" state to turn it off. reason is freeform text, not a fixed enum, so the badge can describe any way a run ends short of completion, not only a manual stop.
Examples
Other reasons a run stops
status.reason covers more than a manual stop: "length", "content-filter", "tool-calls", and "error" are all real values alongside "cancelled". The same component fits any of them with a different label:
const LABEL: Record<string, string> = {
cancelled: "stopped by you",
length: "hit the length limit",
"content-filter": "blocked by a content filter",
error: "failed partway through",
};
<StoppedRun words={words} reason={LABEL[status.reason] ?? "stopped early"} />Nothing about the component ties reason to cancellation specifically; any short label for why the text stopped early is a valid value.
<StoppedRun words={words} reason="connection lost" />Where discard goes
const onDiscard = () => aui.message.delete();delete() removes the message from the thread. Its Promise<void> return exists because a persisted runtime's delete is asynchronous; you rarely need to await it from a click handler.
Standalone, discarding is whatever removes the draft from your own list; the component only calls the handler.
const onDiscard = () => setDrafts((prev) => prev.filter((d) => d.id !== draftId));Restyle the badge and actions
Both lanes take className on the root. The badge reads the shared field and mono surfaces from surfaces.tsx.
<StoppedRun className="max-w-none" /* ... */ />API reference
Message state
| Selector / method | Type | Description |
|---|---|---|
s.message.status | MessageStatus | undefined | { type: "incomplete", reason: "cancelled" } when the message stopped because you pressed stop. Other incomplete reasons: "length", "content-filter", "tool-calls", "error", "other". |
aui.thread.cancelRun() | () => void | Stops the active run; the streaming message settles into the incomplete status above. |
aui.message.reload(config?) | (config?: { runConfig?: RunConfig }) => void | Starts a new run for this turn. Restarts generation; it does not resume the cut-off text. |
aui.message.delete() | () => void | Promise<void> | Removes the message from the thread. |
StoppedRun
| Prop | Type | Default | Description |
|---|---|---|---|
words | readonly string[] | required | The words already streamed, joined with spaces and given a trailing cursor. |
reason | string | required | Freeform label shown in the badge. |
onContinue | () => void | Called when Continue is pressed. | |
onDiscard | () => void | Called when Discard is pressed. | |
className | string | Merged onto the root. |
All other div props are forwarded to the root.