Agent Interface SDK#692
Merged
Merged
Conversation
…istries + hooks refactor: rename Artifact API to DetailedView across react-headless and react-ui
panel via defineAppRenderer (react-headless). Renderers match by toolName (literal string or RegExp), captured at ChatProvider mount with first-wins on duplicates and dev-mode warnings on ambiguity. ToolMessageRenderer (react-ui) dispatches each tool message to its matching renderer or falls back to the default ToolResult. Wired into Shell, CopilotShell, and BottomTray Thread components. Args and response are passed raw to parsers; isStreaming is reserved (always false) until streaming protocol lands.
… migration
Map tool-call results to custom inline preview + detailed-view side panel
via renderers, and migrate openui-artifact-demo to demonstrate.
react-headless:
- defineAppRenderer / defineArtifactRenderer factories tag the config with
`kind` so a matched renderer registers in the apps or artifacts
ThreadContext slice
- AppRenderersContext + useAppRenderer for toolName lookup at mount
(literal Map + regex array; first-wins; dev-mode duplicate + ambiguity
warnings)
- processStreamedMessage handles TOOL_CALL_RESULT: creates a ToolMessage
via createMessage (signature broadened from AssistantMessage to Message)
- TEXT_MESSAGE_START is now a no-op — the previous delete+recreate broke
ordering when tool messages had already been appended; persistence
layers should map ids on save instead. Drop the unused deleteMessage
callback from processStreamedMessage's interface.
react-ui:
- ToolMessageRenderer dispatches matched tool messages; RendererInstance
runs parser → meta → register/unregister (routed by `kind`) and renders
preview inline + actual via DetailedViewPanel
- GenUIAssistantMessage renders matched tool messages outside
BehindTheScenes so the inline preview appears in the chat surface
- withChatProvider forwards `appRenderers` to ChatProvider (was missing
from the prop allowlist)
examples/openui-artifact-demo:
- Replace ArtifactCodeBlock openui-lang component with a `create_code_block`
tool + codeBlockRenderer (defineArtifactRenderer)
- enrichedArgsAdapter splits the backend's {_request, _response} envelope
into standard TOOL_CALL_ARGS + TOOL_CALL_RESULT events so processStreamed
Message and the bridge see the same shape as a standard agentic flow
- Override chat library preamble so the LLM is allowed to mix openui-lang
with tool calls
- Add create_code_block tool definition (declarative; returns {ok:true})
- "Workspace" heading with collapse/expand toggle
- Apps section reads useAppList(); artifacts section reads useArtifactList()
- Each item is the latest version per id; clicking activates the matching
DetailedView via setActiveDetailedView("${id}:${version}")
- Active item highlighted; empty states per section + overall hint
- Auto-collapses when a DetailedView opens (focus on the view) and
auto-expands when it closes; manual toggles between transitions are
preserved
- Hidden on mobile layout (desktop only for v1)
- Uses existing cssUtils tokens (spacing, typography, colors, radius)
- Wired into FullScreen via ComposedStandalone
- ShellStore gains isWorkspaceOpen / setIsWorkspaceOpen
handling
AppRenderers now fire on tool calls whose args are still streaming, before the
tool result is paired in. Same React instance persists across the
streaming → completed transition (keyed by toolCall.id), so renderers can
swap between partial and final UI without remounting.
- react-ui: ToolMessageRenderer accepts ToolMessage|null and signals
isStreaming when null; RendererInstance accepts an isStreaming prop and
propagates to controls + meta ctx; GenUIAssistantMessage dispatches every
tool call (paired or not).
- react-headless: openAIResponsesAdapter handles function_call_output items
in response.output_item.added → yields TOOL_CALL_RESULT. Backends that
inject synthetic events for server-side tool execution can now surface
tool results to the SDK store (OpenAI itself silently absorbs
function_call_output items into the conversation without echoing them).
- AppRendererControls.isStreaming JSDoc rewritten — was "always false in
v1; reserved for streaming protocol", now describes actual behavior.
AppRendererConfig.parser JSDoc notes args may be partial JSON and response
may be null during streaming. AppRendererConfig.meta documents the
ctx.isStreaming parameter.
…es APIs
behind the SDK's AG-UI wire shape.
- Persistent chat: thread storage via OpenAI Conversations API with a JSON
file index at .data/threads.json (Conversations API has no list method)
- Hosted LLM: route streams from openai.responses.create with
conversation: threadId so OpenAI auto-persists user input + responses
- Tool execution: manual loop (Responses API has no runTools helper),
capped at MAX_TOOL_TURNS, with a synthetic response.output_item.added
event injected after each tool execution so the SDK surfaces results live
- Streaming artifact: create_code_artifact tool with strict mode + property
order (language → title → code) and a partial-JSON parser, rendered via
defineArtifactRenderer using controls.isStreaming
- Frontend wires openAIResponsesAdapter + openAIConversationMessageFormat;
processMessage sends only the latest message since OpenAI holds history
Collapse the legacy flat-props ChatProvider config behind two
adapter interfaces:
- ChatStorage (thread, pinning?, share?) drives the storage channel
- ChatLLM (send, streamProtocol) drives the LLM channel
Drops apiUrl, threadApiUrl, processMessage, loadThread, fetchThreadList,
createThread/deleteThread/updateThread props, messageFormat, and the
top-level streamProtocol prop. Their behavior is now configured per
adapter; the bundled fetchLLM factory captures the previous default
fetch + messageFormat path.
react-headless:
- Add src/adapters: ChatStorage / ChatLLM / Thread/Pinning/ShareStorage
types, fetchLLM factory, internal _defaultStorage (in-memory, not
exported) used when ChatProvider has no storage prop
- Rewrite createChatStore to consume { storage, llm }; thread CRUD now
delegates to storage.thread.*, message send to llm.send
- ChatProvider props collapse to { storage?, llm, appRenderers, children }
- Guard process.env reads with `typeof process !== "undefined"` so the
provider/registry/detailed-view store work in non-Node runtimes
- Migrate createChatStore / threadContextSwitch / detailedViewThreadSwitch
tests behind a shared makeStore() helper; drop the apiUrl /
threadApiUrl / messageFormat / ephemeral-thread describe blocks (those
paths now live in the fetchLLM adapter)
react-ui:
- withChatProvider HOC: replace prop-key filtering with a destructure
over the new shape
- Add shared __test-helpers/mockChat.ts (makeMockStorage, makeMockLLM,
mockSSEResponse) used by every story
- Migrate Shell, BottomTray, CopilotShell, and OpenUIChat stories to
<ChatProvider storage={...} llm={...}>
examples/openui-artifact-demo: import fetchLLM from react-headless.
# Conflicts: # packages/react-ui/src/components/CopilotShell/Thread.tsx # packages/react-ui/src/components/Shell/Sidebar.tsx # packages/react-ui/src/components/Shell/thread.scss # packages/react-ui/src/components/index.scss # pnpm-lock.yaml
REST ChatStorage hitting the exact endpoints the removed threadApiUrl prop used (/get · /create · /get/:id · /update/:id · /delete/:id), so consumers migrate by swapping one prop for one factory call. 8 tests cover URL/method/body/headers per method, messageFormat round-trip, and res.ok error surfacing. - store._nextCursor: `any` → `string | undefined`, consistent with the ThreadStorage interface's `string` cursor. - react-ui re-exports the adapter construction surface (fetchLLM, restStorage, ChatStorage, ChatLLM, ThreadStorage, Pinning/Share types, FetchLLMOptions, RestStorageOptions) so AgentInterface consumers don't need to reach into @openuidev/react-headless.
# Conflicts: # pnpm-lock.yaml
renderers, per-thread Workspace rail
Storage (react-headless):
- ArtifactStorage channel on ChatStorage.artifact — list (server-side
name/type filters, string-cursor pagination) · get · update({id,
content}) for editable artifacts. ArtifactSummary carries a required
threadId (drives "go to thread"); Artifact.content must match the
tool-response shape so renderers parse both sources identically.
- ArtifactCategory config ({ name, filter: { type[] } }) on ChatProvider;
useArtifactStorage() + useArtifactCategories() hooks.
- BREAKING: PinningStorage / ShareStorage / ShareTarget removed (dead
interfaces, zero consumers).
Renderer unification (BREAKING):
- defineAppRenderer, kind, AppRendererKind, AppEntry, useAppList deleted.
- defineArtifactRenderer gains type (links renderer ↔ categories ↔ stored
artifacts) and toolName: string | string[] (RegExp matching removed);
parser + meta merged into parser(raw, {isStreaming}) → {props, meta} |
null. Registry is a flat byToolName map + byType index;
useArtifactRenderer replaces useAppRenderer.
- ThreadContext unified into a single artifacts registry; entries carry
type; useArtifactList(filter?: { type?: string[] }).
- ChatProvider prop renamed appRenderers → artifactRenderers (+ new
artifactCategories).
Artifact browser + Workspace (react-ui / AgentInterface):
- AgentInterface.ArtifactNav — one SidebarItem per category (single
"Artifacts" when uncategorized), auto-included in the default sidebar
when storage.artifact is configured.
- Reserved nav paths matched before user Routes:
artifacts/{category} → searchable list (debounced title search +
category type filter, cursor "Load more");
artifacts/{category}/{id} → full-page artifact view (Back, Go to
thread, renderer actual via byType lookup, no DetailedView).
- AgentInterface.Workspace — per-thread right rail listing registered
artifacts grouped by category; auto-shows on first artifact, hidden on
Route/browser pages and mobile; item click opens the in-thread
DetailedView (rail auto-collapses). Modes A/C.
- WorkspaceSidebar (Shell) sections now category-driven.
- react-ui re-exports the artifact surface (defineArtifactRenderer,
ArtifactStorage/Category/Summary types).
Consumers migrated: openui-responses-chat page.tsx (was still on
pre-adapter flat props; now restStorage + custom llm.send),
codeBlockRenderer + codeArtifactRenderer to merged parser + type,
withChatProvider, both tool-renderer copies, mockChat/makeStore.
Tests: registry/threadContext suites rewritten for the unified model
(83 passing). Stories: ArtifactBrowser, ArtifactBrowserUncategorized,
WithWorkspace (scripted tool-call SSE), WorkspaceCustomChildren — all
Storybook-verified.
Example (examples/openui-cloud): Next.js app showing OpenUI Cloud's two-plane integration — server-side generation with the master key, browser-direct reads/edits with a short-lived frontend token — plus artifact renderers over the @openuidev/thesys viewers and a one-call ChatStorage (openuiCloud). openuiCloud's apiBaseUrl is optional and defaults to https://api.thesys.dev. The vendored @openuidev/thesys tarball is gitignored (obtain via the registry, not this repo). SDK: - react-headless: upsert tool messages by toolCallId (artifact morph), SSE cross-read buffering + artifact_call.delta accumulation, and multi-tool reload grouping in fromItems. - react-ui: artifact viewId migration, inline-sentinel message format, scoped ShellStoreProvider under AgentInterface, and content-header round-trip in GenUIAssistantMessage.
…2e fixes - react-headless: openai-responses adapter delivers the accumulated sentinel carrier directly on artifact_call.delta (no JSON re-wrap). - react-ui: rename contentParser -> sentinelParser (unified ]]>openui: family, adds parseArtifactSentinel); RendererInstance now follows an edit's NEW version in an already-open detailed view (cross-instance activeDetailedView migration); update sentinelParser importers. - example: parseArtifact parses the sentinel carrier; artifactStorage.get returns the bare program (fixes the global artifact browser 'could not parse'); next.config aliases lucide dynamic-icon subpaths + the cross-repo @openuidev/thesys-server entry for Turbopack.
…penui into abhishek/openui-chat
# Conflicts: # pnpm-lock.yaml
… provider - Replace invalid `typography(primary, default)` (no such category — it compiled to no font/letter-spacing) with `typography(body, default)` for chat message text, user bubble, and artifact browser/view loading/error/ empty text. - SidebarHeader rendered `<img src="">` when logoUrl was empty (browsers resolve that to the page URL → spurious request + broken-image icon); skip the <img> when logoUrl is empty. - Hoist a single `Tooltip.Provider` into Container and drop the per-instance providers in AgentInterfaceTooltip/SidebarTooltip so Radix's hover-delay / skip-delay behavior is shared across all tooltips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ResizableSeparator between the chat and detailed-view panels was mouse-drag only with no accessibility. Implement the WAI-ARIA window- splitter pattern: - role="separator", aria-orientation, tabIndex=0, aria-label, and aria-controls referencing both the chat and detailed-view panels; live aria-valuenow/min/max/valuetext as % of the container. - Keyboard: Left/Right resize by 16px (Shift = 64px), Home/End snap to min/max; clamped to [420px, 80% of container]. - :focus-visible outline + visible handle. The hook now tracks the chat-panel width in a ref (kept in sync with style.width) so keyboard steps and ARIA read a stable value rather than measuring mid-CSS-transition, and resets it when the detailed view closes so the separator's mount-time ARIA is correct on reopen. Added zero-width-container guards so resizing can't collapse the panel to 0px. Verified in Storybook: arrows/Shift/Home/End, min/max clamping, ARIA values, focus ring, mouse-drag regression, and reopen-after-resize. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g-ui/core to 0.0.53
The user-message renderer only handled text and binary parts that had a
`url` (rendered as a raw <img>), silently dropping base64 (`data`)
attachments, non-image mime types, and filenames.
- Bump @openuidev/react-headless's @ag-ui/core ^0.0.45 -> ^0.0.53. This is
additive for content (InputContent gains first-class image|audio|video|
document parts alongside text|binary) and non-breaking for the streaming
protocol (no EventType members removed; react-headless typecheck + build
+ 83 tests pass).
- Extract UserMessageContent into its own module and handle the full
InputContent union exhaustively (a `default: never` guard surfaces future
variants at compile time — it's what forced the new branches after the bump):
- image/audio/video -> media elements; document/other -> downloadable file chip.
- binary routed by mimeType.
- sources (url + base64 `data`) vetted against a scheme/mime allowlist
(reject javascript:/blob:/etc.); unusable or failed-to-load media degrade
to a file chip instead of being dropped or showing a broken glyph.
- filenames surfaced; real alt/aria-label; loading="lazy".
Verified in Storybook: url/data images, audio, pdf/zip chips, and a
javascript: source were all handled correctly (no unsafe src reaches the DOM).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Navigating to a reserved `artifacts/{category}` path whose category isn't
configured (stale/renamed path, hand-edited or bookmarked URL) previously
fell through to an unfiltered `storage.list()` and rendered EVERY artifact
under the bogus category name — looking like a real category that contains
everything.
- Detect the case (`categoryName` set but not in `artifactCategories`) and
render a "No category named X" state with a "View all artifacts" button;
skip the storage query entirely.
- Remove the dead, required `title` prop from `ArtifactPreviewIllustration`
(the component is aria-hidden and never rendered it) and its call sites,
clearing a pre-existing unused-var lint error on this file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tup reproducible - page.tsx: use the useOpenuiCloudStorage() hook in-component (the module-scope openuiCloud() factory was replaced upstream in @openuidev/thesys on ap-server). - README: add a "Local dependency wiring" section so teammates can reproduce the gitignored vendor setup (build genui-sdk c1 + c1-server on ap-server, pack/copy into vendor/, pnpm install --force); refresh the hook + renderer references. - Add .env.example template + a .gitignore exception (!.env.example) so it ships while .env.local stays ignored. - next.config: turbopack alias @openuidev/thesys-server -> vendored build, plus lucide dynamic-icon stubs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete the openui-artifact-demo example app in its entirety. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsBw2b6YnH9J5GAhkDyonF
# Conflicts: # docs/content/docs/openui-lang/examples/harnesses/pi-agent-harness.mdx # examples/harnesses/pi-agent-harness/src/app/page.tsx # pnpm-lock.yaml
Delete the openui-responses-chat example app in its entirety. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsBw2b6YnH9J5GAhkDyonF
Remove a TODO and internal branch references from the DetailedViewPanel height-fix comment; keep the technical rationale. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsBw2b6YnH9J5GAhkDyonF
…penui into abhishek/openui-chat
abhithesys
previously approved these changes
Jun 29, 2026
abhithesys
approved these changes
Jun 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
This PR lands the Agent Interface SDK — a ground-up redesign of OpenUI's chat surface. It replaces the legacy flat-prop chat shells (
Shell,CopilotShell,BottomTray,OpenUIChatstories) with a single composable<AgentInterface>component built on an adapter-based headless core (storage: ChatStorage+llm: ChatLLM), reworks artifact/panel state into purpose-built stores, ships a real tool-call activity model with accessible rendering, renames theArtifact*panel API toDetailedView*, adds two reference example apps (OpenUI Cloud + OpenAI Responses), and replaces the oldchat/*docs with a full Agent Interface documentation section.It is a large, intentionally breaking change:
+19,752 / −13,719across 381 files. The deletions are almost entirely the retired shells and their stories/docs.Highlights
ChatProviderno longer takesapiUrl/threadApiUrl/streamProtocol/messageFormat+ 6 callbacks. It now takesllm: ChatLLM(required) andstorage?: ChatStorage(defaults to in-memory). Two batteries-included factories cover the common case:fetchLLM({ url, streamAdapter })andrestStorage({ baseUrl }).<AgentInterface>provides an opinionated two-panel desktop layout (collapsible sidebar + thread + resizable detailed-view panel) and a mobile slide-over, all overridable through a static slot namespace (AgentInterface.Sidebar,.Composer,.Workspace,.Route,.Welcome, …).ToolActivitydiscriminated union (streaming → executing → complete | error) driven by genuine AG-UI stream events (TOOL_CALL_CHUNK/_END/_RESULT), replacing the old "guess from whether content arrived" heuristic. Backed by an accessibleToolCallprimitive set and a staggered-reveal timeline.Artifactpanel factory/hooks/components are replaced by identically-shapedDetailedViewequivalents (config keypanel→actual). Artifact registration (the browser rail) is now a separate concern viadefineArtifactRenderer+defineArtifactCategories.ParsedArtifact.metagains an optionaltype; the renderer registers each artifact under its real kind when the parser sets it, so a renderer that serves multiple kinds labels each correctly in the browser.AgentInterface.agent/*section (18 pages) + navbar tab; the oldchat/*section is removed.1 · Headless core — adapter-based
ChatProvider(@openuidev/react-headless)The monolithic
ChatProviderPropsis replaced by a two-adapter API.ChatProviderPropsis now{ llm: ChatLLM; storage?: ChatStorage; artifactRenderers?; artifactCategories?; children }. Removed:apiUrl,threadApiUrl,streamProtocol,messageFormat,fetchThreadList,createThread,deleteThread,updateThread,loadThread,processMessage.createChatStorenow takes{ storage, llm }. All thread CRUD delegates tostorage.thread.*; sending delegates tollm.send({ threadId, messages, signal }); the stream protocol is read fromllm.streamProtocol. The old ephemeral-thread fallback is gone — acreateThreadalways happens on first send (no-storage = in-memory threads with real UUIDs).adapters/layer:fetchLLM({ url, streamAdapter, messageFormat?, headers?, fetch? }): ChatLLM— POSTs an AG-UIRunAgentInput-shaped body and streams the response through the chosen adapter.restStorage({ baseUrl, messageFormat?, headers?, fetch? }): ChatStorage— implements the thread channel against the same REST conventions the oldthreadApiUrlused (/get,/create,/get/:id,/update/:id,/delete/:id). Artifact storage is supplied separately asstorage.artifact.createDefaultInMemoryStorage()— internal default (not exported).ChatLLM,ChatStorage,ThreadStorage,ArtifactStorage,Artifact,ArtifactSummary,ArtifactCategory,ArtifactListParams,FetchLLMOptions,RestStorageOptions.@ag-ui/corebumped^0.0.45 → ^0.0.53.2 · State model — DetailedView, ThreadContext & renderer registries
The old single
ArtifactContext/createArtifactStore(tracked onlyactiveArtifactId) is deleted and split into focused stores + stateless registries, wired as nested contexts inChatProvider(Chat → DetailedView → ThreadContext → ArtifactRenderers → ArtifactStorage → ArtifactCategories):useDetailedView(viewId),useActiveDetailedView(),useDetailedViewPortalTarget()).(id, version);ArtifactEntry { id, version, heading, type, updatedAt? }, versions kept sorted ascending. Read viauseArtifactList(filter?).{ byToolName, byType }Map registry with first-wins dedup on duplicatetoolName/type(dev-mode warning).lookupArtifactRenderer/lookupArtifactRendererByType. Captured once at mount — changing theartifactRenderersprop after mount is ignored with a dev warning, so consumers must memoize or use a module constant.defineArtifactRenderer<Props>(config)—{ type, toolName: string | string[], parser({ args, response }, { isStreaming }) → ParsedArtifact<Props> | null, preview, actual, icon?, label? }.ParsedArtifact.metais{ id, version, heading, type? } | null—meta: nullrenders but skips registration (the common streaming case), and the optionaltypelets a parser stamp the artifact's real kind.defineArtifactCategories(groups)— accepts[{ name, renderers, icon? }]and returns{ artifactRenderers, artifactCategories }ready to spread onto<AgentInterface>; each category'sfilter.typeis derived from its renderers.Removed (breaking):
ArtifactContext,useArtifactStore,ArtifactState/Actions/Store,useActiveArtifact,useArtifact,useArtifactPortalTarget,openArtifact/closeArtifact/resetArtifacts.3 · Streaming & message formats
sseLineIterator(stream/adapters/_shared/sseLines.ts) buffers the partial line straddling a network-chunk boundary and flushes the tail at end-of-stream — fixing a bug that silently dropped large SSE payloads.ag-ui,openai-completions, andopenai-readable-streamadopt it;openai-responsesandlanggraphget the equivalent buffer/flush inline (and moveif (done) breakafter block processing so the final block isn't dropped).processStreamedMessagegains first-class tool-call handling:TOOL_CALL_CHUNK(AG-UI combined form — lazily starts a call, fills a late-arriving name, accumulates args; previously dropped),TOOL_CALL_END(marks the call executing),TOOL_CALL_RESULT(creates/upserts a realToolMessage, mappingisError/error).RUN_ERRORclears in-flight executing flags without synthesizing empty tool messages (no blank renderer flash).TEXT_MESSAGE_STARTno longer deletes+recreates the assistant message — avoiding ordering corruption when tool messages already landed.deleteMessageis removed;markToolExecuting/clearToolExecutingare added (optional).ThreadState.executingToolCallIds: Set<string>drives the "executing" status; cleared on run start, thread switch, cancel, and the run'sfinally(so adapters that emitTOOL_CALL_ENDwith noTOOL_CALL_RESULTdon't stick).fromItemsnow buffers all tool messages of a multi-call assistant turn and flushes them together — eliminating duplicate "Behind the scenes" groups on thread reload.openai-responseshandlesfunction_call_outputitems (response.output_item.added), emittingTOOL_CALL_RESULTwithcall_id/output.4 · Tool-call activity & rendering (
@openuidev/react-ui)ToolActivity<T>+pairToolActivity(react-headless/store/toolActivity.ts): a 4-state union pairing calls↔results bytoolCallId(not positional), readingexecutingToolCallIds, attributing orphan tool messages, withpartialJSONParsefor tolerant mid-stream args. Exposed viauseToolActivities(message, allMessages).ToolCallcompound primitives (Root/Trigger/Content/StatusIcon/StatusText/ToolName/Parameters/Result): every part takes an optionalrenderescape hatch.StatusTextemitsrole="status" aria-live="polite";Triggeremitsaria-expanded/aria-controls;Contentemitsrole="region". PlusDefaultToolCard(chevron collapsible, opens on error) andTimelineToolCard(dot+connector row).ToolCallTimelinereplaces the legacyBehindTheScenes + ToolCallComponent + ToolResultcomposition: staggered reveal for live messages (historical start fully revealed), a settled toggle label showing failure count (Behind the scenes · 2 failed), and a visually-hidden live region. "Thinking" derives fromuseThread(s => s.isRunning)+ last status — no magic_request/_responsekeys._shared/tool-renderer/):ToolCallEntry/TimelineEntrylook upuseArtifactRenderer(toolName)and render the matched renderer xor the default card — never both.ToolActivityRendererrunsrenderer.parserand:meta.type ?? renderer.typeso a shared renderer classifies each kind correctly in the browser rail;ToolCallErrorFallbackinline if the parser throws (one bad renderer can't blank the thread);ToolMessageRendereris kept as a@deprecatedbackward-compatible wrapper.webSearchRenderer(renderers/webSearchRenderer.tsx) forweb_search/webSearch— reads{ query, reasoning, sources }viapartialJSONParse(meta: null, inline-only), withSourceIconfavicon + 16×16 fallback. Not auto-registered; consumers pass it inartifactRenderers.streaming|executing && isRunning && isLast), a comprehensiveprefers-reduced-motionblock,aria-expanded/aria-controlson toggles,role="region"panels, and focus-visible rings.5 ·
<AgentInterface>component suite (@openuidev/react-ui)A complete, opinionated layout wrapping
ChatProvider+ThemeProvider+ Nav/Starters/Labels/Store providers.ChatProviderProps):componentLibrary,components ({ AssistantMessage?, UserMessage? }),theme,disableThemeProvider,logoUrl,agentName,labels,starters,starterVariant,path/defaultPath/onNavigate(controlled/uncontrolled nav),scrollVariant('always' | 'user-message-anchor'),scrollOnLoad(defaulttrue). Children are slot-extracted; duplicate slots warn and use the first.Sidebar,SidebarHeader/Content/Separator/Item,ArtifactNav,Workspace,Route,MobileHeader,ThreadHeader,Welcome,Composer,NewChatButton,ThreadList,Messages,MessageLoading,ScrollArea.useNav()controlled/uncontrolled split (onNavigatepresent → controlled).AgentInterface.Routedeclares exact-path custom pages that replace the thread region. Theartifacts/path prefix is reserved and matched before user Routes.ThreadListgroups by Today/Yesterday/7d/30d/Year/Older with a per-thread Delete action.artifactCategoriesentry; tabs/filtering were dropped — all sections show, and artifacts matching no category land in a trailing fallback section (never dropped). Hidden when empty, when a DetailedView is active, or off the thread view.ArtifactBrowserPage(debounced search + cursor pagination viaArtifactStorage.list, with an explicit not-found state for unknown categories) andArtifactViewPage(rendersrenderer.actualfrom stored content withargs: undefined, isStreaming: false).ArtifactNavrenders one item per category (or a single "Artifacts" item), nothing when artifact storage is absent.src/detailed-view/top-level export):DetailedView<P>(config)factory (title,preview,actual,panelProps),DetailedViewPanel(portals into the nearest target, error-boundary wrapped, configurable header),DetailedViewOverlay(mobile slide-in/out),DetailedViewPortalTarget.ResizableSeparatorimplements the WAI-ARIA window-splitter pattern (drag + ArrowKeys/Shift/Home/End);useDetailedViewResizeclamps the chat panel to[420px, 80%]and auto-collapses the sidebar when a detailed view opens.6 · Legacy shell retirement (BREAKING)
packages/react-ui/src/components/Shell/,CopilotShell/,BottomTray/, and theOpenUIChatstories — including their.scss, stories, and orphaned assets (the bulk of the −13,719 lines). TheShell/CopilotShellnamespace star-exports are removed fromindex.ts.Artifact*→DetailedView*rename in the public surface:Artifactfactory →DetailedView(config keypanel→actual);ArtifactConfig/Controls→DetailedViewConfig/Controls.ArtifactPanel/Overlay/PortalTarget(+ prop types) →DetailedView*equivalents.useActiveArtifact/useArtifact→useActiveDetailedView/useDetailedView.index.tsnow doesexport * from '@openuidev/react-headless'andexport * from './components/AgentInterface'.Artifactdomain-model type now shadows the oldArtifactfactory name — code doingArtifact({...})gets a type error (caught at build), not a silent runtime bug.utils/contentParser.ts(XML<content>/<context>envelope) replaced byutils/sentinelParser.ts(the]]>openui:inline-sentinel format) with an internalparseLegacyXmlfallback so previously persisted messages still render.useScrollToBottomgainsscrollOnLoad(defaulttrue, surfaced onAgentInterfaceProps) to suppress initial-load auto-scroll without affecting mid-stream following.restore user-message stylingfix included.7 · Example apps & build config
examples/openui-cloud(new) — OpenUI Cloud two-plane integration: generation plane (/api/chatllm proxy,THESYS_*key) + read/edit plane (useOpenuiCloudStorageover/v1withfct_frontend-token mint/refresh). Consumes the vendored@openuidev/thesys, wiresdefineArtifactCategoriesfor slides/report. Excluded from the pnpm workspace (!examples/openui-cloudinpnpm-workspace.yaml) because it depends on a gitignoredfile:tarball and a cross-repolink:that don't exist in CI — keeping monorepopnpm install --frozen-lockfilegreen.examples/openui-responses-chat(new) — OpenAI Conversations+Responses reference:restStorage+openAIResponsesAdapter, aMAX_TOOL_TURNSagentic loop with file-based thread persistence, and acreate_code_artifactrenderer.examples/openui-artifact-demomigrated fromprocessMessage/openAIAdaptertofetchLLM; theArtifactCodeBlockopenui-lang component is dropped in favor of acreate_code_blocktool +codeBlockRenderer.hands-on-table-chat,shadcn-chat,material-ui-chat,langgraph-chat,mastra-chat,openui-chat, harnesses,react-email) and thefastapi-backendfrontend migrated off the legacy shells toAgentInterface. Theopenui-cliscaffold template is intentionally kept onFullScreenfor now (reverted).lang-corestream parser fix:createStreamParserscans completed statements over the preprocessed (cleaned) buffer and recovers when a code fence/prose prefix shifts offsets — fixing statement-boundary corruption when markdown precedes the openui-lang program.8 · Docs
docs/content/docs/agent/section (18 MDX pages: Getting Started / Core Concepts / Customize / Reference / Guides) + a new "Agent Interface" navbar tab. Reference pages cover allAgentInterfaceprops, adapters/formats,defineArtifactRenderer, the hook set, components/slots, and self-hosting;guides/migratingdocuments the full legacy→AgentInterfacemapping.docs/content/docs/chat/*section and retargeted READMEs/skill off the deleted shells.Breaking changes & migration
ChatProvider/AgentInterfaceapiUrl,threadApiUrl,streamProtocol,messageFormatllm={fetchLLM({ url, streamAdapter })},storage={restStorage({ baseUrl })}fetchThreadList/createThread/deleteThread/updateThread/loadThread/processMessagepropsstorage.thread.*(e.g.restStorage) +llm.sendArtifact({...})factory,ArtifactConfig/ControlsDetailedView({...})(configpanel→actual),DetailedViewConfig/ControlsArtifactPanel/ArtifactOverlay/ArtifactPortalTarget(+ props)DetailedViewPanel/Overlay/PortalTarget(+ props)useArtifact,useActiveArtifact,useArtifactPortalTargetuseDetailedView,useActiveDetailedView,useDetailedViewPortalTargetArtifactContext,useArtifactStore,openArtifact/closeArtifactuseDetailedViewStore,setActiveDetailedView/resetimport * as Shell/import * as CopilotShell<AgentInterface>slotsprocessStreamedMessage(..., deleteMessage)markToolExecuting/clearToolExecuting(optional)Notes consumers should know:
artifactRenderersmust be memoized/module-constant (registry frozen at mount);restStoragecovers only the thread channel (providestorage.artifactseparately); toolName matching is exact-match only in this PR (no RegExp/*dispatch yet); the top-levelArtifactname now resolves to the headless domain type, not the old factory.Test plan
restStorage,processStreamedMessage(+ tool-status),toolActivity,sseLines,artifactRendererRegistry(first-wins + dev warns),createThreadContextStore,createDetailedViewStore, thread-switch resets.apiUrl/threadApiUrlprops are TS errors;fetchLLM+restStoragedrive a thread end-to-end.TOOL_CALL_CHUNK-only stream renders;TOOL_CALL_END→executing; run ends without result → no perpetual shimmer; errored call suppresses the preview;Behind the scenes · N failedcount;web_searchrenders preview + side panel.ArtifactNav/Workspace/browser render nothing; unknown category → not-found + "View all artifacts"; uncategorized artifacts appear in the fallback section; DetailedView opens → sidebar collapses, splitter clamps[420px, 80%], keyboard resize works;<768px→ slide-over sidebar.import * as Shell/import { Artifact }(as factory) fail to compile; legacy XML-envelope messages still render viaparseLegacyXml; new]]>openui:sentinel parses.prefers-reduced-motionstops all reveal/shimmer/spin animations; status changes announce via the live region.openui-cloud(with vendored tarball + keys) loads threads and opens slides/report panels;openui-responses-chatmulti-turn tools + code artifact; migrated examples build and run; monorepopnpm install --frozen-lockfilestays green withopenui-cloudexcluded./docs/agent/*renders with the new navbar tab; old/docs/chat/*is gone; migration guide links resolve.Known follow-ups / caveats
docs/agent/getting-started/openui-cloud.mdxis a "Coming soon" stub; several concept/customize pages carry inlineTODO/ASSUMEDand "add video/image" placeholders (notablytools.mdx).allowBuildsstanza added topnpm-workspace.yamlhas placeholder string values and is inert until populated with real booleans.openui-cloudis not installed by a rootpnpm install(workspace-excluded); itsvendor/tarball must be produced fromgenui-sdkfirst.Shell/CopilotShell/BottomTraydirectories are fully deleted (no dead code left on disk).