Skip to content

Web Component API

<rtc-agent> is the sole component exposed by RTC Agent. Built on Lit, it contains 57 sub-components and 23 Controllers internally, but presents only a clean Web Component interface externally — attribute configuration, event listening, and CSS variable customization.

Use createRtcAgent() to create a component instance. This is the recommended integration method, providing full type safety and lifecycle management:

import { createRtcAgent, z } from '@rtc-agent/component';
import type { RtcAgentConfig, RtcAgentWithLifecycle } from '@rtc-agent/component';
const config: RtcAgentConfig = {
// Basic configuration
appLabel: 'My AI Assistant',
theme: 'system',
lang: 'en-US',
databaseName: 'my-app-rtc',
// Server configuration
server: {
url: 'https://rtc-agent.cherish.chat',
redirectUri: '/auth/callback.html',
},
// SharedWorker URL (required for multi-tab support)
workerURL: '/rtc-agent/shared-worker.js',
// Authentication (AuthProvider, see "Auth Configuration" below)
auth: {
getToken: async () => localStorage.getItem('token') || '',
getUserId: () => 'user-123',
deviceId: 'uuid-from-your-backend', // required — must match the Device ID in the server-issued JWT
},
// Window configuration
window: {
defaultMode: 'normal',
draggable: true,
resizable: true,
bubblePosition: {
corner: 'bottom-right',
offset: { x: -24, y: 24 },
},
},
// Activity Bar configuration (hide unnecessary tabs)
activityBar: {
disabledActivities: ['settings'],
},
// Function registration
agentName: 'MyApp',
agentDescription: 'AI assistant for my application',
persona: 'You are a helpful assistant...',
groups: [
{
name: 'editor',
description: 'Editor operations',
functions: [
{
name: 'getCode',
description: 'Get the current code',
handler: () => window.editorAPI.getCode(),
returns: { zodSchema: z.string().describe('Current code in the editor') },
},
],
},
],
// Event handlers
on: {
ready: () => {
console.log('RTC Agent is ready');
},
},
};
const agent: RtcAgentWithLifecycle = createRtcAgent(config);
// ⚠️ Must be manually appended to DOM
document.body.appendChild(agent);
// Cleanup when done (removes all event listeners and WebSocket connections)
agent.destroy();

💡 Important: createRtcAgent() returns the component instance but does not auto-append to DOM. You must manually call document.body.appendChild(agent).

For simple scenarios or CDN quick previews, you can use HTML attributes directly:

<script type="module" src="https://cdn.jsdelivr.net/npm/@rtc-agent/component@0.3.0-rc.4/dist/index.js"></script>
<rtc-agent theme="dark" app-label="My AI Assistant"></rtc-agent>

⚠️ HTML attributes cannot configure complex object options like auth. For production, the factory function is recommended for full configuration.

The component package includes .d.ts type declarations — no extra @types package is needed. Once you install @rtc-agent/component, TypeScript recognizes the types automatically.

import type {
// Factory function config and return type
RtcAgentConfig, // Config object type for createRtcAgent()
RtcAgentWithLifecycle, // Return type of createRtcAgent() (includes destroy())
// Auth configuration
AuthConfig,
AuthProvider,
// Event callbacks and event type map
EventCallbacks,
RtcAgentEventDetailMap, // DOM event name → detail type mapping
// Window & Activity Bar
WindowConfig,
ActivityBarConfig,
ResolvedActivityBarConfig,
Activity, // 'chat' | 'files' | 'functions' | 'settings'
// Agent declarative config
AgentConfig,
AgentFunctionGroup,
// Data models
Session,
Message,
ConnectionState, // Connection state (for connectionStateChange callback)
// Function registration and validation
FunctionDef,
FunctionGroupDef,
ParameterDef,
ReturnDef,
OpenAPISchema,
VisualHooks,
ValidationError,
ValidationResult,
// Function Debugger
DebugHistoryItem,
FunctionDebugState,
LogEntry,
LogLevel,
ExecutionStatus,
HistoryPaginationState,
// Scenario documents
ScenarioDef,
ScenarioManifest,
RegistryConfig,
} from '@rtc-agent/component';
import {
// Factory function
createRtcAgent,
// Ready signal
whenReady,
// Imperative function registration
defineRegistry,
FunctionRegistry,
FunctionGroup,
// Zod validation utilities
z, // Re-export of the Zod library
validateParams,
formatValidationError,
buildValidator,
withValidation,
zodToParams,
zodToOpenAPISchema,
openApiToZod,
withMeta, // Attach example and other metadata to Zod schemas
// Scenario document loading
loadScenariosFromURL,
loadScenariosContent,
parseFrontmatter,
// Markdown documentation generation
generateFunctionMd,
generateFunctionsIndex,
generateScenariosIndex,
generateAgentMd,
// EventBus (advanced — observe internal tool call events)
EventBus,
eventBus,
createEventBus,
// Activity Bar config utilities
DEFAULT_ACTIVITY_BAR_CONFIG,
resolveActivityBarConfig,
// Internationalization
switchLocale,
initLocale,
getLocale,
persistLocale,
sourceLocale,
targetLocales,
localeContext,
isValidLocale,
// Theme
switchTheme,
initTheme,
getEffectiveTheme,
getStoredTheme,
persistTheme,
// Exception class
CancelledError, // User cancellation error (throw from hooks.onStart to cancel execution)
} from '@rtc-agent/component';

