React SDK Reference
The React SDK provides the rendering layer for Freesail. It connects to the gateway via SSE, renders A2UI surfaces as React component trees, and handles user actions and two-way data binding.
Installation
Section titled “Installation”npm install freesail @freesail/standard-catalog @freesail/chat-catalogFreesailProvider
Section titled “FreesailProvider”FreesailProvider manages the connection to the gateway, registers catalogs, applies the active theme, and provides context to all child components. There is no separate theme provider — theme is a prop directly on FreesailProvider.
import { ReactUI } from 'freesail';import { StandardCatalog } from '@freesail/standard-catalog';import { ChatCatalog } from '@freesail/chat-catalog';
<ReactUI.FreesailProvider theme={ReactUI.defaultLightTokens} catalogs={[StandardCatalog, ChatCatalog]} onConnectionChange={(connected) => console.log('Connected:', connected)} onError={(error) => console.error('Error:', error)}> <App /></ReactUI.FreesailProvider>| Prop | Type | Default | Description |
|---|---|---|---|
theme | FreesailThemeTokens | Partial<FreesailThemeTokens> | defaultLightTokens | Full or partial token object. Use ReactUI.defaultLightTokens or ReactUI.defaultDarkTokens as a base, spread with overrides for customisation. |
catalogs | CatalogDefinition[] | [] | Catalogs to register. Components are auto-registered on mount. |
name | string | — | Optional scope name. Only needed when two providers on the same page connect to the same gateway — scopes the sessionStorage key to prevent session collisions. |
transportOptions | object | — | Additional transport configuration (reconnect delays, queue size, etc.) |
additionalCapabilities | Record<string, unknown> | — | Extra capability key/values merged into the standard catalog list advertised to the agent |
onConnectionChange | (connected: boolean) => void | — | Called when the connection state changes |
onError | (error: Error) => void | — | Called when a transport or parse error occurs |
onBeforeCreateSurface | (surfaceId, catalogId, sendDataModel, surfaceManager) => InterceptorResult | — | Called before honouring an agent createSurface. Return { allowed: false, message } to block. |
onBeforeUpdateComponents | (surfaceId, components) => InterceptorResult | — | Called before honouring an agent updateComponents. Return { allowed: false, message } to block. |
onBeforeUpdateDataModel | (surfaceId, path) => InterceptorResult | — | Called before honouring an agent updateDataModel. Return { allowed: false, message } to block. |
onBeforeDeleteSurface | (surfaceId) => InterceptorResult | — | Called before honouring an agent deleteSurface. Return { allowed: false, message } to block. |
Gateway URL
Section titled “Gateway URL”In the free version, the gateway and web app must share the same origin. Omit the gateway prop entirely — it defaults to '', using relative paths with no CORS required. This is the standard setup when the gateway is reverse-proxied onto the same domain as the UI (nginx in production, Vite proxy in development).
Cross-origin deployments — setting gateway to an explicit URL on a different domain — require Freesail Enterprise.
Interceptors
Section titled “Interceptors”Each onBefore* prop allows you to validate or block agent operations before they are applied to the surface manager. Return { allowed: false, message: '...' } to block — the message is sent back to the agent as an error payload over MCP. Return { allowed: true, message: '...' } to allow and optionally notify the agent with a validation message.
The onBeforeCreateSurface interceptor receives the surfaceManager as its fourth argument, which can be used to inspect existing surfaces:
<ReactUI.FreesailProvider theme={ReactUI.defaultLightTokens} catalogs={[StandardCatalog]} onBeforeCreateSurface={(surfaceId, catalogId, sendDataModel, surfaceManager) => { const surfaces = surfaceManager.getAllSurfaces().filter(s => s.id !== '__chat'); if (surfaces.length >= 3) { return { allowed: false, message: 'Surface limit reached.' }; } return { allowed: true, message: '' }; }}> ...</ReactUI.FreesailProvider>Theming
Section titled “Theming”Theme prop
Section titled “Theme prop”The theme prop on FreesailProvider accepts a full FreesailThemeTokens object. Two complete baseline sets are exported:
// Light theme<ReactUI.FreesailProvider theme={ReactUI.defaultLightTokens} catalogs={CATALOGS}>
// Dark theme<ReactUI.FreesailProvider theme={ReactUI.defaultDarkTokens} catalogs={CATALOGS}>
// Custom overridesconst myTheme = { ...ReactUI.defaultLightTokens, primary: '#e11d48', primaryHover: '#be123c', bgRaised: '#fff1f2',};<ReactUI.FreesailProvider theme={myTheme} catalogs={CATALOGS}>Runtime theme switching
Section titled “Runtime theme switching”Pass a state variable to the theme prop:
const [mode, setMode] = useState<'light' | 'dark'>('light');const activeTheme = mode === 'dark' ? ReactUI.defaultDarkTokens : ReactUI.defaultLightTokens;
<ReactUI.FreesailProvider theme={activeTheme} catalogs={CATALOGS}> <button onClick={() => setMode(m => m === 'light' ? 'dark' : 'light')}>Toggle</button> ...</ReactUI.FreesailProvider>FreesailThemeTokens
Section titled “FreesailThemeTokens”The full token interface. CSS custom properties (--freesail-*) are injected into the document.
Colour tokens:
| Token | CSS variable | Description |
|---|---|---|
bg | --freesail-bg | Page/surface root background |
bgRaised | --freesail-bg-raised | Card and panel backgrounds |
bgMuted | --freesail-bg-muted | Subtle background fills |
bgOverlay | --freesail-bg-overlay | Modal/dialog overlay backdrop |
textForeground | --freesail-text-foreground | Primary text |
textSecondary | --freesail-text-secondary | Secondary text |
primary | --freesail-primary | Brand accent colour |
primaryHover | --freesail-primary-hover | Hover state for primary |
primaryForeground | --freesail-primary-foreground | Text on primary background |
error | --freesail-error | Error states |
success | --freesail-success | Success states |
warning | --freesail-warning | Warning states |
info | --freesail-info | Informational states |
border | --freesail-border | Borders and dividers |
Structure tokens:
| Token | CSS variable | Description |
|---|---|---|
radiusSm | --freesail-radius-sm | Small border radius |
radiusMd | --freesail-radius-md | Medium border radius |
radiusLg | --freesail-radius-lg | Large border radius |
shadowSm | --freesail-shadow-sm | Small box shadow |
shadowMd | --freesail-shadow-md | Medium box shadow |
Fluid spacing tokens (container-relative via cqi):
| Token | CSS variable | Description |
|---|---|---|
spaceXs | --freesail-space-xs | Extra-small spacing |
spaceSm | --freesail-space-sm | Small spacing |
spaceMd | --freesail-space-md | Medium spacing |
spaceLg | --freesail-space-lg | Large spacing |
spaceXl | --freesail-space-xl | Extra-large spacing |
Fluid typography tokens (container-relative via cqi):
| Token | CSS variable | Description |
|---|---|---|
typeCaption | --freesail-type-caption | Caption font size |
typeLabel | --freesail-type-label | Label font size |
typeBody | --freesail-type-body | Body font size |
typeH5 | --freesail-type-h5 | Heading 5 font size |
typeH4 | --freesail-type-h4 | Heading 4 font size |
typeH3 | --freesail-type-h3 | Heading 3 font size |
typeH2 | --freesail-type-h2 | Heading 2 font size |
typeH1 | --freesail-type-h1 | Heading 1 font size |
Fluid icon tokens (container-relative via cqi):
| Token | CSS variable | Description |
|---|---|---|
iconSm | --freesail-icon-sm | Small icon size |
iconMd | --freesail-icon-md | Medium icon size |
iconLg | --freesail-icon-lg | Large icon size |
iconXl | --freesail-icon-xl | Extra-large icon size |
icon2xl | --freesail-icon-2xl | 2XL icon size |
icon3xl | --freesail-icon-3xl | 3XL icon size |
icon4xl | --freesail-icon-4xl | 4XL icon size |
All spacing, typography, and icon tokens use fluid clamp() values with cqi units so they scale proportionally with the container width.
FreesailSurfaceTheme
Section titled “FreesailSurfaceTheme”The subset of colour tokens an agent can set on container components (Row, Column, Card, Modal, TabularGrid) via the theme prop in the component definition. Spacing, typography, and radii are restricted to the host app.
| Token | CSS variable |
|---|---|
primary | --freesail-primary |
primaryHover | --freesail-primary-hover |
primaryForeground | --freesail-primary-foreground |
bg | --freesail-bg |
bgRaised | --freesail-bg-raised |
bgMuted | --freesail-bg-muted |
textForeground | --freesail-text-foreground |
textSecondary | --freesail-text-secondary |
border | --freesail-border |
FreesailSurface
Section titled “FreesailSurface”Renders a single A2UI surface. The surface must have been created by the gateway (agent-managed) or bootstrapped by the React app (client-managed).
<ReactUI.FreesailSurface surfaceId="workspace" />| Prop | Type | Default | Description |
|---|---|---|---|
surfaceId | string | Required | The surface to render |
theme | FreesailThemeTokens | Partial<FreesailThemeTokens> | — | Per-surface token overrides. Useful for scoping a different visual treatment to one surface (e.g. a light chat panel in a dark app). |
className | string | — | CSS class for the wrapper element |
loading | ReactNode | Freesail spinner | Shown while the surface is being initialised |
error | ReactNode | Error message | Shown if the catalog is not registered |
empty | ReactNode | Freesail spinner | Shown while waiting for components |
Per-surface theming
Section titled “Per-surface theming”{/* Chat panel always uses light tokens regardless of global theme */}<ReactUI.FreesailSurface surfaceId="__chat" theme={{ ...ReactUI.defaultLightTokens }}/>
{/* Main surfaces inherit the global theme */}<ReactUI.FreesailSurface surfaceId="workspace" />Bootstrapping Client-Managed Surfaces
Section titled “Bootstrapping Client-Managed Surfaces”Client-managed surfaces (prefixed with __) are created and wired up in React before the agent connects. The __chat surface is the primary example — the app creates the surface, defines its component tree, and sets the initial data model. The agent only writes to the data model.
Use useFreesailContext() to access the surfaceManager directly:
import { ReactUI } from 'freesail';import { ChatCatalog } from '@freesail/chat-catalog';
const CHAT_CATALOG_ID = ChatCatalog.namespace;
function ChatBootstrapper() { const { surfaceManager } = ReactUI.useFreesailContext();
useEffect(() => { // 1. Create the surface surfaceManager.createSurface({ surfaceId: '__chat', catalogId: CHAT_CATALOG_ID, sendDataModel: false, });
// 2. Define the component tree surfaceManager.updateComponents('__chat', [ { id: 'root', component: 'ChatContainer', title: 'Chat', height: '100%', children: ['message_list', 'agent_stream', 'typing', 'chat_input'], }, { id: 'message_list', component: 'ChatMessageList', children: { componentId: 'msg_template', path: '/messages' }, }, { id: 'msg_template', component: 'ChatMessage', role: { path: 'role' }, content: { path: 'content' }, timestamp: { path: 'timestamp' }, }, { id: 'agent_stream', component: 'AgentStream', token: { path: '/stream/token' }, active: { path: '/stream/active' }, }, { id: 'typing', component: 'ChatTypingIndicator', visible: { path: '/isTyping' }, text: 'Thinking...', }, { id: 'chat_input', component: 'ChatInput', placeholder: 'Type a message...', }, ]);
// 3. Set the initial data model surfaceManager.updateDataModel('__chat', '/', { messages: [], isTyping: false, stream: { token: '', active: false }, }); }, [surfaceManager]);
return null;}Place <ChatBootstrapper /> inside FreesailProvider before any FreesailSurface that needs __chat.
Session State Persistence
Section titled “Session State Persistence”FreesailProvider automatically persists surface state to sessionStorage on every significant change (components updated, data model updated, surface created or deleted). On reconnect within the gateway’s grace period, the provider restores this state so surfaces survive page refreshes without a round-trip to the agent. Client-managed surfaces bootstrapped by the app are overwritten with the saved state so conversation history is recovered correctly.
useFreesailContext()
Section titled “useFreesailContext()”Returns the full Freesail context. Throws if used outside a FreesailProvider.
const { surfaceManager, transport, sendAction, getSurface, isConnected } = useFreesailContext();useFreesailTheme()
Section titled “useFreesailTheme()”Returns the current theme tokens and mode.
const { tokens, mode } = ReactUI.useFreesailTheme();// tokens: FreesailThemeTokens — the resolved token values// mode: 'light' | 'dark'useSurface(surfaceId)
Section titled “useSurface(surfaceId)”Subscribes to a surface and re-renders on any change (components, data model, deletion).
const surface = useSurface('workspace');// surface.components, surface.dataModel, surface.catalogId, etc.useSurfaceData(surfaceId, path?)
Section titled “useSurfaceData(surfaceId, path?)”Subscribes to a specific path in a surface’s data model.
const price = useSurfaceData<string>('ticker', '/currentPrice');useConnectionStatus()
Section titled “useConnectionStatus()”Returns the current connection state.
const { isConnected } = useConnectionStatus();useSurfaces()
Section titled “useSurfaces()”Returns all active surfaces.
const surfaces = useSurfaces();const agentSurfaces = surfaces.filter(s => !s.id.startsWith('__'));useSessionId()
Section titled “useSessionId()”Returns the session ID assigned by the gateway, or null until connected.
const sessionId = useSessionId();useAction(surfaceId)
Section titled “useAction(surfaceId)”Returns a dispatch function for sending actions on a surface.
const dispatch = useAction('workspace');await dispatch('submit_form', 'submit_btn', { name: 'Alice' });CatalogDefinition
Section titled “CatalogDefinition”The interface for registering custom catalogs with FreesailProvider:
interface CatalogDefinition { namespace: string; // catalogId URI schema: object; // parsed catalog JSON components: Record<string, ComponentType<FreesailComponentProps>>; functions?: Record<string, FunctionImplementation>;}The namespace must exactly match the catalogId in your catalog JSON. Pass the catalog’s exported constant:
import { StandardCatalog } from '@freesail/standard-catalog';// StandardCatalog.namespace === 'https://freesail.dev/catalogs/standard-catalog.json'FreesailComponentProps
Section titled “FreesailComponentProps”All catalog components receive this props object:
| Prop | Type | Description |
|---|---|---|
component | A2UIComponent | Resolved component definition (data bindings already resolved) |
children | ReactNode | Rendered child components |
meta | ComponentMeta | Metadata object with getBinding(propName) for accessing raw data bindings |
dataModel | Record<string, unknown> | Full surface data model snapshot |
scopeData | unknown | Current item data when inside a dynamic list template |
onAction | (name, context) => void | Dispatch a named action to the agent |
onDataChange | (path, value) => void | Write a value to the local data model (two-way binding) |
onFunctionCall | (call) => void | Execute a client-side function (e.g. show, hide) |
Two-way binding
Section titled “Two-way binding”Input components use the meta object to resolve the bound data model path, then call onDataChange on user interaction:
export function MyInput({ component, meta, onDataChange }: FreesailComponentProps) { const value = (component['value'] as string) ?? ''; const boundPath = meta.getBinding('value')?.path ?? `/input/${component.id}`;
return ( <input value={value} onChange={(e) => onDataChange?.(boundPath, e.target.value)} /> );}When inside a dynamic list template, relative paths are automatically converted to absolute paths, so onDataChange always writes to the correct location in the data model.
Component visibility
Section titled “Component visibility”The visible property of components is checked before rendering. In addition to the agent-set value, the client-side show/hide functions (in the Standard Catalog) perform a visibility override. This override takes precedence over the agent’s value until the agent explicitly updates visible in the component definition, which clears the override.
Exports
Section titled “Exports”The freesail package re-exports the React SDK under the ReactUI namespace:
import { ReactUI } from 'freesail';
// Provider and surfaceReactUI.FreesailProviderReactUI.FreesailSurface
// HooksReactUI.useFreesailContext()ReactUI.useFreesailTheme()ReactUI.useConnectionStatus()ReactUI.useSurfaces()ReactUI.useSessionId()ReactUI.useAction(surfaceId)
// Theme utilitiesReactUI.defaultLightTokens // FreesailThemeTokensReactUI.defaultDarkTokens // FreesailThemeTokensReactUI.tokensToCssVars(tokens, mode)
// TypesReactUI.FreesailThemeTokensReactUI.FreesailSurfaceThemeReactUI.CatalogDefinitionReactUI.FreesailComponentPropsRegistering Custom Catalogs
Section titled “Registering Custom Catalogs”The recommended way to create catalogs is using the Freesail CLI:
npx freesail new catalogYou can also use registerCatalog to register a catalog imperatively outside of FreesailProvider:
import { registerCatalog } from '@freesail/react';
registerCatalog( 'https://mycompany.com/catalogs/my-catalog.json', { MyCard, MyChart }, { myFormat }, catalogSchema);Or use withCatalog to register a single component at import time:
import { withCatalog } from '@freesail/react';
export const MyCard = withCatalog( 'https://mycompany.com/catalogs/my-catalog.json', 'MyCard', MyCardComponent);