UI Surface Model¶
The frontend runtime is organized around one persistent session layer and a small state machine that decides which surface is visible.
The canonical owner is chat-ui/src/state/uiSurfaceReducer.js. ChatUIContext stores the reducer state plus the session caches that survive navigation:
activeChatIdactiveWorkflowNameaskMessagesworkflowMessagesworkflowStatus
Core rule¶
There is one session, not separate chat products.
The user is either:
- on the full chat surface
- on another route with the same session available through the widget
- in workflow
viewlayout, where the artifact takes over the screen and the widget becomes the re-entry point back into chat
Visible surfaces¶
| Surface | When visible | Owner |
|---|---|---|
| Full chat surface | /chat and workflow chat routes | chat-ui/src/pages/ChatPage.js |
| Floating widget | non-chat routes | chat-ui/src/widget/GlobalChatWidgetWrapper.jsx + chat-ui/src/components/chat/PersistentChatWidget.jsx |
Floating widget in view layout | fullscreen artifact inside ChatPage | ArtifactPanel via floatingWidget |
view is not a third conversation product. It is a workflow layout state.
Surface state machine¶
The reducer tracks three related concepts:
Conversation mode¶
askmeans general chatworkflowmeans an active workflow run
ask always collapses to full layout because it does not own an artifact panel.
Layout mode¶
fullmeans chat onlysplitmeans chat plus artifactminimizedmeans chat compressed while artifact remains visibleviewmeans fullscreen artifact with widget re-entry
Widget state¶
The reducer also tracks widget visibility separately from route ownership:
isInWidgetModeisWidgetVisibleisChatOverlayOpenwidgetOverlayOpen
GlobalChatWidgetWrapper suppresses widget rendering on primary chat routes and enables it on other routes.
Event-driven surface changes¶
Frontend surfaces are not changed by ad hoc component logic alone. The reducer reacts to event classes:
| Event family | Effect on surface |
|---|---|
chat.tool_call with display=artifact|view|fullscreen | opens artifact surface |
This mapping lives in mapSurfaceEventToAction(...).
Session continuity¶
Navigation must not discard the active session.
Current implementation:
ChatUIContextsurvives route changesChatPagerestores cachedaskMessagesandworkflowMessages- artifact state is cached for restoration
- the widget reads the same shared caches instead of opening a second session
That is why a user can leave /chat, browse elsewhere, and still reopen the same run from the widget.
Widget contract¶
The widget is the session entry point outside the full chat surface.
Where it renders¶
| Context | Mounted by |
|---|---|
| non-chat routes | GlobalChatWidgetWrapper |
ChatPage view layout | ArtifactPanel |
What it shows¶
The widget follows the active context:
- if a workflow is active, it defaults to
workflowMessages - otherwise it shows
askMessages
The user can switch to ask context inline without navigating.
Header contract¶
The expanded widget keeps a two-button header:
- left button: switch to ask context or open the full ask chat
- right button: return to the active workflow chat when one exists
The compose affordance for ask mode lives in the sub-header as + New conversation.
Route suppression¶
GlobalChatWidgetWrapper returns null on:
/chat/chat/*/app/:id/:workflow
That keeps the widget from competing with the full chat surface.
Canonical implementation files¶
chat-ui/src/state/uiSurfaceReducer.jschat-ui/src/context/ChatUIContext.jsxchat-ui/src/pages/ChatPage.jschat-ui/src/widget/GlobalChatWidgetWrapper.jsxchat-ui/src/components/chat/PersistentChatWidget.jsx