Use the RtcAgent type with querySelector to get full property hints:

import type { RtcAgent } from '@rtc-agent/component';
const agent = document.querySelector<RtcAgent>('rtc-agent');
// ✅ Full property and event type hints
agent!.theme = 'dark';
agent!.windowConfig = { embedded: true };
agent!.addEventListener('rtc-agent-ready', () => {
// ...
});

The component extends HTMLElementEventMap, so addEventListener automatically infers the detail type:

const agent = document.querySelector('rtc-agent')!;
agent.addEventListener('rtc-theme-change', (e) => {
// e.detail is automatically inferred as { theme: 'light' | 'dark' | 'system' }
console.log(e.detail.theme);
});

The destroy() method performs a complete, ordered teardown of the component:

const agent = createRtcAgent(config);
document.body.appendChild(agent);
// When the component is no longer needed:
agent.destroy();

destroy() executes the following steps in order:

  1. Removes the element from the DOM — calls this.remove() on the host element, which triggers disconnectedCallback (clears DOM event listeners, UIUpdateBus subscriptions, RTC processor, auth callbacks, auto-save timers, etc.)
  2. Clears pending auth configuration references — releases factory-captured _pendingAuthConfig / _pendingDynamicAuth / _pendingAuthProvider
  3. Calls all DOM event unsubscribe functions — every addEventListener registered through the config on field is removed
  4. Calls all EventBus unsubscribe functions — internal subscriptions (tool call event bridging, etc.) are torn down

💡 When to call: Always call destroy() before removing the component from the page (e.g., in SPA route changes, modal close, or framework unmount hooks). Skipping it leaks event listeners and WebSocket connections.

Note: destroy() does not clear authentication tokens from localStorage (to support Vite HMR — clearing tokens would log the user out on every code change during development). To log out explicitly, call auth.logout().

Full integration guide: Integration Tutorial

The auth field in RtcAgentConfig controls how the component obtains authentication credentials. The recommended approach is AuthProvider mode, which delegates all authentication concerns to your provider.

💡 Recommended Approach: For enterprise integration scenarios, Token Exchange mode is strongly recommended. It offers simple integration, great user experience, and eliminates the need to implement a full OAuth2 flow. See Authentication & Authorization - Complete Token Exchange Guide for details.

Full control over the authentication lifecycle. Implement this when your host application already manages auth state and you want the component to integrate with it.

AuthProvider supports two authentication modes:

Modetype valueRecommendationUse case
Token Exchange (Recommended)'token-exchange'⭐⭐⭐⭐⭐Enterprise integration with existing auth systems
OAuth2 Redirect'oauth2-redirect'⭐⭐⭐Standalone apps requiring full login flow

When the host application holds an external JWT (e.g., an admin JWT issued by admin-server), the component can exchange it for an RTC main-server JWT via Token Exchange. This is suitable for Admin UI integration scenarios.

const config: RtcAgentConfig = {
// ...other config
auth: {
type: 'token-exchange',
getExchangeToken: async () => adminAuthStore.getAdminToken(),
isLoggedIn: () => adminAuthStore.isAuthenticated(),
logout: async () => { await adminAuthStore.clearSession(); },
getUserId: () => adminAuthStore.getUserId(),
deviceId: 'uuid-from-your-backend',
},
};

💡 Token Exchange mode does not require getToken() or refreshToken() — the component handles JWT exchange and refresh internally. The main server must configure token_exchange.external_issuers in config.yaml to trust the corresponding JWT issuer.

📚 Complete Integration Guide: See Authentication & Authorization - Complete Token Exchange Integration Guide for architecture, configuration, JWT signing, JWKS endpoints, and more.

const config: RtcAgentConfig = {
// ...other config
auth: {
// type: 'oauth2-redirect', // default, can be omitted
getToken: async () => myAuthStore.getAccessToken(),
refreshToken: async () => myAuthStore.refreshAccessToken(),
isLoggedIn: () => myAuthStore.isAuthenticated,
logout: async () => { await myAuthStore.signOut(); },
getUserId: () => myAuthStore.getUserId(), // returns the current user's unique ID
deviceId: 'uuid-from-your-backend', // required — must match the Device ID in the server-issued JWT
},
};

💡 PKCE handled automatically: In OAuth2 redirect mode, the component’s built-in login dialog automatically generates PKCE code_verifier / code_challenge (RFC 7636) — sending code_challenge in the authorization request and code_verifier during token exchange. Developers do not need to handle PKCE parameters manually — PKCE is enabled by default when using the built-in login flow. If the host application manages the OAuth2 flow independently (without the built-in login dialog), it must implement PKCE on its own.

