Elements

Elements · Thread

Connection state

The socket drops, the run keeps going on the server, and the stream is picked back up.

Connection lost. The run kept going on the server.
fig. 01 · plays once, replay from the corner

Installation

npx shadcn@latest add "@assistant-ui/elements-connection-state"
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.

A connection state banner tells the reader when the transport under a run has hiccuped: the socket dropped, the run is still going server side, or the stream just picked back up. With a runtime this maps to catching a transport failure and resuming the run; standalone you drive the phase directly.

Getting started

assistant-ui doesn't expose a single connection field on thread state, because a dropped socket is something an adapter observes, not a concept the runtime itself models. What it exposes instead is a resumable run: catch the failure where your adapter reports it, show the banner, then call resumeRun to pick the same run back up.

Track the phase around the run

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

import { useEffect, useState } from "react";
import { useAui, useAuiState } from "@assistant-ui/react";
import { ConnectionState, type ConnectionPhase } from "./connection-state";

function ConnectionBanner({ resumeFromId }: { resumeFromId: string | null }) {
  const aui = useAui();
  const isRunning = useAuiState((s) => s.thread.isRunning);
  const [phase, setPhase] = useState<ConnectionPhase>("online");

  useEffect(() => {
    if (phase === "reconnecting" && isRunning) setPhase("resumed");
  }, [phase, isRunning]);

  return (
    <ConnectionState
      phase={phase}
      onRetry={() => {
        setPhase("reconnecting");
        aui.thread.resumeRun({ parentId: resumeFromId });
      }}
    />
  );
}

Where phase first flips to "dropped" is adapter-specific: wherever your transport surfaces a network failure, which ConnectionState itself has no opinion on. resumeRun returns no promise to await; watch isRunning flip back to true to know the reconnect actually landed.

Resume instead of restarting

resumeRun takes the same config shape as startRun, a parentId (and optional sourceId and runConfig), not a raw connection handle. Whether that continues the same generation or starts a fresh one is up to the adapter: assistant-ui's resumable transports retain the run server side and replay it rather than asking the model again:

aui.thread.resumeRun({ parentId: resumeFromId, sourceId: null });

Anatomy

<div data-slot="connection-state">
  {/* dropped: icon, message, Reconnect button */}
  {/* reconnecting: spinner, message, attempt count */}
  {/* resumed: check, message, resumed token count */}
</div>

ConnectionState renders nothing at all while phase is "online", the only phase without a visible banner. The other three phases are mutually exclusive and each show a different icon and message; attempt and resumedTokens are optional counters shown only when you pass them.

Examples

Every phase

<ConnectionState phase="dropped" onRetry={() => {}} />
<ConnectionState phase="reconnecting" attempt={2} />
<ConnectionState phase="resumed" resumedTokens={340} />

Silence the online case explicitly

Since phase="online" already renders null, conditionally mounting the banner at all is optional; leaving it mounted lets it animate in and out on its own fade and slide transition each time the phase changes away from "online".

API reference

Thread methods

MethodTypeDescription
aui.thread.resumeRun(config)(config: CreateResumeRunConfig) => voidReconnects to (or restarts, depending on the adapter) the run for config.parentId. Same shape as startRun.
aui.thread.cancelRun()() => voidCancels the in-flight run outright, rather than resuming it.

There is no built-in selector for "the socket is dropped": that phase lives in your adapter and the app state around it, not in AssistantState.