assistant-ui Copilots
Always consult assistant-ui.com/llms.txt for the latest API.
Copilots ground an assistant in your running app: steer it with instructions, feed it lazy app state, let it read rendered components, click and edit UI, and read or update persistent interactable state.
References
- ./references/instructions.md -- useAssistantInstructions
- ./references/model-context.md -- useAssistantContext and imperative modelContext().register
- ./references/visible.md -- makeAssistantVisible
- ./references/interactables.md -- interactable components
Orientation
All APIs ship from @assistant-ui/react and run inside AssistantRuntimeProvider. Pick the smallest tool for the job:
What do you need the assistant to know or do?
├─ Steer behavior with a system prompt → useAssistantInstructions("...")
├─ Feed read-only app state (page, selection, cart) → useAssistantContext({ getContext })
├─ Let it read / click / edit a rendered component → makeAssistantVisible(Component, { clickable, editable })
├─ Read AND write persistent component state via tools → useAssistantInteractable(name, config)
└─ Register instructions + tools together imperatively → useAui().modelContext().register({ getModelContext })
Instructions and context are the lightweight starting point. Reach for makeAssistantVisible when the assistant needs to perceive or drive existing DOM, and for interactables when it needs structured two-way state it can mutate through auto-generated update_{name} tools.
import { useAssistantInstructions, useAssistantContext } from "@assistant-ui/react";
function CheckoutCopilot() {
useAssistantInstructions("You help users complete checkout. Be concise.");
useAssistantContext({ getContext: () => `Current page: ${window.location.href}` });
return null;
}
getContext is evaluated fresh each time the model context is read, so it always reflects current state. Register imperatively when you need instructions and tools in one provider:
import { useAui, tool } from "@assistant-ui/react";
import { useEffect } from "react";
function SearchCopilot() {
const aui = useAui();
useEffect(() => {
return aui.modelContext().register({
getModelContext: () => ({
system: "You are a helpful search assistant.",
tools: { search: mySearchTool },
}),
});
}, [aui]);
return null;
}
register returns an unsubscribe function; returning it from useEffect cleans up the provider on unmount. Multiple providers compose: system strings concatenate and tools maps merge.
Common Gotchas
Assistant ignores instructions or context
- The hook or
registercall must run insideAssistantRuntimeProvider. - For
useAui().modelContext().register, call it inuseEffectand return the result so it unsubscribes; registering in render leaks providers.
Context is stale
- Use the
getContextcallback form, not a captured value. It is re-read at send time, so closures over fresh state work; a precomputed string will not update.
makeAssistantVisible does nothing
- Without options the component is read-only (exposes its
outerHTML). Pass{ clickable: true }to allow clicks and{ editable: true }for<input>/<textarea>editing. Nested visible components expose only the outermost.
Interactable resets its state on every render
- Define
stateSchemaandinitialStateoutside the component (or memoize). A new schema each render re-registers the interactable and wipes its state. Register the scope withuseAui({ interactables: Interactables() }).
Partial updates drop fields
- The AI sends only the fields it changes and the merge is shallow (one level deep); nested objects are replaced, not deep-merged.
Related Skills
- tools -- frontend and backend tools and custom tool-call UI (
makeAssistantTool,useAssistantTool,makeAssistantToolUI). - runtime -- runtime creation,
AssistantRuntimeProvider, and reading or mutating thread state (useAui,useAuiState,useAuiEvent).