MethodRequiredApplicable modesDescription
typeOptionalBoth'oauth2-redirect' (default) or 'token-exchange'
getToken()Required for OAuth2 modeoauth2-redirectAsync — returns the current authentication token
refreshToken()Required for OAuth2 modeoauth2-redirectAsync — refreshes the token when expired
getExchangeToken()Required for Token Exchange modetoken-exchangeAsync — returns the external JWT, which the component exchanges for an RTC JWT
isLoggedIn()RequiredBothSynchronous — returns boolean indicating whether the user is authenticated
logout()OptionalBothAsync — called when the component needs to terminate the session
getUserId()OptionalBothSynchronous — returns the current user’s unique ID. Strongly recommended; otherwise all users share the same IndexedDB
deviceIdRequiredBothDevice unique identifier, must match the Device ID embedded in the server-issued JWT

For detailed guidance, see Authentication & Authorization.

AttributeTypeDefaultDescription
🎨 theme"light" | "dark" | "system""system"Theme mode. system follows the OS setting. Can be set dynamically via JS: agent.theme = 'dark'
🌐 lang"zh-CN" | "en-US""zh-CN"UI language. Priority: HTML attribute > localStorage > browser language > default (zh-CN). Can be set dynamically via JS: agent.lang = 'en-US'
💾 database-namestring"rtc-agent"IndexedDB name prefix. The final DB name is {prefix}-{userId}. For example, <rtc-agent database-name="my-app-rtc"> produces my-app-rtc-{userId}
📛 app-labelstring"RTC Agent"Title bar text + minimized bubble tooltip
🖼️ bubble-iconstringDefault iconSVG / HTML content displayed inside the minimized bubble
📄 scenarios-urlstring—URL of the scenario manifest, pointing to manifest.json
🔗 server-urlstring""Server address. Falls back to the current page’s domain when empty
🔁 redirect-uristringwindow.location.origin + '/auth/callback.html'OAuth callback URL. Supports both absolute and relative paths
👷 worker-urlstring""SharedWorker file URL. Used for multi-tab WebSocket sharing. Use workerURL in factory config

JS Properties (set via JavaScript only, not HTML attributes):

PropertyTypeDefaultDescription
⚙️ agentConfigobjectnullDeclarative function registration (recommended approach)
📦 registryFunctionRegistrynullImperative function registration (created via defineRegistry)
🪟 windowConfigWindowConfignullWindow behavior configuration (mode, size, interaction limits)
🎛️ activityBarConfigActivityBarConfignullActivity Bar button visibility configuration
🖼️ logo{ light?: string; dark?: string }nullCustom logo replacing the default Logo on the login page, empty state, and settings page. Supports separate light/dark SVG variants
<!-- Minimal integration -->
<rtc-agent></rtc-agent>
<!-- Custom theme and title -->
<rtc-agent theme="dark" app-label="My AI Assistant"></rtc-agent>
<!-- Custom database name prefix and language -->
<rtc-agent database-name="my-app-rtc" lang="en-US"></rtc-agent>
<!-- With scenario docs and function registration -->
<rtc-agent
app-label="Order Assistant"
scenarios-url="https://example.com/scenarios"
.agentConfig=${{
name: 'OrderApp',
persona: 'You are an order management assistant',
groups: [{ /* ... */ }]
}}
></rtc-agent>
// Window configuration (JS property, set after rtc-agent-ready)
const agent = document.querySelector('rtc-agent');
agent.addEventListener('rtc-agent-ready', () => {
// Embedded panel: disable drag/resize/buttons, default to maximized
agent.windowConfig = { embedded: true };
// Keep only chat, hide files/settings buttons
agent.activityBarConfig = {
disabledActivities: ['files', 'settings'],
};
});

All events support two listening patterns: DOM events (via addEventListener) and config callbacks (via the on field in RtcAgentConfig).

💡 Event naming convention: DOM event names use the rtc- prefix with kebab-case (e.g. rtc-theme-change). Config callback names use camelCase (e.g. themeChange). The factory function internally maps callbacks to the corresponding DOM events.

DOM EventConfig CallbackTriggerPurpose
🟢 rtc-agent-readyon.readyComponent initialization completeSafe to access the component instance and set attributes
🟡 rtc-before-destroyon.beforeDestroydisconnectedCallback runs, before cleanupLast chance to read state or cancel teardown
🎨 rtc-theme-changeon.themeChangeTheme changes (attribute, JS property, or system preference)Sync external UI with the component’s theme. event.detail is { theme: 'light' | 'dark' | 'system' }
DOM EventConfig CallbackTriggerPurpose
📨 rtc-before-message-sendon.beforeMessageSendUser submits a message, before it is sentInspect or modify the message. Return false to cancel the send

