Migrating from flat-props ChatProvider
Move from the legacy flat-props chat component to AgentInterface and its two adapter objects — with a complete old-to-new mapping and the behavior changes to watch for.
The legacy chat component took its backend wiring as a flat bag of props — apiUrl, streamProtocol, processMessage, threadApiUrl, fetchThreadList, loadThread, and a dozen more — directly on the component. AgentInterface replaces all of that with two adapter objects: an llm for producing replies and a storage for persistence. The component's prop surface shrinks to something small and stable, and each adapter is testable in isolation — you can unit-test a ChatLLM or ChatStorage without rendering anything.
The whole migration, in one idea: the flat backend props are removed (not deprecated), and their values move into fetchLLM({ ... }) for the LLM and restStorage({ ... }) for threads.
@openuidev/react-ui: AgentInterface, the fetchLLM / restStorage / defineArtifactRenderer factories, the stream adapters, the message formats, and hooks like useArtifactList.The whole diff
If you were on the common setup — POST to a route, REST-backed threads — the migration is: build two adapters, pass two props.
Before:
// legacy flat-props chat component
<LegacyChat
apiUrl="/api/chat"
streamProtocol="openai"
messageFormat="openai"
threadApiUrl="/api/threads"
/>;After:
import { AgentInterface, fetchLLM, restStorage, openAIAdapter, openAIMessageFormat } from "@openuidev/react-ui";
const llm = fetchLLM({
url: "/api/chat",
streamAdapter: openAIAdapter(),
messageFormat: openAIMessageFormat,
});
const storage = restStorage({
baseUrl: "/api/threads",
messageFormat: openAIMessageFormat,
});
<AgentInterface llm={llm} storage={storage} />;Your route handler doesn't change: fetchLLM POSTs the same { threadId, messages } to url, and restStorage calls the same thread endpoints threadApiUrl did. Everything else on this page is the field-by-field mapping behind those two factory calls.
Old → new at a glance
| Legacy flat prop | New home | Notes |
|---|---|---|
apiUrl | fetchLLM({ url }) | The LLM endpoint moves into the factory's url. |
streamProtocol | fetchLLM({ streamAdapter }) | String enum → a stream-adapter factory call (openAIAdapter()). |
messageFormat | fetchLLM({ messageFormat }) (and restStorage({ messageFormat })) | String enum → a MessageFormat value (openAIMessageFormat). |
processMessage | ChatLLM.send | Custom send logic becomes a send implementation. abortController → the signal you receive. |
threadApiUrl | restStorage({ baseUrl }) | The thread REST root moves into restStorage. |
fetchThreadList | storage.thread.listThreads | Now takes an optional cursor; returns { threads, nextCursor? }. |
createThread | storage.thread.createThread | Receives the first UserMessage, returns the new Thread. |
updateThread | storage.thread.updateThread | Takes a full Thread, returns the updated Thread. |
deleteThread | storage.thread.deleteThread | Takes an id. |
loadThread | storage.thread.getMessages | Renamed; takes a threadId, returns Message[]. |
appRenderers | artifactRenderers | Prop rename; array of renderer configs. |
defineAppRenderer | defineArtifactRenderer | kind → type; toolName is string | string[]; parser returns { props, meta }. |
useAppList | useArtifactList | Hook rename; per-thread artifact registry. |
Artifact* panel APIs | DetailedView* | The in-thread panel hooks were renamed around "detailed view." |
| legacy chat component | AgentInterface | Component rename, same package. |
LLM props → llm
Four old props collapse into one fetchLLM call. Two things change beyond the renames:
streamProtocolwas a string;streamAdapteris a factory call. Pick the adapter matching your provider's wire format and call it —openAIAdapter(), not the bare reference. The options areagUIAdapter,openAIAdapter,openAIReadableStreamAdapter,openAIResponsesAdapter, andlangGraphAdapter.messageFormatwas a string; it's now a value. UseopenAIMessageFormat,openAIConversationMessageFormat(the one that pairs withopenAIResponsesAdapter()),langGraphMessageFormat, oridentityMessageFormat(the default — the canonicalMessagearray, unchanged).
processMessage → a custom ChatLLM
If processMessage did something fetchLLM can't express — a non-fetch transport, a websocket, bespoke request shaping — implement the ChatLLM interface directly instead of calling fetchLLM.
Before — processMessage received an abortController:
<LegacyChat
streamProtocol="openai"
processMessage={async (messages, { abortController }) => {
return fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ messages }),
signal: abortController.signal,
});
}}
/>;After — send receives the signal directly, and the parser moves onto the object as streamProtocol:
import { AgentInterface, type ChatLLM, openAIAdapter } from "@openuidev/react-ui";
const llm: ChatLLM = {
streamProtocol: openAIAdapter(),
async send({ threadId, messages, signal }) {
return fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ threadId, messages }),
signal, // was abortController.signal
});
},
};
<AgentInterface llm={llm} />;The behavior change to internalize: you no longer create or own an AbortController. AgentInterface owns it and hands you the signal — wired to the UI's stop control — in every send call. Just forward it.
Thread props → storage
threadApiUrl plus the per-operation callbacks become a single ChatStorage whose thread member holds five methods.
The REST case → restStorage
If your old setup pointed threadApiUrl at a REST backend, the migration is one factory call:
import { restStorage } from "@openuidev/react-ui";
const storage = restStorage({ baseUrl: "/api/threads" }); // was threadApiUrl
<AgentInterface llm={llm} storage={storage} />;restStorage hits the exact endpoints the old threadApiUrl prop did — GET {baseUrl}/get, POST {baseUrl}/create, GET {baseUrl}/get/{threadId}, PATCH {baseUrl}/update/{id}, DELETE {baseUrl}/delete/{id} — so an existing backend keeps working. Pass messageFormat here too if your backend stores messages in a provider shape.
storage at all, AgentInterface uses an internal in-memory store — fine for prototyping, but wiped on reload.Custom thread callbacks → storage.thread.*
If you supplied individual thread callbacks, each maps to a method on storage.thread:
import { AgentInterface, type ChatStorage } from "@openuidev/react-ui";
const storage: ChatStorage = {
thread: {
listThreads: fetchThreadList, // now takes an optional cursor, returns { threads, nextCursor? }
createThread, // receives the first UserMessage, returns the new Thread
getMessages: loadThread, // was loadThread — takes threadId, returns Message[]
updateThread, // takes a full Thread, returns the updated Thread
deleteThread, // takes an id
},
};
<AgentInterface llm={llm} storage={storage} />;Two renames carry behavior changes: fetchThreadList → listThreads now takes an optional cursor and must return { threads, nextCursor? } (cursor pagination for the sidebar's "load more"); loadThread → getMessages is the same job, just renamed for symmetry. If your callbacks already match these shapes, hand them over directly; otherwise wrap them.
Renderers: appRenderers → artifactRenderers
The "app renderer" concept was renamed to artifact renderer throughout — the prop, the factory, and the parser contract all changed.
import { AgentInterface, defineArtifactRenderer } from "@openuidev/react-ui";
const codeArtifactRenderer = defineArtifactRenderer({
type: "code_artifact", // was kind
toolName: "create_code_artifact", // now string | string[]
parser: ({ args, response }, { isStreaming }) => {
const data = response as CodeArtifact | null;
if (!data) return null; // tolerate partial data while streaming
return {
props: data, // props AND meta now come from one return value
meta: isStreaming ? null : { id: `code:${data.title}`, version: 1, heading: data.title },
};
},
preview: (props, controls) => <CodeCard {...props} onOpen={controls.open} />,
actual: (props) => <CodeBlock language={props.language} codeString={props.code} />,
});
<AgentInterface llm={llm} artifactRenderers={[codeArtifactRenderer]} />;The behavior changes inside the renderer:
kind→type. A literal string that links the renderer to its stored artifacts'type.toolNameis nowstring | string[]. Register one renderer for several tools by passing an array. Names are literal only — no RegExp. First registration wins on a duplicatetoolName.- The parser returns
{ props, meta }(ornull), not props alone.metais{ id, version, heading } | null: return the object to render and register the artifact in the thread (so it appears in the Workspace rail and artifact lists); returnnullformetato render without registering (the common move while streaming). Returningnullfrom the parser entirely skips rendering. - The parser must tolerate partial data. It's called on every stream update —
responseisnulluntil the result lands, andargsmay be a partial JSON string. Guard accordingly.
Hooks and panel APIs
useAppList() → useArtifactList(filter?), imported from @openuidev/react-ui. It returns the per-thread artifact registry, optionally filtered by type:
import { useArtifactList } from "@openuidev/react-ui";
const artifacts = useArtifactList();
const codeArtifacts = useArtifactList({ type: ["code_artifact"] });The in-thread panel hooks that used to be named around "Artifact" are now named around detailed view: useActiveDetailedView(), useDetailedView(viewId), useDetailedViewStore(), useDetailedViewPortalTarget(). If you reached into the old Artifact* panel hooks, swap to these.
Migration checklist
- Rename the legacy chat component →
AgentInterface(same@openuidev/react-uipackage). - Build
llm = fetchLLM({ url, streamAdapter, messageFormat })from your oldapiUrl/streamProtocol/messageFormat. Call the adapter (openAIAdapter()). - If you had a custom
processMessage, implementChatLLMdirectly and forward thesignal— drop your ownAbortController. - Build
storage = restStorage({ baseUrl })fromthreadApiUrl, or assemblestorage.thread.*from yourfetchThreadList(→listThreads),createThread,updateThread,deleteThread, andloadThread(→getMessages). - Pass
llm(required) andstorage(optional) toAgentInterface. - Rename
appRenderers→artifactRenderersanddefineAppRenderer→defineArtifactRenderer; changekind→type, allowtoolNamearrays, and return{ props, meta }from each parser. - Rename
useAppList→useArtifactListand anyArtifact*panel hooks →DetailedView*. - Delete every remaining flat backend prop — they're gone. If TypeScript flags an unknown prop, find it in the table above and move it into the right adapter.
Related
AgentInterface props
Adapters & formats
fetchLLM, restStorage, stream adapters, and message formats.defineArtifactRenderer
meta shape.Conversations
llm and storage drive a thread end to end.Hooks
useArtifactList, the DetailedView* hooks, and more.