Chat
ChatWidget renders streaming chat, tool calls, approvals, questions, and session history. AmodalChat wraps it with position="inline" and a runtime URL from the nearest AmodalProvider.
Import ChatWidget from @amodalai/react/widget for a chat-only integration, or from the package root when using provider hooks. The widget subpath also exports components and hooks for a custom chat interface.
Installation
npm install @amodalai/reactQuick start
Inside a same-origin AmodalProvider using the default hosted team login:
import { AmodalChat } from '@amodalai/react';
import '@amodalai/react/style.css';
<AmodalChat />;For a multi-tenant app, use an application proxy that authenticates each request and authorizes scope and session access. The callback supplies your application token; the proxy retains runtime credentials:
import { ChatWidget } from '@amodalai/react/widget';
import '@amodalai/react/widget/style.css';
<ChatWidget serverUrl="https://app.example.com/api/agent" getToken={getAppToken} position="floating" historyEnabled />;Keep runtime tokens and persistent API keys on the backend for multi-tenant use. Direct runtime access is suitable only for callers trusted to access the agent's sessions across scopes. AmodalChat inherits the provider's URL only; pass getToken to it separately for bearer authentication. See Embedding for proxy requirements.
Props
AmodalChat accepts every ChatWidget prop except position (pinned to inline); its serverUrl is optional and defaults to the provider's runtimeUrl.
Connection and identity
| Prop | Default | Meaning |
|---|---|---|
serverUrl | required | Base URL of your application proxy, or the runtime for trusted direct access. |
user | none | { id: string }; accepted but neither sent to the runtime nor displayed. It does not authenticate a user. |
getToken | none | Returns a bearer token for serverUrl (sync or async): an application token for your proxy, or a runtime token for trusted direct access. |
scopeId | none | Scope requested for chat sessions, memory, and agent store tools. A verified token scope takes precedence. |
scopeContext | none | Key-value pairs injected into connection API calls via contextInjection. |
metadata | none | Observability-only values such as userId and screen; a map, or a function read on each send. See Request metadata. |
scopeId identifies the tenant, workspace, or record; sessionId identifies one conversation. Reusing a scope does not resume a chat. Use resumeSessionId to continue a saved conversation. Your backend must authorize the scope and session on every request; see Embedding.
Behavior
| Prop | Default | Meaning |
|---|---|---|
position | floating | See Positions. |
defaultOpen | false | Start open (togglable positions only). |
historyEnabled | false | Session history drawer. |
showHeader / showInput / showFeedback | true / true / false | Header bar; input bar; thumbs up/down on assistant messages. |
agent | none | Root-agent surface name from the bundle's agents; that agent's declared resources scope what the session loads. |
deployId | active deploy | Pin a specific deployment. |
initialMessage | none | Auto-sent once on mount. |
resumeSessionId | none | Load an existing session on mount and continue sending on that session; takes precedence over initialMessage. |
Callbacks
| Prop | Meaning |
|---|---|
onToolCall(call) | A tool call completed; receives the full ToolCallInfo. |
onKBProposal(proposal) | The agent proposed a knowledge-base update. |
onEvent(event) | Every widget event (agent-driven and interaction). |
onSessionCreated(sessionId) | First stream init returned a session id. |
onStreamEnd() | The SSE stream ended. |
onStateChange({ sessionId, messages }) | Session state changed (for external persistence). |
Extension points
| Prop | Meaning |
|---|---|
theme | See Theming. |
widgets | WidgetRegistry of custom renderers for rich inline widgets. |
inlineBlockRenderers | Renderers for block types the widget doesn't render natively; native types (text, widget, reasoning, tool_calls, confirmation, ask_user, ask_choice, durable_approval, show_preview) cannot be overridden. |
streamFn | Custom transport ((text, signal, images?, attachments?) => AsyncIterable<SSEEvent>). Owns request construction, authentication, and session state. |
onDurableApproval(sessionId, value, followUpMessage?) | Required with a custom streamFn when handling durable approval cards. Submit the answer to the runtime and resume or follow the run. The standard transport does this itself. |
askDisplay | inline by default. Use docked when the host displays questions through AskChoiceBanner from @amodalai/react. |
entityExtractors | Replaces the default entity extractor for entity events. |
Imperative handle
The ref exposes ChatWidgetHandle:
const chat = useRef<ChatWidgetHandle>(null);
chat.current?.sendMessage("Summarize today's alerts");
chat.current?.getSessionId();Other handle methods: stop(), isStreaming(), toggleHistory(), submitAskChoice(askId, values, message), and respondDurableApproval(sessionId, value, followUpMessage?).
Keeping A Scoped Chat Alive
To retain one conversation per user and scope, store the session ID and restore it when the panel remounts. Use your authorized proxy URL as serverUrl:
const storageKey = `amodal-session:${userId}:${scopeId}`;
const [resumeSessionId] = useState(() => localStorage.getItem(storageKey));
<ChatWidget
serverUrl={serverUrl}
getToken={getAppToken}
scopeId={scopeId}
scopeContext={{ tenant_id: tenantId, page: 'case-review' }}
resumeSessionId={resumeSessionId ?? undefined}
onStateChange={({ sessionId }) => {
if (sessionId) localStorage.setItem(storageKey, sessionId);
}}
/>;The containing component must remount when the user or scope changes because useState reads storage only on mount. Give it a React key containing both values. Clear saved IDs on logout when the browser is shared. The backend must check access to the saved session; a storage key does not enforce it. See the complete embedding example.
Positions
| Position | Behavior |
|---|---|
inline | Renders in-place within your layout |
floating | Floating button that expands into a chat panel |
right | Fixed panel on the right side |
bottom | Fixed panel at the bottom |
Theming
The theme prop covers the common cases:
<ChatWidget
serverUrl={serverUrl}
getToken={getAppToken}
theme={{
mode: 'auto', // 'light' | 'dark' | 'auto' (follows prefers-color-scheme)
primaryColor: '#6e56cf',
borderRadius: '12px',
headerText: 'Ask the agent',
placeholder: 'Type a message…',
verboseTools: true, // full tool-call params, results, timing
}}
/>Other ChatTheme fields: backgroundColor, fontFamily, fontSize, userBubbleColor, agentBubbleColor, toolCallColor, emptyStateText.
Which key drives which surface
Each theme key sets one CSS custom property that drives specific surfaces. primaryColor is shared: the floating launcher, the send button, focus rings, and accent text all read --pcw-primary, so you cannot recolor the launcher independently through the prop (override the CSS variable on a narrower selector for that).
theme key | CSS variable | Surfaces it controls |
|---|---|---|
primaryColor | --pcw-primary | Floating launcher, send button, focus rings, accent text/links |
backgroundColor | --pcw-bg | Panel and header background |
userBubbleColor | --pcw-user-bubble | User message bubbles |
agentBubbleColor | --pcw-agent-bubble | Agent bubbles and inline cards |
toolCallColor | --pcw-tool-call-bg | Tool-call card background |
borderRadius | --pcw-radius | Panel, bubbles, buttons |
fontFamily / fontSize | --pcw-font / --pcw-font-size | All widget text |
headerText | --pcw-header-text † | Header title label |
placeholder | --pcw-placeholder † | Input placeholder |
emptyStateText | --pcw-empty-state-text † | Empty-conversation message |
mode | data-theme / prefers-color-scheme | Light vs. dark palette (see below) |
† These variables carry a text string, not a style value, and the text is rendered by the component rather than read back from the variable. Set the prop to change the copy; overriding the CSS variable alone has no effect.
Surfaces with no matching theme key (header/body text, muted text, borders, panel tint, shadow) are CSS-only: --pcw-text, --pcw-text-muted, --pcw-border, --pcw-panel-tint, --pcw-user-text, --pcw-shadow. Override those directly.
For anything the prop doesn't cover, override the CSS custom properties (no Tailwind dependency):
.pcw-widget {
--pcw-primary: #6e56cf;
--pcw-bg: #ffffff;
--pcw-text: #1a1a1a;
--pcw-border: #e5e5e5;
--pcw-radius: 12px;
}Saved Embed settings
The agent's Embed page saves configuration for its preview and generates a React snippet. A mounted ChatWidget reads its props; it does not fetch that saved configuration.
After changing saved settings, copy the generated values into your application and release that change. Editing props in your application directly also works.
SSE events
The widget handles these event types from the runtime:
| Event | Description |
|---|---|
text_delta | Streaming text output |
tool_call_start | Tool execution beginning |
tool_call_result | Tool execution complete |
skill_activated | Skill activation |
widget | Widget rendered inline |
confirmation_required | Write operation needs approval |
done | Response complete |
Building blocks
For a custom chat surface, the /widget subpath exports the pieces ChatWidget is made of:
- Components:
MessageList,InputBar,SessionHistory,StreamingIndicator,ToolCallCard,AskUserCard,KBProposalCard,SkillPill,TagEditor,FormattedText, andWidgetRenderer(with itsWidgetRegistrytype). - Hooks:
useChat(the widget's chat state machine),useWidgetEvents,useSessionHistory. - Chat API:
listSessions,getSessionHistory,createSession. - Theme utilities:
defaultTheme,applyTheme,mergeTheme.
The provider-based hooks and headless clients live in the package root; see @amodalai/react.