The beforeMessageSend callback receives { message: { content: string, metadata?: Record<string, unknown> } } and must return boolean | Promise<boolean>. Return false to cancel the send. You can also mutate message.content in place to rewrite the message before it goes out.

DOM EventConfig CallbackTriggerevent.detail
🔌 rtc-connection-retryon.connectionRetryUser clicks the retry buttonvoid
🔐 rtc-auth-login-requestedon.authLoginRequestedUser requests loginvoid
❌ rtc-auth-refresh-failedon.authErrorAuth refresh failedvoid
🚪 rtc-auth-logouton.authLogoutUser logs outvoid
📡 rtc-connection-state-changeon.connectionStateChangeConnection state transitions{ state: ConnectionState }
✅ rtc-auth-loginon.authLoginLogin successful (incl. initial load, refresh, manual login){ userId: string }
DOM EventConfig CallbackTriggerevent.detail
📝 rtc-session-createdon.sessionCreatedNew session created{ session: Session }
🔀 rtc-session-switchedon.sessionSwitchedSession switched{ id: string }
✏️ rtc-session-renamedon.sessionRenamedSession renamed{ id: string, title: string }
🗑️ rtc-session-deletedon.sessionDeletedSession deleted{ id: string }
📥 rtc-message-receivedon.messageReceivedAI message received{ message: Message }
📤 rtc-message-senton.messageSentUser message sent{ message: Message }

Tool call events are bridged from the internal EventBus and only support the on.* config callback pattern — they are not dispatched as DOM events. This avoids putting high-frequency tool call events on the DOM event system.

Config CallbackTriggerCallback ArgsEventBus Source
on.toolCallStartTool call begins{ path, params }function:start
on.toolCallSuccessTool call completes successfully{ path, result }function:success
on.toolCallErrorTool call fails{ path, error }function:error
on.toolCallProgressTool call reports intermediate progress{ path, progress }function:progress

path is the full function path (e.g. "myGroup.myFunction"); params are the validated input arguments.

const agent = createRtcAgent({
// ...other config
on: {
// Lifecycle
ready: () => {
console.log('RTC Agent is ready');
},
beforeDestroy: () => {
console.log('Component is about to be destroyed');
},
themeChange: (detail) => {
console.log('Theme changed to:', detail.theme);
document.body.dataset.theme = detail.theme;
},
// Message interception — validate before send
beforeMessageSend: async ({ message }) => {
// Block empty messages
if (!message.content.trim()) {
return false;
}
// Append a signature
message.content += '\n\n— Sent from My App';
return true;
},
// Tool call tracking (config callbacks only, not DOM events)
toolCallStart: ({ path, params }) => {
console.log(`Tool call started: ${path}`, params);
},
toolCallSuccess: ({ path, result }) => {
console.log(`Tool call succeeded: ${path}`, result);
},
toolCallError: ({ path, error }) => {
console.error(`Tool call failed: ${path}`, error);
},
toolCallProgress: ({ path, progress }) => {
updateProgressBar(path, progress);
},
},
});

The same lifecycle events can also be listened to via DOM addEventListener (useful when you cannot set config callbacks):

const agent = document.querySelector('rtc-agent');
agent.addEventListener('rtc-agent-ready', () => {
// Component is ready, safe to operate
agent.theme = 'dark';
agent.lang = 'en-US';
agent.appLabel = 'Custom Title';
});
agent.addEventListener('rtc-theme-change', (e) => {
console.log('Theme is now:', e.detail.theme);
});
agent.addEventListener('rtc-before-message-send', (e) => {
const { message } = e.detail;
if (containsSensitiveWords(message.content)) {
e.preventDefault(); // cancel send
}
});

💡 Best Practice: Always wait for the rtc-agent-ready event before interacting with the component to avoid errors caused by uninitialized state.

<rtc-agent> includes three built-in window modes, supporting floating, fullscreen, and minimized bubble:

InteractionDescription
🖱️ DragThe title bar serves as the drag handle
↔️ ResizeSupports 8-directional resize operations
⌨️ KeyboardArrow keys move the window; Shift to accelerate
📏 Viewport constraintWindow always stays within the visible area and cannot be dragged off-screen

Use the windowConfig property to control default window behavior and interaction limits:

agent.windowConfig = {
// Default window mode
defaultMode: 'maximized', // 'normal' | 'maximized' | 'minimized'
// Embedded mode (shortcut)
// Equivalent to: defaultMode: 'maximized' + draggable: false + resizable: false
// + showMinimize: false + showMaximize: false
// CSS uses position: relative to fill the parent container (instead of position: fixed filling the viewport)
embedded: true,
// Fine-grained control
draggable: false, // Whether the window can be dragged
resizable: false, // Whether the window can be resized
showMinimize: false, // Whether to show the minimize button
showMaximize: false, // Whether to show the maximize button
showClose: false, // Whether to show the close button
// Size and position
initialSize: { width: 420, height: 640 },
initialPosition: { x: 100, y: 100 },
minWidth: 350,
minHeight: 520,
maxWidth: Infinity,
maxHeight: Infinity,
// Minimized bubble position
bubblePosition: {
corner: 'bottom-right', // Origin corner of the coordinate system
offset: { x: -20, y: 20 }, // Cartesian coordinate offset
},
};

