Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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/react

Quick 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

PropDefaultMeaning
serverUrlrequiredBase URL of your application proxy, or the runtime for trusted direct access.
usernone{ id: string }; accepted but neither sent to the runtime nor displayed. It does not authenticate a user.
getTokennoneReturns a bearer token for serverUrl (sync or async): an application token for your proxy, or a runtime token for trusted direct access.
scopeIdnoneScope requested for chat sessions, memory, and agent store tools. A verified token scope takes precedence.
scopeContextnoneKey-value pairs injected into connection API calls via contextInjection.
metadatanoneObservability-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

PropDefaultMeaning
positionfloatingSee Positions.
defaultOpenfalseStart open (togglable positions only).
historyEnabledfalseSession history drawer.
showHeader / showInput / showFeedbacktrue / true / falseHeader bar; input bar; thumbs up/down on assistant messages.
agentnoneRoot-agent surface name from the bundle's agents; that agent's declared resources scope what the session loads.
deployIdactive deployPin a specific deployment.
initialMessagenoneAuto-sent once on mount.
resumeSessionIdnoneLoad an existing session on mount and continue sending on that session; takes precedence over initialMessage.

Callbacks

PropMeaning
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

PropMeaning
themeSee Theming.
widgetsWidgetRegistry of custom renderers for rich inline widgets.
inlineBlockRenderersRenderers 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.
streamFnCustom 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.
askDisplayinline by default. Use docked when the host displays questions through AskChoiceBanner from @amodalai/react.
entityExtractorsReplaces 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

PositionBehavior
inlineRenders in-place within your layout
floatingFloating button that expands into a chat panel
rightFixed panel on the right side
bottomFixed 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 keyCSS variableSurfaces it controls
primaryColor--pcw-primaryFloating launcher, send button, focus rings, accent text/links
backgroundColor--pcw-bgPanel and header background
userBubbleColor--pcw-user-bubbleUser message bubbles
agentBubbleColor--pcw-agent-bubbleAgent bubbles and inline cards
toolCallColor--pcw-tool-call-bgTool-call card background
borderRadius--pcw-radiusPanel, bubbles, buttons
fontFamily / fontSize--pcw-font / --pcw-font-sizeAll widget text
headerText--pcw-header-textHeader title label
placeholder--pcw-placeholderInput placeholder
emptyStateText--pcw-empty-state-textEmpty-conversation message
modedata-theme / prefers-color-schemeLight 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:

EventDescription
text_deltaStreaming text output
tool_call_startTool execution beginning
tool_call_resultTool execution complete
skill_activatedSkill activation
widgetWidget rendered inline
confirmation_requiredWrite operation needs approval
doneResponse 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, and WidgetRenderer (with its WidgetRegistry type).
  • 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.