Developer Guide
UI surfaces
The host app owns the shell around extension UI. Your extension can opt into specific surfaces when the default settings form or status layout is not enough.
Settings schema
Use manifest.settings when the app's default form is enough. It gives you persistence, per-persona overrides, and quick layouts without building custom React.
export const manifest = {
apiVersion: 6,
version: '1.0.0',
key: 'device-dashboard',
name: 'Device Dashboard',
category: 'device',
icon: 'Heart',
description: 'Shows live device controls in chat.',
settings: {
fields: {
connection: {
id: 'connection',
type: 'device_connection',
scope: 'global',
title: 'Device connection',
},
autoReconnect: {
id: 'autoReconnect',
type: 'toggle',
scope: 'global',
title: 'Reconnect on load',
default: true,
allowPersonaOverride: false,
},
},
mainLayout: ['connection', 'autoReconnect'],
quickLayout: ['connection'],
},
chatFooter: { order: 100 },
chatPanel: { order: 100 },
FooterComponent: DeviceFooter,
PanelComponent: DevicePanel,
};
function DeviceFooter({ extension, chat }) {
if (!extension.isEnabled) return null;
return (
<button onClick={() => chat.appendToMessage('Use the connected device.')}>
Add device instruction
</button>
);
}fieldsRecord<string, ExtensionSettingField>Map of field ids to field definitions.mainLayoutstring[]Full settings page order.quickLayoutstring[]Compact field order for dropdowns, panels, and footer fallbacks.Field types
togglefield typeBoolean switch.selectfield typeSingle-choice menu with options.sliderfield typeNumeric slider with min, max, and optional step.text / passwordfield typeShort text inputs. Use secret: true for sensitive values.textareafield typeLong-form text input with optional rows.numberfield typeNumeric input with optional bounds.device_connectionfield typeHost-owned connect or disconnect control.buttonfield typeHost-owned action control that calls invokeControl(fieldId).Global and persona inheritance
Persisted value fields follow a global-first model: default value, then saved global settings, then sparse persona overrides. Action fields keep their explicit scope and do not participate in inheritance.
Defaults
Secrets
renderDefaultSettings('global' | 'persona' | 'all') inside a custom SettingsComponent when you want richer layout without reimplementing the host form.Custom settings bodies
Add manifest.SettingsComponent when setup needs explanatory content, grouping, device walkthroughs, or a hybrid layout that still embeds the default form.
manifest.SettingsComponentReact componentCustom body inside the app-owned extension detail page.renderDefaultSettings(scope?)helperEmbeds the host-rendered form inside your custom settings UI.resetPersonaOverrides()functionClears sparse persona overrides and returns that persona to inheritance.useExtensionVariables(...)hookLets settings UIs edit the same runtime variables that hooks update later.Top-bar dropdowns
The top bar is still host-owned. Use quick layouts for small inline controls orStatusComponent for richer inspection and custom compact UI.
showInTopBarbooleanSet false to hide the extension from the top bar entirely.quickLayoutstring[]Compact settings rendered in the default dropdown body.manifest.StatusComponentReact componentCustom dropdown content for dashboards or richer controls.ExtensionStatusProps.onConnectfunctionOptional host callback for connection-focused status components.Chat footer drawer
Use the footer drawer when the extension needs controls, status text, or helper actions near the composer. It is better than chatBarActions for anything larger than a small icon.
manifest.chatFooter{ order: number }Registers footer content and controls ordering.manifest.FooterComponentReact componentCustom footer body that may return null when it should hide.ExtensionFooterProps.chatChatBarActionContextAppend to the composer or send a message immediately.ExtensionFooterProps.isDrawerExpandedbooleanLets the footer adapt to the drawer state.chatBarActionsChatBarAction[]Small icon buttons above the composer. Keep these tiny.Chat panel sections
Use the right-hand extension panel for persistent controls, device dashboards, or richer readouts that need more space than the footer or dropdown.
manifest.chatPanel{ order: number }Registers a section in the chat extension panel.manifest.PanelComponentReact componentCustom panel body for that section.ExtensionPanelProps.onManagefunctionOpens the app-owned manage/settings page for the extension.Fullscreen overlay (API v6+)
The overlay slot mounts a true fullscreen layer at the app root via React portal. Multiple extensions can register overlays and they composite simultaneously, z-ordered by overlay.order ascending. Use this for hypnosis effects, sensor HUDs, full-screen camera previews — anything that needs to escape route content and panel chrome.
manifest.overlay{ order: number }Opts into the overlay stack and sets the z-order. Lower renders behind.manifest.OverlayComponentReact componentMounted by the host inside a position:fixed inset:0 pointer-events:none wrapper. Return null when there's nothing to show.ExtensionOverlayProps.extensionExtensionState & { manifest }Same merged extension entry the other slot props receive, including resolvedSettings.ExtensionOverlayProps.runtime{ variables, emergencyStop, chat, ui }Bound runtime subset. Includes chat.sendMessage / chat.runTurn and ui.toast / ui.showModal so the overlay can advance the conversation or surface notifications.Transient notifications (toasts)
Use runtime.ui.toast(...) for fire-and-forget feedback that should not block the user — connection state changes, webhook confirmations, schedule-fired alerts. Modals are still the right surface when the extension needs a decision; toasts are for acknowledgements and warnings.
runtime.ui.toast(input)(input) => voidFire-and-forget. No return value.input.messagestringRequired body text.input.level'info' | 'success' | 'warning' | 'error'Visual styling. Defaults to info.input.durationMsnumberAuto-dismiss timeout. Defaults to 4000. Pass 0 to require manual dismiss.input.actionLabel + input.onActionstring + functionOptional inline action button. Toast auto-dismisses after the action fires.Prompt state, instances, and controls
Use promptState for structured prompt context that the app renders consistently. Use instances and invokeControl() for device-like extensions that expose connected resources or host-owned actions.
promptState
promptState.toys[] is for live toy or device state.
promptState.playerInfo[] is for metrics such as heart rate or other player data.
instances
Use instances for connected devices or other named resources the host should list.
Use invokeControl(fieldId) when a host-rendered action field needs extension logic.
When to use host layouts vs custom React
Stay with host layouts
Use custom components
Theming
The app ships several user-selectable themes — dark and light. They are driven by CSS custom properties on <html> (--color-bg, --color-surface, --color-text, --color-accent, and matching --color-success/danger/warning). Every custom surface you render inherits them — but only if you use theme-aware colors. Hardcoded hex values and stock Tailwind palette classes do not recolor, so a dropdown built with them stays dark and unreadable when the user picks a light theme.
Use theme tokens
bg-bg-deep, text-text-primary, orchid-*, eucalyptus-*, rose-*, border-border. In inline styles, rgb(var(--color-surface)), rgb(var(--color-text)), etc.Never hardcode
#0B0817), no stock palette names (emerald, slate, gray), and no opacity whites (bg-white/5). They ignore the active theme.