bubblePosition uses a mathematical Cartesian coordinate system to control the position of the minimized bubble:

FieldTypeDescription
corner'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'The corner of the host application where the coordinate origin is placed
offset.xnumberHorizontal offset (positive = right, negative = left)
offset.ynumberVertical offset (positive = up, negative = down, mathematical coordinate system)

Examples:

// Bottom-right corner, 20px inward offset (default)
bubblePosition: { corner: 'bottom-right', offset: { x: -20, y: 20 } }
// Top-left corner, 20px offset to bottom-right
bubblePosition: { corner: 'top-left', offset: { x: 20, y: -20 } }
// Bottom-left corner, 30px offset to top-right
bubblePosition: { corner: 'bottom-left', offset: { x: 30, y: 30 } }

💡 Expand Direction: On the first restore from minimized state, the window expands based on the corner configuration. For example, with corner: 'bottom-right', the window’s bottom-right corner aligns with the bubble position, expanding toward the upper-left. Subsequent minimize/restore cycles use the remembered position.

Common Scenarios:

ScenarioConfiguration
Embedded panel{ embedded: true }
Fixed position window{ draggable: false, resizable: false }
No minimize button{ showMinimize: false, defaultMode: 'maximized' }
Floating chat windownull (uses defaults)

Use the activityBarConfig property to control button visibility in the Activity Bar:

agent.activityBarConfig = {
// Activities to hide (chat is always visible and cannot be hidden)
disabledActivities: ['files', 'settings'],
// Default active activity
defaultActivity: 'chat',
};
ActivityDescriptionHideable
💬 chatChat interface❌ Always visible
📁 filesFile manager✅ via disabledActivities
🛠️ functionsFunction Debugger panel✅ via disabledActivities
⚙️ settingsSettings panel✅ via disabledActivities

The Function Debugger is an optional panel in the Activity Bar that provides a complete function debugging interface:

  • Function Tree: Displays all registered functions grouped by their function groups, with expand/collapse state persisted to localStorage
  • Parameter Editor: JSON syntax highlighting + validation, with Zod schema auto-derivation for parameter templates
  • Execution Console: Color-coded log output (info/warn/error/debug) with real-time execution results
  • Execution History: Bottom drawer panel with pagination, per-function filtering, and full details (params, logs, result) for each entry
  • Parameter Docs: Recursively expanded parameter table reusing schemaToTypeString for type strings

Add 'functions' to disabledActivities to hide the Function Debugger button (suitable for end-user scenarios where debugging is not needed).

The component uses 23 Controllers internally to manage state. 11 core state Controllers handle business logic, and 12 UI Controllers handle interface interactions. Controllers do not reference each other directly; instead, the root component <rtc-agent> acts as the central hub orchestrating cross-Controller communication:

💡 Design Principle: Controllers are decoupled from each other; all cross-Controller communication goes through the root component. Sub-components obtain state via @lit/context and do not hold direct Controller references.

RTC Agent includes complete internationalization support, implemented with @lit/localize for runtime language switching.

Language CodeLanguageDescription
zh-CNSimplified ChineseDefault language (source)
en-USEnglishTarget language

Switch the interface language using the switchLocale() API:

import { switchLocale } from '@rtc-agent/component';
// Switch to English
await switchLocale('en-US');
// Switch to Chinese
await switchLocale('zh-CN');

Language selection is automatically saved to localStorage (key: rtc-agent-locale) and persists across page refreshes.

Use the @localized() decorator and msg() function in components:

import { localized, msg } from '@lit/localize';
import { consume } from '@lit/context';
import { localeContext } from '@rtc-agent/component';
@localized()
@customElement('my-component')
export class MyComponent extends LitElement {
@consume({ context: localeContext, subscribe: true })
private _localeCtx!: LocaleContextValue;
render() {
// React to locale changes
void this._localeCtx.locale;
return html`
<div>${msg('Welcome to RTC Agent')}</div>
`;
}
}
  1. Mark text: Wrap translatable text with msg() in components
  2. Extract translations: Run npm run localize:extract to generate XLIFF files
  3. Translate text: Edit xliff/en-US.xlf file
  4. Build translations: Run npm run localize:build to generate language packs

For detailed guidance, see Internationalization Integration Guide.

RTC Agent supports light, dark, and system-follow themes, switchable at runtime via the switchTheme() API.

import { switchTheme } from '@rtc-agent/component';
// Switch to dark theme
switchTheme('dark');
// Switch to light theme
switchTheme('light');
// Follow system preference
switchTheme('system');

switchTheme() simultaneously:

  1. Updates the theme property on all <rtc-agent> elements
  2. Persists to localStorage (key: rtc-agent-theme)
  3. Sets the data-theme attribute on document.documentElement (resolved effective theme)
import { getStoredTheme, getEffectiveTheme } from '@rtc-agent/component';
// User preference (may be 'system')
const preference = getStoredTheme(); // 'light' | 'dark' | 'system'
// Resolved effective theme (always 'light' or 'dark')
const effective = getEffectiveTheme(); // 'light' | 'dark'

Call initTheme() at app startup to restore the theme preference from localStorage:

import { initTheme } from '@rtc-agent/component';
initTheme();

Listen for the rtc-theme-change event to sync external UI:

agent.addEventListener('rtc-theme-change', (e) => {
console.log('Theme switched to:', e.detail.theme);
});

For detailed guidance, see Settings System.

CSS variables allow you to customize the component’s appearance and dimensions without modifying source code:

rtc-agent {
/* Window default dimensions */
--rtc-window-default-width: 420px;
--rtc-window-default-height: 640px;
/* Minimized bubble size */
--rtc-bubble-size: 40px;
/* User-defined font size (affects all text) */
--rtc-font-size-user: 16px;
/* Brand colors */
--rtc-color-primary-rgb: 39 65 254; /* Light: #2741FE */
--rtc-color-accent: #2741FE;
}
VariableDefaultDescription
--rtc-window-default-width420pxDefault width of the floating window
--rtc-window-default-height640pxDefault height of the floating window
--rtc-bubble-size40pxDiameter of the minimized bubble
--rtc-font-size-user14pxUser-defined base font size (12-24px)
--rtc-color-primary-rgbLight: 39 65 254
Dark: 26 122 176
Primary color RGB value (for opacity calculations)
--rtc-color-accentLight: #2741FE
Dark: #1A7AB0
Accent color (links, buttons, etc.)

Setting --rtc-font-size-user proportionally scales all font sizes:

TokenFormulaExample (base=14px)
--rtc-font-size-xsbase * 0.85712px
--rtc-font-size-smbase * 0.92913px
--rtc-font-size-basebase14px
--rtc-font-size-mdbase * 1.14316px
--rtc-font-size-lgbase * 1.28618px
--rtc-font-size-xlbase * 1.42920px
--rtc-font-size-2xlbase * 1.71424px

Use the logo property to replace the default brand Logo on the login page, empty state, and settings page. It supports separate light and dark SVG variants that automatically switch with the theme.

const agent = createRtcAgent({
// ...other config
logo: {
light: '<svg viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"><circle cx="40" cy="40" r="36" fill="#2741FE"/><text x="40" y="52" text-anchor="middle" fill="white" font-size="32" font-weight="bold">M</text></svg>',
dark: '<svg viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"><circle cx="40" cy="40" r="36" fill="#1A7AB0"/><text x="40" y="52" text-anchor="middle" fill="white" font-size="32" font-weight="bold">M</text></svg>',
},
});

You can also set only one variant — missing variants fall back to the default Logo:

agent.logo = {
light: '<svg>...</svg>', // only replace the Logo in light theme
// dark is not set — dark theme uses the default Logo
};

The component internally uses the <rtc-logo> sub-component + Lit Context API to implement automatic theme switching:

  • <rtc-logo> consumes LogoContext and renders the corresponding Logo variant based on the current theme (light / dark)
  • Supports the theme attribute: 'light' | 'dark' | 'system', where system listens to the prefers-color-scheme media query
  • If no custom Logo is provided, it renders the default RTC Agent Logo

bubbleIcon and logo are two independent concepts:

PropertyLocationPurpose
bubbleIconMinimized bubbleIcon inside the circular bubble after the window is minimized
logoLogin page, empty state, settings pageBrand identity for displaying company/product branding

Both can be customized with SVG, but they serve different purposes.

To modify the default Logo, replace the following SVG files:

FileDescriptionUsage
logo.svgBlue sky LogoLight theme
logo-dark.svgNight sky LogoDark theme

RTC Agent automatically attempts to establish a connection when the component initializes. Connection state is managed through an internal state machine, with the frontend reflecting connection progress in real-time.

Design Principle: Connection failures do not trigger automatic retries. Instead, control is handed to the user, who can check network conditions before manually triggering a reconnection. This avoids wasting resources on repeated attempts during network outages.

When connection fails, users can manually reconnect in the following ways:

Method 1: Click the title bar retry button

Section titled “Method 1: Click the title bar retry button”

After connection fails, a red retry button appears in the title bar. Click to trigger reconnection.

const agent = document.querySelector('rtc-agent');
// Manually trigger reconnection
await agent.reconnect();

Connection state can be queried through the following read-only properties:

PropertyTypeDescription
connectionFailedbooleanWhether connection has failed
connectionErrorstringError message when connection failed
// Check connection state
agent.addEventListener('rtc-agent-ready', () => {
console.log('Connection failed:', agent.connectionFailed);
console.log('Connection error:', agent.connectionError);
if (agent.connectionFailed) {
console.error('Connection failed:', agent.connectionError);
}
});

Note: Detailed connection states (disconnected, connecting, connected, reconnecting) are managed internally by the component and not exposed externally. External code can only determine connection success via connectionFailed and connectionError.

The title bar shows different visual feedback based on connection state:

StateStatus Dot ColorTextRetry Button
connected🟢 Green“Connected”❌ Hidden
connecting🟡 Yellow pulse“Connecting”❌ Hidden
reconnecting🟡 Yellow pulse“Reconnecting”❌ Hidden
disconnected🔴 Red“Disconnected”❌ Hidden
Connection failed🔴 Red blinking“Connection failed: …”✅ Shown

The component uses a three-layer style architecture, progressing from foundational tokens to top-level variables:

LayerContentDescription
🏗️ Design TokensSpacing, typography, radius, shadow, transition, z-indexFoundational design constants ensuring visual consistency
🌓 Color ThemesLight / Dark (VS Code style)Two complete color schemes with automatic adaptation
🔧 CSS VariablesComponent-level customization (window dimensions, bubble size)Overridable by host applications for personalization

<rtc-agent> exposes a window.rtcAgentDebug object in all builds (dev + prod), providing 40+ methods for E2E testing, debugging, and automation.

const debug = window.rtcAgentDebug;
DomainMethodsDescription
StategetState(), clearData(), seedData()State access and data management
AuthloginAs(userId, tokens?), logout()Authentication simulation
VirtualFSlistFiles(path?), readFile(path), writeFile(path, content), deleteFile(path)Virtual file system operations
SessioncreateSession(), switchSession(id), deleteSession(id), renameSession(id, title), getSessions(), getCurrentSessionId()Session management
MessagesendMessage(content), getMessages(), addDemoMessage(content, role?), clearMessages()Message operations
Tool CallgetToolCalls(), addPendingToolCall(call), approveToolCall(id), denyToolCall(id), approveAllToolCalls(toolName)Tool call simulation
UI Controlclick(selector), scrollIntoView(selector), typeText(selector, text)UI interaction simulation
ToastshowToast(message, type?), getToasts()Toast notifications
SettingsgetSettings(), updateSettings(section, patch)Settings management
ActivitysetActivity(activity), getActivity()Activity Bar control
NetworksimulateOffline(), restoreNetwork(), isOfflineNetwork simulation
EventtriggerEvent(name, detail?)Custom event simulation
MetricsgetMetrics()Performance metrics collection
Componentelement, waitForReady(timeout?), waitForConnected(timeout?)Component reference and waiting
Logslogs, clearLogs()Log access
const debug = window.rtcAgentDebug;
// Wait for component to be ready
const agent = await debug.waitForReady(5000);
// Wait for connection
await debug.waitForConnected(10000);
// Create a session and send a message
const sessionId = debug.createSession();
debug.switchSession(sessionId);
await debug.sendMessage('Hello, help me write a Hello World');
// Simulate offline mode
debug.simulateOffline();
console.log('Is offline:', debug.isOffline);
// Restore network
debug.restoreNetwork();
// Get all messages
const messages = debug.getMessages();
console.log('Message count:', messages.length);
// Get current state
const state = debug.getState();
console.log('Current session:', debug.getCurrentSessionId());
// Playwright example
test('send message and get response', async ({ page }) => {
await page.goto('/');
// Wait for component to be ready
await page.waitForFunction(() => window.rtcAgentDebug?.waitForReady);
await page.evaluate(() => window.rtcAgentDebug.waitForReady(5000));
// Send a message
await page.evaluate(() => window.rtcAgentDebug.sendMessage('Hello'));
// Wait for AI response
await page.waitForFunction(() => {
const msgs = window.rtcAgentDebug.getMessages();
return msgs.some(m => m.role === 'assistant');
}, { timeout: 30000 });
// Verify response
const messages = await page.evaluate(() => window.rtcAgentDebug.getMessages());
expect(messages.length).toBeGreaterThan(1);
});

💡 The Debug API is available in production builds too, which is useful for diagnosing live issues. However, we recommend using data-modifying methods like seedData() and clearData() only in development and test environments.

AreaBehavior
⌨️ Input AreaTextarea + bottom toolbar; Enter to submit, Shift+Enter for newline; toolbar includes attach, commands, scenario selection, mode toggle, send/stop; supports file upload (click attach button or Ctrl/Cmd+V to paste), files are first cached to local IndexedDB then uploaded to S3 in background; message can only be sent after all files are uploaded; when scenarios-url is configured, a scenario selection button is shown in the toolbar, opening the rtc-scenario-panel component for users to pick a scenario
📎 File UploadClick the attach button to open the file picker (supports image/* and text/*), or use Ctrl/Cmd+V to paste clipboard files; files are first cached locally (FileStorage.cacheFileForUpload()), thumbnails display immediately; background upload to S3 with real-time progress display; failed uploads can be retried by clicking the retry button; send condition: all files must be in loaded state
🖼️ File PreviewClick a thumbnail to open a fullscreen preview modal (rtc-file-preview-modal); supports image preview (object-fit: contain) and text preview (monospace font, large files truncated to 1000 lines); three close methods: ESC key, backdrop click, close button; falls back to S3 presigned URL when download fails
📨 Message ListAuto-scrolls to bottom; “New messages” button shown when user scrolls away; supports Markdown rendering and code highlighting
⚡ Tool Confirmation DialogDisplays tool name and parameters; Yes / No buttons; clicking the background is equivalent to rejecting
🔄 Connection RetryA retry button is displayed when the connection fails; you can also call agent.reconnect() programmatically to reconnect
🗂️ Tool Call CardsEach tool call is displayed as a timeline card. Clicking the timeline dot copies the current content to the clipboard (input cards copy parameters, reply cards copy output). For the todoWrite tool, cards render as a task list with status icons (✓ completed / ● in progress / ○ pending) instead of raw JSON

Each file has three states during upload, managed via the uploadStates Map:

StateIconDescription
loading🔄 Progress ringUploading to S3, showing 0-100% progress
loaded✅ CompleteUpload successful, thumbnail displays normally
error❌ ErrorUpload failed, retry button shown

Send button availability is controlled by the _canSubmit computed property:

// Simplified logic
get _canSubmit(): boolean {
if (!this._hasContent) return false;
// Check if any files are uploading or failed
const hasUploadingOrFailedFiles = Array.from(this._uploadStates.values())
.some(state => state === 'loading' || state === 'error');
return !hasUploadingOrFailedFiles;
}
  • _hasContent is true when there is text or file attachments
  • Send button is enabled when all files are in loaded state
  • Send button is disabled when any file is in loading or error state

File preview related sub-components:

ComponentPurposeKey Features
rtc-file-preview-areaAttachment preview area containerHorizontal scrolling list showing all attachment thumbnails; supports edit mode (shows delete buttons) and readonly mode (message display)
rtc-file-thumbnailSingle file thumbnailShows thumbnail for image files (object-fit: cover), file icon for text files; displays upload progress ring, sync status badge, filename
rtc-file-preview-modalFullscreen preview modalImage preview (object-fit: contain), text preview (monospace font, large file truncation); three close methods: ESC, backdrop click, close button

FileStorage Context:

File preview components obtain the FileStorage instance via FileStorageContext for:

  • Loading thumbnails: fileStorage.getThumbnailUrl({md5, ext})
  • Downloading files: fileStorage.download(md5, ext, {signal})
  • Getting presigned URLs: fileStorage.getPresignedUrl({md5, ext}, expires) (fallback approach)

FileStorage is the high-level file storage abstraction exposed by the component, obtained via FileStorageContext. It supports an offline-first upload/download lifecycle, batch operations, and cache management.

MethodSignatureDescription
cacheFileForUpload(file: File) => Promise<FileInfo>Offline-first: immediately writes the file to local IndexedDB cache (calculates MD5), returns FileInfo (syncStatus: 'pending'). Thumbnails display immediately; actual S3 upload is performed by a subsequent upload() call
upload(opts: UploadOptions) => Promise<FileInfo>Uploads the file to S3. Supports onProgress callback and signal for abort. After upload, syncStatus changes to 'synced'
download(md5: string, ext: string, opts?: DownloadOptions) => Promise<Blob>Downloads a file. Reads from local cache first; falls back to S3 when cache misses. Supports forceRefresh to bypass cache
getThumbnailUrl(opts: {md5, ext}) => Promise<string>Gets a thumbnail Blob URL. Uses shared cache with reference counting to avoid duplicates
releaseThumbnailUrl(url: string) => voidReleases a thumbnail Blob URL reference. Revokes the blob when refcount reaches zero
getPresignedUrl(opts: {md5, ext}, expires?: number) => Promise<string>Gets an S3 presigned URL (fallback approach)
MethodSignatureDescription
uploadBatch(files: File[], opts?: BatchOptions) => Promise<FileInfo[]>Batch upload files with concurrency control and error handling strategy ('continue' skips failures / 'cancel' aborts all)
downloadBatch(items: {md5, ext}[], opts?: BatchOptions) => Promise<Blob[]>Batch download files
deleteBatch(items: {md5, ext}[]) => Promise<void>Batch delete files (local cache + S3)
MethodSignatureDescription
getCacheStats() => Promise<CacheStats>Returns cache statistics (total files, total size, etc.)
evictCache(opts?: {maxAge?: number}) => Promise<number>Evicts expired cache files, returns the number evicted
resumeInterruptedUploads() => Promise<void>Resumes interrupted uploads (call after network recovery)

💡 Offline-First: cacheFileForUpload() writes the file to IndexedDB immediately, so thumbnails display even without network. A subsequent upload() syncs the file to S3 — syncStatus transitions from 'pending' to 'syncing' to 'synced' (or 'failed'). UploadOptions.cacheTtlMs controls local cache expiration (default: 7 days).