import { IconNode } from 'lucide'; /** * Plugin interface for customizing widget components */ interface AgentWidgetPlugin { /** * Unique identifier for the plugin */ id: string; /** * Optional priority (higher = runs first). Default: 0 */ priority?: number; /** * Custom renderer for message bubbles * Return null to use default renderer */ renderMessage?: (context: { message: AgentWidgetMessage; defaultRenderer: () => HTMLElement; config: AgentWidgetConfig; }) => HTMLElement | null; /** * Custom renderer for launcher button * Return null to use default renderer */ renderLauncher?: (context: { config: AgentWidgetConfig; defaultRenderer: () => HTMLElement; onToggle: () => void; }) => HTMLElement | null; /** * Custom renderer for panel header * Return null to use default renderer */ renderHeader?: (context: { config: AgentWidgetConfig; defaultRenderer: () => HTMLElement; onClose?: () => void; }) => HTMLElement | null; /** * Custom renderer for composer/input area * Return null to use default renderer */ renderComposer?: (context: { config: AgentWidgetConfig; defaultRenderer: () => HTMLElement; onSubmit: (text: string) => void; /** * When true, the assistant stream is active: same moment `session.isStreaming()` becomes true. * Prefer wiring controls to `data-persona-composer-disable-when-streaming` plus `setComposerDisabled` * in the host, or react to `footer.dataset.personaComposerStreaming === "true"`. */ streaming: boolean; /** * Legacy alias: host disables the primary submit control while `streaming` is true. * @deprecated Use `streaming` for new plugins. */ disabled: boolean; /** Opens the hidden file input when `config.attachments.enabled` is true (no-op otherwise). */ openAttachmentPicker: () => void; /** From `config.composer.models` */ models?: Array<{ id: string; label: string; }>; /** From `config.composer.selectedModelId` */ selectedModelId?: string; /** Updates `config.composer.selectedModelId` for the running widget instance. */ onModelChange?: (modelId: string) => void; /** * Same behavior as the built-in mic when voice is enabled. * Omitted when `config.voiceRecognition.enabled` is not true. */ onVoiceToggle?: () => void; }) => HTMLElement | null; /** * Custom renderer for reasoning bubbles * Return null to use default renderer */ renderReasoning?: (context: { message: AgentWidgetMessage; defaultRenderer: () => HTMLElement; config: AgentWidgetConfig; }) => HTMLElement | null; /** * Custom renderer for tool call bubbles * Return null to use default renderer */ renderToolCall?: (context: { message: AgentWidgetMessage; defaultRenderer: () => HTMLElement; config: AgentWidgetConfig; }) => HTMLElement | null; /** * Custom renderer for `ask_user_question` tool calls. * * When a plugin returns an `HTMLElement`, it is inserted into the transcript * in place of the default (which is no transcript bubble: the built-in * renders a sheet over the composer). The built-in composer-overlay sheet * is suppressed so the plugin's UI fully owns the interaction. * * Return `null` to fall through to the built-in overlay sheet. * * The context gives you a pre-parsed `payload` (may be partial while the * tool call is still streaming: check `complete`) and two callbacks: * `resolve(answer)` resumes the paused LOCAL tool with the user's answer, * and `dismiss()` cancels with the sentinel `"(dismissed)"`. * * @example * ```typescript * renderAskUserQuestion: ({ payload, resolve, dismiss }) => { * const prompt = payload.questions?.[0]; * if (!prompt) return null; * const root = document.createElement("div"); * root.textContent = prompt.question ?? ""; * (prompt.options ?? []).forEach((option) => { * const btn = document.createElement("button"); * btn.textContent = option.label; * btn.addEventListener("click", () => resolve(option.label)); * root.appendChild(btn); * }); * return root; * } * ``` */ renderAskUserQuestion?: (context: { message: AgentWidgetMessage; /** * Parsed `{ questions: [...] }` payload. May be partial while the tool * call is still streaming; see `complete`. `null` when no payload has * arrived yet. */ payload: Partial | null; /** `true` once the tool-call args have fully streamed in. */ complete: boolean; /** * Resume the paused LOCAL tool with the user's answer. Posts to the * resume endpoint, pipes the SSE stream back into the session, and * appends a user-visible answer bubble to the transcript. */ resolve: (answer: string) => void; /** * Cancel the question. Resumes with the sentinel `"(dismissed)"` so the * server doesn't sit in `waiting_for_local` forever. Idempotent. */ dismiss: () => void; config: AgentWidgetConfig; }) => HTMLElement | null; /** * Custom renderer for approval bubbles. * * Return an `HTMLElement` to fully own the approval UI, `defaultRenderer()` * to render (or wrap) the built-in bubble, or `null` to fall through to the * default. Unlike the built-in bubble: whose Approve/Deny buttons are wired * via delegation: a fully custom element resolves the approval by calling * the `approve`/`deny` callbacks. Both route through the same path the * built-in buttons use (optimistic update, `onDecision`, in-place anchoring). * * An approval is a single binary gate, so there are exactly two outcomes. * Pass `{ remember: true }` to flag a "remember this" affordance (e.g. an * "Always allow" button); the current approval resolves identically, but the * flag is forwarded to `config.approval.onDecision` so you can persist a * don't-ask-again policy for future approvals. * * `renderApproval` is called again whenever the approval's status changes, so * branch on `message.approval?.status` to render the resolved state (and tear * down any global listeners you added while pending). * * @example * ```typescript * // An alternative prompt: "Always allow" / "Allow once" / "Deny". * renderApproval: ({ message, approve, deny }) => { * const approval = message.approval; * if (!approval || approval.status !== "pending") return null; // default renders resolved state * const root = document.createElement("div"); * root.textContent = `${approval.toolName} requires approval`; * * const always = document.createElement("button"); * always.textContent = "Always allow"; * always.addEventListener("click", () => approve({ remember: true })); * * const once = document.createElement("button"); * once.textContent = "Allow once"; * once.addEventListener("click", () => approve()); * * const no = document.createElement("button"); * no.textContent = "Deny"; * no.addEventListener("click", () => deny()); * * root.append(always, once, no); * return root; * } * ``` */ renderApproval?: (context: { message: AgentWidgetMessage; defaultRenderer: () => HTMLElement; config: AgentWidgetConfig; /** Resolve this approval as approved. Pass `{ remember: true }` for an "Always allow" affordance. */ approve: (options?: AgentWidgetApprovalDecisionOptions) => void; /** Resolve this approval as denied. Pass `{ remember: true }` for an "Always deny" affordance. */ deny: (options?: AgentWidgetApprovalDecisionOptions) => void; }) => HTMLElement | null; /** * Custom renderer for loading indicator * Return null to use default renderer (or config-based renderer) * * @example * ```typescript * renderLoadingIndicator: ({ location, defaultRenderer }) => { * if (location === 'standalone') { * const el = document.createElement('div'); * el.textContent = 'Thinking...'; * return el; * } * return defaultRenderer(); * } * ``` */ renderLoadingIndicator?: (context: LoadingIndicatorRenderContext) => HTMLElement | null; /** * Custom renderer for idle state indicator. * Called when the widget is idle (not streaming) and has at least one message. * Return an HTMLElement to display, or null to hide (default). * * @example * ```typescript * renderIdleIndicator: ({ lastMessage, messageCount }) => { * if (messageCount === 0) return null; * if (lastMessage?.role !== 'assistant') return null; * const el = document.createElement('div'); * el.className = 'idle-pulse'; * el.setAttribute('data-preserve-animation', 'true'); * return el; * } * ``` */ renderIdleIndicator?: (context: IdleIndicatorRenderContext) => HTMLElement | null; /** * Custom renderer for the entire event stream view. * Return null to use default renderer. */ renderEventStreamView?: (context: EventStreamViewRenderContext) => HTMLElement | null; /** * Custom renderer for individual event stream rows. * Return null to use default renderer. */ renderEventStreamRow?: (context: EventStreamRowRenderContext) => HTMLElement | null; /** * Custom renderer for the event stream toolbar/header bar. * Return null to use default renderer. */ renderEventStreamToolbar?: (context: EventStreamToolbarRenderContext) => HTMLElement | null; /** * Custom renderer for the expanded event payload display. * Return null to use default renderer. */ renderEventStreamPayload?: (context: EventStreamPayloadRenderContext) => HTMLElement | null; /** * Called when plugin is registered */ onRegister?: () => void; /** * Called when plugin is unregistered */ onUnregister?: () => void; } type TokenType = 'color' | 'spacing' | 'typography' | 'shadow' | 'border' | 'radius'; type TokenReference<_T extends TokenType = TokenType> = string; interface ColorShade { 50?: string; 100?: string; 200?: string; 300?: string; 400?: string; 500?: string; 600?: string; 700?: string; 800?: string; 900?: string; 950?: string; [key: string]: string | undefined; } interface ColorPalette { gray: ColorShade; primary: ColorShade; secondary: ColorShade; accent: ColorShade; success: ColorShade; warning: ColorShade; error: ColorShade; info: ColorShade; [key: string]: ColorShade; } interface SpacingScale { 0: string; 1: string; 2: string; 3: string; 4: string; 5: string; 6: string; 8: string; 10: string; 12: string; 16: string; 20: string; 24: string; 32: string; 40: string; 48: string; 56: string; 64: string; [key: string]: string; } interface ShadowScale { none: string; sm: string; md: string; lg: string; xl: string; '2xl': string; [key: string]: string; } interface BorderScale { none: string; sm: string; md: string; lg: string; [key: string]: string; } interface RadiusScale { none: string; sm: string; md: string; lg: string; xl: string; full: string; [key: string]: string; } interface TypographyScale { fontFamily: { sans: string; serif: string; mono: string; }; fontSize: { xs: string; sm: string; base: string; lg: string; xl: string; '2xl': string; '3xl': string; '4xl': string; }; fontWeight: { normal: string; medium: string; semibold: string; bold: string; }; lineHeight: { tight: string; normal: string; relaxed: string; }; } interface SemanticColors { primary: TokenReference<'color'>; secondary: TokenReference<'color'>; accent: TokenReference<'color'>; surface: TokenReference<'color'>; background: TokenReference<'color'>; container: TokenReference<'color'>; text: TokenReference<'color'>; textMuted: TokenReference<'color'>; textInverse: TokenReference<'color'>; border: TokenReference<'color'>; divider: TokenReference<'color'>; interactive: { default: TokenReference<'color'>; hover: TokenReference<'color'>; focus: TokenReference<'color'>; active: TokenReference<'color'>; disabled: TokenReference<'color'>; }; feedback: { success: TokenReference<'color'>; warning: TokenReference<'color'>; error: TokenReference<'color'>; info: TokenReference<'color'>; }; } interface SemanticSpacing { xs: TokenReference<'spacing'>; sm: TokenReference<'spacing'>; md: TokenReference<'spacing'>; lg: TokenReference<'spacing'>; xl: TokenReference<'spacing'>; '2xl': TokenReference<'spacing'>; } interface SemanticTypography { fontFamily: TokenReference<'typography'>; fontSize: TokenReference<'typography'>; fontWeight: TokenReference<'typography'>; lineHeight: TokenReference<'typography'>; } interface SemanticTokens { colors: SemanticColors; spacing: SemanticSpacing; typography: SemanticTypography; } interface ComponentTokenSet { background?: TokenReference<'color'>; foreground?: TokenReference<'color'>; border?: TokenReference<'color'>; borderRadius?: TokenReference<'radius'>; padding?: TokenReference<'spacing'>; margin?: TokenReference<'spacing'>; shadow?: TokenReference<'shadow'>; opacity?: number; } interface ButtonTokens extends ComponentTokenSet { primary: ComponentTokenSet; secondary: ComponentTokenSet; ghost: ComponentTokenSet; } interface InputTokens extends ComponentTokenSet { background: TokenReference<'color'>; placeholder: TokenReference<'color'>; focus: { border: TokenReference<'color'>; ring: TokenReference<'color'>; }; } interface LauncherTokens extends ComponentTokenSet { size: string; iconSize: string; shadow: TokenReference<'shadow'>; } interface PanelTokens extends ComponentTokenSet { width: string; maxWidth: string; height: string; maxHeight: string; } interface HeaderTokens extends ComponentTokenSet { background: TokenReference<'color'>; border: TokenReference<'color'>; borderRadius: TokenReference<'radius'>; /** Background of the rounded avatar tile next to the title (Lucide / emoji / image). */ iconBackground: TokenReference<'color'>; /** Foreground (glyph stroke or emoji text) on the header avatar tile. */ iconForeground: TokenReference<'color'>; /** Header title line (next to the icon, or minimal layout title). */ titleForeground: TokenReference<'color'>; /** Header subtitle line under the title. */ subtitleForeground: TokenReference<'color'>; /** Default color for clear / close icon buttons when launcher overrides are unset. */ actionIconForeground: TokenReference<'color'>; /** Box-shadow on the header (e.g., a fade shadow to replace the default border). */ shadow?: string; /** Override the header bottom border (e.g., `none`). */ borderBottom?: string; } interface MessageTokens { user: { background: TokenReference<'color'>; text: TokenReference<'color'>; borderRadius: TokenReference<'radius'>; /** User bubble box-shadow (token ref or raw CSS, e.g. `none`). */ shadow?: string; }; assistant: { background: TokenReference<'color'>; text: TokenReference<'color'>; borderRadius: TokenReference<'radius'>; /** Assistant bubble border color (CSS color). */ border?: TokenReference<'color'>; /** Assistant bubble box-shadow (token ref or raw CSS, e.g. `none`). */ shadow?: string; }; /** Border color between messages in the thread. */ border?: TokenReference<'color'>; } /** * Welcome / intro card rendered above the message list when no messages exist. * Set `copy.showWelcomeCard: false` to hide it; use `layout.slots["body-top"]` * to replace it wholesale. */ interface IntroCardTokens extends ComponentTokenSet { background?: TokenReference<'color'>; borderRadius?: TokenReference<'radius'>; padding?: TokenReference<'spacing'>; /** Box-shadow on the intro card (token ref or raw CSS, e.g. `none`). */ shadow?: string; } /** Collapsible widget chrome (tool bubbles, reasoning bubbles, approval bubbles). */ interface CollapsibleWidgetTokens { /** Background for content areas. */ container?: TokenReference<'color'>; /** Background for code blocks inside collapsible sections. */ surface?: TokenReference<'color'>; /** Border color for collapsible sections. */ border?: TokenReference<'color'>; } interface MarkdownTokens { inlineCode: { background: TokenReference<'color'>; foreground: TokenReference<'color'>; }; /** Foreground for `` in rendered markdown (assistant bubbles + artifact pane). */ link?: { foreground: TokenReference<'color'>; }; /** * Body font for rendered markdown blocks (artifact pane + markdown bubbles). * Use a raw CSS `font-family` value, e.g. `Georgia, serif`. */ prose?: { fontFamily?: string; }; /** Optional heading scale overrides (raw CSS or resolvable token paths). */ heading?: { h1?: { fontSize?: string; fontWeight?: string; }; h2?: { fontSize?: string; fontWeight?: string; }; }; /** Fenced code block styling. */ codeBlock?: { background?: TokenReference<'color'>; borderColor?: TokenReference<'color'>; textColor?: TokenReference<'color'>; }; /** Table styling. */ table?: { headerBackground?: TokenReference<'color'>; borderColor?: TokenReference<'color'>; }; /** Horizontal rule styling. */ hr?: { color?: TokenReference<'color'>; }; /** Blockquote styling. */ blockquote?: { borderColor?: TokenReference<'color'>; background?: TokenReference<'color'>; textColor?: TokenReference<'color'>; }; } interface VoiceTokens { recording: { indicator: TokenReference<'color'>; background: TokenReference<'color'>; border: TokenReference<'color'>; }; processing: { icon: TokenReference<'color'>; background: TokenReference<'color'>; }; speaking: { icon: TokenReference<'color'>; }; } interface ApprovalTokens { requested: { background: TokenReference<'color'>; border: TokenReference<'color'>; text: TokenReference<'color'>; /** Box-shadow for the approval bubble (token ref or raw CSS, e.g. `none`). */ shadow: string; }; approve: ComponentTokenSet; deny: ComponentTokenSet; } interface AttachmentTokens { image: { background: TokenReference<'color'>; border: TokenReference<'color'>; }; } /** Tool-call row chrome (collapsible tool bubbles). */ interface ToolBubbleTokens { /** Box-shadow for tool bubbles (token ref or raw CSS, e.g. `none`). */ shadow: string; } /** Reasoning / “thinking” row chrome. */ interface ReasoningBubbleTokens { shadow: string; } /** Composer (message input) chrome. */ interface ComposerChromeTokens { /** Box-shadow on the composer form (raw CSS, e.g. `none`). */ shadow: string; } /** Artifact toolbar chrome. */ interface ArtifactToolbarTokens { iconHoverColor?: string; iconHoverBackground?: string; iconPadding?: string; iconBorderRadius?: string; iconBorder?: string; toggleGroupGap?: string; toggleBorderRadius?: string; copyBackground?: string; copyBorder?: string; copyColor?: string; copyBorderRadius?: string; copyPadding?: string; copyMenuBackground?: string; copyMenuBorder?: string; copyMenuShadow?: string; copyMenuBorderRadius?: string; copyMenuItemHoverBackground?: string; /** Base background of icon buttons (defaults to --persona-surface). */ iconBackground?: string; /** Border on the toolbar (e.g., `none` to remove the bottom border). */ toolbarBorder?: string; } /** Artifact tab strip chrome. */ interface ArtifactTabTokens { background?: string; activeBackground?: string; activeBorder?: string; borderRadius?: string; textColor?: string; /** Hover background for inactive tabs. */ hoverBackground?: string; /** Tab list container background. */ listBackground?: string; /** Tab list container border color. */ listBorderColor?: string; /** Tab list container padding (CSS shorthand). */ listPadding?: string; } /** Artifact pane chrome. */ interface ArtifactPaneTokens { /** * Background for the artifact column (toolbar + content), resolved from the theme. * Defaults to `semantic.colors.container` so the pane matches assistant message surfaces. * `features.artifacts.layout.paneBackground` still wins when set (layout escape hatch). */ background?: string; toolbarBackground?: string; } /** Icon button chrome (used by createIconButton). */ interface IconButtonTokens { background?: string; border?: string; color?: string; padding?: string; borderRadius?: string; hoverBackground?: string; hoverColor?: string; /** Background when aria-pressed="true". */ activeBackground?: string; /** Border color when aria-pressed="true". */ activeBorder?: string; } /** Label button chrome (used by createLabelButton). */ interface LabelButtonTokens { background?: string; border?: string; color?: string; padding?: string; borderRadius?: string; hoverBackground?: string; fontSize?: string; gap?: string; } /** Scroll-to-bottom pill chrome shared by transcript + event stream. */ interface ScrollToBottomTokens extends ComponentTokenSet { size?: string; gap?: string; fontSize?: string; iconSize?: string; } /** Toggle group chrome (used by createToggleGroup). */ interface ToggleGroupTokens { /** Gap between toggle buttons. Default: 0 (connected). */ gap?: string; /** Border radius for first/last buttons. */ borderRadius?: string; } interface ComponentTokens { button: ButtonTokens; input: InputTokens; launcher: LauncherTokens; panel: PanelTokens; header: HeaderTokens; message: MessageTokens; /** Welcome / intro card shown above the message list. */ introCard?: IntroCardTokens; /** Markdown surfaces (chat + artifact pane). */ markdown?: MarkdownTokens; voice: VoiceTokens; approval: ApprovalTokens; attachment: AttachmentTokens; toolBubble: ToolBubbleTokens; reasoningBubble: ReasoningBubbleTokens; composer: ComposerChromeTokens; /** Icon button styling tokens. */ iconButton?: IconButtonTokens; /** Label button styling tokens. */ labelButton?: LabelButtonTokens; /** Scroll-to-bottom indicator styling tokens. */ scrollToBottom?: ScrollToBottomTokens; /** Toggle group styling tokens. */ toggleGroup?: ToggleGroupTokens; /** Artifact toolbar, tab strip, and pane chrome. */ artifact?: { toolbar?: ArtifactToolbarTokens; tab?: ArtifactTabTokens; pane?: ArtifactPaneTokens; }; /** Collapsible widget chrome (tool/reasoning/approval bubbles). */ collapsibleWidget?: CollapsibleWidgetTokens; } interface PaletteExtras { transitions?: Record; easings?: Record; } interface PersonaThemeBase { palette: { colors: ColorPalette; spacing: SpacingScale; typography: TypographyScale; shadows: ShadowScale; borders: BorderScale; radius: RadiusScale; } & PaletteExtras; } interface PersonaThemeSemantic { semantic: SemanticTokens; } interface PersonaThemeComponents { components: ComponentTokens; } type PersonaTheme = PersonaThemeBase & PersonaThemeSemantic & PersonaThemeComponents; /** Recursive partial for `config.theme` / `config.darkTheme` overrides. */ type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial; } : T; interface ResolvedToken { path: string; value: string; type: TokenType; } interface ThemeValidationError { path: string; message: string; severity: 'error' | 'warning'; } interface ThemeValidationResult { valid: boolean; errors: ThemeValidationError[]; warnings: ThemeValidationError[]; } interface PersonaThemePlugin { name: string; version: string; transform(theme: PersonaTheme): PersonaTheme; cssVariables?: Record; afterResolve?(resolved: Record): Record; } interface CreateThemeOptions { plugins?: PersonaThemePlugin[]; validate?: boolean; extend?: PersonaTheme; } type RuntypeExecutionStreamEvent = ({ agentId?: string; agentName?: string; config?: Record; executionId: string; flowId?: string; flowName?: string; kind: "agent" | "flow"; maxTurns?: number; seq: number; source?: string; startedAt: string; totalSteps?: number; type: "execution_start"; }) | ({ completedAt?: string; durationMs?: number; executionId: string; failedSteps?: number; finalOutput?: string; iterations?: number; kind: "agent" | "flow"; seq: number; stopReason?: string; success: boolean; successfulSteps?: number; totalCost?: number; totalSteps?: number; totalTokens?: { input: number; output: number; }; type: "execution_complete"; }) | ({ code?: string; completedAt?: string; error: string | { code: string; details?: Record; message: string; }; executionId: string; kind: "agent" | "flow"; seq: number; type: "execution_error"; upgradeUrl?: string; }) | ({ executionId: string; id: string; iteration?: number; role: "user" | "assistant" | "system"; seq: number; turnIndex?: number; type: "turn_start"; }) | ({ completedAt?: string; content?: string; cost?: number; executionId: string; fallback?: { attempts: Array<{ attempt: number; error?: string; model?: string; success: boolean; type: "retry" | "model" | "message" | "flow"; }>; exhausted: boolean; model?: string; reason?: string | null; used: boolean; }; id: string; iteration?: number; role: "user" | "assistant" | "system"; seq: number; stopReason?: "end_turn" | "max_tool_calls" | "length" | "content_filter" | "error" | "unknown"; tokens?: { input: number; output: number; }; type: "turn_complete"; }) | { executionId: string; id: string; index?: number; name?: string; outputVariable?: string; seq: number; startedAt?: string; stepType?: string; totalSteps?: number; type: "step_start"; } | ({ completedAt?: string; durationMs?: number; error?: string; executionId: string; fallback?: { attempts: Array<{ attempt: number; error?: string; model?: string; success: boolean; type: "retry" | "model" | "message" | "flow"; }>; exhausted: boolean; model?: string; reason?: string | null; used: boolean; }; id: string; name?: string; result?: unknown; seq: number; stepType?: string; stopReason?: "end_turn" | "max_tool_calls" | "length" | "content_filter" | "error" | "unknown"; success?: boolean; tokensUsed?: number; type: "step_complete"; unresolvedVariables?: Array; }) | { executionId: string; id: string; index?: number; name?: string; seq: number; skippedAt?: string; stepType?: string; totalSteps?: number; type: "step_skip"; when?: string; } | ({ executionId: string; id: string; parentToolCallId?: string; role?: "user" | "assistant" | "system"; seq: number; stepId?: string; turnId?: string; type: "text_start"; }) | { delta: string; executionId: string; id: string; seq: number; type: "text_delta"; } | { executionId: string; id: string; seq: number; text?: string; type: "text_complete"; } | ({ executionId: string; id: string; parentToolCallId?: string; scope?: "turn" | "loop"; seq: number; type: "reasoning_start"; }) | { delta: string; executionId: string; id: string; seq: number; type: "reasoning_delta"; } | ({ executionId: string; id: string; scope?: "turn" | "loop"; seq: number; text?: string; type: "reasoning_complete"; }) | ({ executionId: string; id: string; mediaType: string; role?: "user" | "assistant" | "system"; seq: number; toolCallId?: string; type: "media_start"; }) | { delta: string; executionId: string; id: string; seq: number; type: "media_delta"; } | { data?: string; executionId: string; id: string; mediaType?: string; seq: number; toolCallId?: string; type: "media_complete"; url?: string; } | ({ artifactType: "markdown" | "component"; component?: string; executionId?: string; id: string; seq?: number; title?: string; type: "artifact_start"; }) | { delta: string; executionId?: string; id: string; seq?: number; type: "artifact_delta"; } | { component: string; executionId?: string; id: string; props: Record; seq?: number; type: "artifact_update"; } | { executionId?: string; id: string; seq?: number; type: "artifact_complete"; } | { executionId: string; id?: string; seq: number; sourceType?: string; title?: string; type: "source"; url?: string; [key: string]: unknown; } | ({ executionId: string; hiddenParameterNames?: Array; iteration?: number; origin?: "webmcp" | "sdk"; pageOrigin?: string; parameters?: Record; seq: number; startedAt?: string; stepId?: string; toolCallId: string; toolName: string; toolType: string; type: "tool_start"; }) | { delta: string; executionId: string; seq: number; toolCallId: string; type: "tool_input_delta"; } | { executionId: string; hiddenParameterNames?: Array; parameters: Record; seq: number; toolCallId: string; toolName?: string; type: "tool_input_complete"; } | { delta: string; executionId: string; seq: number; toolCallId: string; type: "tool_output_delta"; } | { error?: string; executionId: string; executionTime?: number; iteration?: number; result?: unknown; seq: number; stepId?: string; success: boolean; toolCallId: string; toolName?: string; type: "tool_complete"; } | { approvalId: string; description?: string; executionId: string; iteration?: number; parameters?: Record; reason?: string; seq: number; startedAt?: string; subagent?: { agentName?: string; toolName: string; }; timeout?: number; toolCallId?: string; toolName: string; toolType?: string; type: "approval_start"; } | ({ approvalId: string; completedAt?: string; decision: "approved" | "denied" | "timeout"; executionId: string; resolvedBy?: "user" | "system"; seq: number; type: "approval_complete"; }) | ({ awaitedAt?: string; executionId: string; origin?: "webmcp" | "sdk"; pageOrigin?: string; parameters?: Record; seq: number; toolCallId?: string; toolId?: string; toolName?: string; type: "await"; }) | ({ error: string | { code: string; details?: Record; message: string; }; executionId: string; recoverable?: boolean; seq: number; type: "error"; }) | { executionId: string; seq: number; timestamp: string; type: "ping"; } | { executionId?: string; name: string; seq?: number; type: "custom"; value?: unknown; }; type RuntypeFlowSSEEvent = { executionContext?: Record; executionId?: string; flowId: string; flowName?: string; input?: unknown; seq?: number; source?: string; startedAt: string; toolContext?: { executionId: string; stepId: string; toolId: string; }; totalSteps?: number; type: "flow_start"; } | { claudeManagedAgentId?: string; completedAt?: string; completedSteps?: number; duration?: number; executionContext?: Record; executionId?: string; executionTime?: number; failedSteps?: number; finalOutput?: string; flowId?: string; flowName?: string; output?: unknown; seq?: number; source?: string; success?: boolean; successfulSteps?: number; toolContext?: { executionId: string; stepId: string; toolId: string; }; totalSteps?: number; totalTokensUsed?: number; type: "flow_complete"; } | ({ code?: string; error: string | { code: string; message: string; stepId?: string; stepType?: string; }; executionId?: string; executionTime?: number; flowId?: string; seq?: number; timestamp?: string; toolContext?: { executionId: string; stepId: string; toolId: string; }; type: "flow_error"; upgradeUrl?: string; }) | ({ awaitedAt: string; executionId?: string; flowId: string; origin?: "webmcp" | "sdk"; pageOrigin?: string; parameters?: Record; seq?: number; toolCallId?: string; toolId?: string; toolName?: string; type: "flow_await"; }) | { estimatedTokens?: number; executionId?: string; id?: string; index?: number; name?: string; outputVariable?: string; seq?: number; startedAt: string; stepId?: string; stepName?: string; stepType?: string; toolContext?: { executionId: string; stepId: string; toolId: string; }; totalSteps?: number; type: "step_start"; } | { delta?: string; executionId?: string; id?: string; messageId?: string; partId?: string; seq?: number; text?: string; toolContext?: { executionId: string; stepId: string; toolId: string; }; toolId?: string; type: "step_delta"; } | ({ completedAt?: string; duration?: number; durationMs?: number; error?: string; executionId?: string; executionTime?: number; id?: string; index?: number; name?: string; output?: unknown; result?: unknown; seq?: number; stepId?: string; stepName?: string; stepType?: string; stopReason?: "end_turn" | "max_tool_calls" | "length" | "content_filter" | "error" | "unknown"; success?: boolean; tokensUsed?: number; toolContext?: { executionId: string; stepId: string; toolId: string; }; type: "step_complete"; unresolvedVariables?: Array; }) | { error: string; executionId?: string; executionTime?: number; id?: string; index?: number; name?: string; seq?: number; stepType?: string; type: "step_error"; } | { error?: string; executionId?: string; id: string; index?: number; name?: string; seq?: number; skippedAt: string; stepType: string; totalSteps: number; type: "step_skip"; when: string; } | { executionId?: string; reason?: string; seq?: number; type: "step_await"; [key: string]: unknown; } | ({ agentContext?: { executionId: string; iteration: number; seq: number; }; executionId?: string; hiddenParameterNames?: Array; name?: string; parameters?: Record; providerOptions?: Record; seq?: number; startedAt?: string; stepId?: string; toolCallId?: string; toolId?: string; toolName?: string; toolType: "flow" | "mcp" | "builtin" | "custom" | "external" | "advisor" | "subagent" | "local"; type: "tool_start"; [key: string]: unknown; }) | { delta?: string; executionId?: string; seq?: number; toolId?: string; type: "tool_delta"; [key: string]: unknown; } | { delta: string; executionId?: string; seq?: number; stepId?: string; toolCallId?: string; toolId?: string; type: "tool_input_delta"; } | { executionId?: string; hiddenParameterNames?: Array; parameters: Record; providerOptions?: Record; seq?: number; stepId?: string; toolCallId?: string; toolId?: string; toolName?: string; type: "tool_input_complete"; } | { agentContext?: { executionId: string; iteration: number; seq: number; }; completedAt?: string; error?: string; executionId?: string; executionTime?: number; name?: string; result?: unknown; seq?: number; stepId?: string; success: boolean; toolCallId?: string; toolCost?: number; toolId?: string; toolName?: string; type: "tool_complete"; } | { agentContext?: { executionId: string; iteration: number; seq: number; }; error: string; executionId?: string; executionTime?: number; failedAt?: string; name: string; seq?: number; toolId: string; type: "tool_error"; } | { executionId?: string; id: string; seq?: number; text: string; type: "chunk"; } | { executionId?: string; seq?: number; type: "text_start"; [key: string]: unknown; } | { executionId?: string; seq?: number; type: "text_end"; [key: string]: unknown; } | { executionId?: string; seq?: number; type: "reason_start"; [key: string]: unknown; } | { executionId?: string; seq?: number; type: "reason_delta"; [key: string]: unknown; } | { executionId?: string; seq?: number; type: "reason_complete"; [key: string]: unknown; } | { executionId?: string; seq?: number; type: "source"; [key: string]: unknown; } | { executionId?: string; seq?: number; type: "fallback_start"; [key: string]: unknown; } | { executionId?: string; seq?: number; type: "fallback_complete"; [key: string]: unknown; } | { executionId?: string; seq?: number; type: "fallback_exhausted"; [key: string]: unknown; }; type RuntypeStreamEventOf = Extract; type RuntypeTurnCompleteEvent = RuntypeStreamEventOf; type RuntypeStepCompleteEvent = RuntypeStreamEventOf; type RuntypeStopReasonKind = NonNullable; type RuntypeClientInitRequest = { flowId?: string; sessionId?: string; token: string; }; type RuntypeClientInitResponse = { config: { placeholder?: string; theme?: unknown; welcomeMessage?: string | null; }; expiresAt: string; flow: { description?: string | null; id: string; name?: string | null; }; sessionId: string; }; type RuntypeClientChatRequest = { assistantMessageId?: string; clientTools?: Array<{ description: string; name: string; origin?: "webmcp" | "sdk"; pageOrigin?: string; parametersSchema: { type: "object"; [key: string]: unknown; }; untrustedContentHint?: boolean; }>; clientToolsFingerprint?: string; inputs?: Record; messages: Array<{ content: string | (Array<{ text: string; type: "text"; } | { image: string; mimeType?: string; type: "image"; } | { data: string; filename: string; mimeType: string; type: "file"; } | { providerOptions?: Record; text: string; type: "reasoning"; }>); id?: string; role: "user" | "assistant" | "system"; }>; metadata?: Record; sessionId: string; submitMode?: "normal" | "interrupt"; turnId?: string; }; type RuntypeClientChatStreamEvent = RuntypeExecutionStreamEvent; type RuntypeClientResumeRequest = { executionId: string; messages?: Array<{ content: string | (Array<{ text: string; type: "text"; } | { image: string; mimeType?: string; type: "image"; } | { data: string; filename: string; mimeType: string; type: "file"; } | { providerOptions?: Record; text: string; type: "reasoning"; }>); role: "system" | "user" | "assistant"; }>; sessionId: string; streamResponse?: boolean; toolOutputs?: Record; }; type RuntypeClientResumeStreamEvent = RuntypeExecutionStreamEvent; type RuntypeClientFeedbackRequest = { comment?: string; messageId?: string; metadata?: Record; rating?: number; sessionId: string; token: string; type: "upvote" | "downvote" | "copy" | "csat" | "nps"; }; type RuntypeClientFeedbackResponse = { feedbackId: string; message?: string; success: true; }; type RuntypeClientFeedbackType = RuntypeClientFeedbackRequest["type"]; /** * Target resolution for the normalized, backend-neutral `target` field. * * `target` is a single string that selects which backend resource a widget * talks to, optimized for a browser widget (always serializable, no live * objects). Three shapes are supported: * * - Runtype TypeID (no prefix): `"agent_…"` / `"flow_…"` route to the * Runtype agent/flow paths. The TypeID prefix is self-describing, so no * wrapper is needed for the common case. * - Provider-prefixed: `":"` is handed to the matching * `targetProviders[provider]` resolver, which returns the dispatch payload * fragment for that backend (e.g. `eve`, `langgraph`). `"runtype:…"` is a * built-in that re-detects a TypeID. * - Bare name: `"support"` requires a `targetProviders.default` resolver, * otherwise it throws (a bare name is ambiguous without one). * * Resolvers are registered, not passed as the value, which keeps `target` * itself a plain string that survives script-tag installs, `data-config`, * persisted state, and codegen. */ /** Resolver for a provider-prefixed (or default) target id. */ type TargetResolver = (id: string) => { payload: Record; }; /** Normalized routing produced from a `target` string. */ type ResolvedTarget = { kind: "agentId"; agentId: string; } | { kind: "flowId"; flowId: string; } | { kind: "payload"; payload: Record; }; /** * Text content part for multi-modal messages */ type TextContentPart = { type: 'text'; text: string; }; /** * Image content part for multi-modal messages * Supports base64 data URIs or URLs */ type ImageContentPart = { type: 'image'; image: string; mimeType?: string; alt?: string; }; /** * File content part for multi-modal messages * Supports PDF, TXT, DOCX, and other document types */ type FileContentPart = { type: 'file'; data: string; mimeType: string; filename: string; }; /** * Audio content part for multi-modal messages * Supports base64 data URIs or URLs */ type AudioContentPart = { type: 'audio'; audio: string; mimeType?: string; }; /** * Video content part for multi-modal messages * Supports base64 data URIs or URLs */ type VideoContentPart = { type: 'video'; video: string; mimeType?: string; }; /** * Union type for all content part types */ type ContentPart = TextContentPart | ImageContentPart | FileContentPart | AudioContentPart | VideoContentPart; /** * Message content can be a simple string or an array of content parts */ type MessageContent = string | ContentPart[]; type AgentWidgetContextProviderContext = { messages: AgentWidgetMessage[]; config: AgentWidgetConfig; }; type AgentWidgetContextProvider = (context: AgentWidgetContextProviderContext) => Record | void | Promise | void>; type AgentWidgetRequestPayloadMessage = { role: AgentWidgetMessageRole; content: MessageContent; createdAt: string; }; type AgentWidgetRequestPayload = { messages: AgentWidgetRequestPayloadMessage[]; flowId?: string; agent?: { agentId: string; }; context?: Record; metadata?: Record; /** Per-turn template variables for /v1/client/chat (merged as root-level {{var}} in Runtype). */ inputs?: Record; /** * Per-turn page-discovered tools (WebMCP). Sent to Runtype's dispatch so the * agent can call them as `webmcp:`. The widget snapshots * `document.modelContext.__getRegisteredTools()` each turn and ships only * the JSON-serializable surface (no `execute`). */ clientTools?: ClientToolDefinition[]; }; /** * Configuration for agent loop behavior. */ type AgentLoopConfig = { /** Maximum number of agent turns (1-100). The loop continues while the model calls tools. */ maxTurns: number; /** Maximum cost budget in USD. Agent stops when exceeded. */ maxCost?: number; /** Enable periodic reflection during execution */ enableReflection?: boolean; /** Number of iterations between reflections (1-50) */ reflectionInterval?: number; }; /** * Configuration for agent tools (search, code execution, MCP servers, etc.) */ type AgentToolsConfig = { /** Tool IDs to enable (e.g., "builtin:exa", "builtin:dalle", "builtin:openai_web_search") */ toolIds?: string[]; /** Per-tool configuration overrides keyed by tool ID */ toolConfigs?: Record>; /** Inline tool definitions for runtime-defined tools */ runtimeTools?: Array>; /** Custom MCP server connections */ mcpServers?: Array>; /** Maximum number of tool invocations per execution */ maxToolCalls?: number; /** How the model is steered toward tools: let it decide, force a call, or disable */ toolCallStrategy?: "auto" | "required" | "none"; /** Per-tool invocation limits / requirements keyed by tool name */ perToolLimits?: Record; /** Tool approval configuration for human-in-the-loop workflows */ approval?: { /** Tool names/patterns to require approval for, or true for all tools */ require: string[] | boolean; /** Approval timeout in milliseconds (default: 300000 / 5 minutes) */ timeout?: number; /** Ask the agent to state its intent alongside approval requests (default: true) */ requestReason?: boolean; }; /** * Enables the synthesized `spawn_subagent` tool: the model can spin up * ad-hoc child agents at runtime, restricted to `toolPool` (tool IDs / * runtime-tool names already granted to the parent agent). */ subagentConfig?: { toolPool: string[]; defaultMaxTurns?: number; maxTurnsLimit?: number; maxSpawnsPerRun?: number; defaultModel?: string; allowNesting?: boolean; defaultTimeoutMs?: number; }; /** * Enables the synthesized `code_mode` tool: the model writes JS that calls * pool tools inside a sandbox instead of issuing individual tool calls. */ codeModeConfig?: { toolPool: string[]; description?: string; timeoutMs?: number; }; }; /** Artifact kinds for the Persona sidebar and dispatch payload */ type PersonaArtifactKind = "markdown" | "component"; /** * Agent configuration for agent execution mode. * When provided in the widget config, enables agent loop execution instead of flow dispatch. */ type ArtifactConfigPayload = { enabled: true; types: PersonaArtifactKind[]; }; type AgentConfig = { /** Agent display name */ name: string; /** Model identifier (e.g., 'openai:gpt-4o-mini', 'qwen/qwen3-8b') */ model: string; /** System prompt for the agent */ systemPrompt: string; /** Temperature for model responses */ temperature?: number; /** Tool configuration for the agent */ tools?: AgentToolsConfig; /** Persona artifacts: sibling of tools (virtual agent / API parity) */ artifacts?: ArtifactConfigPayload; /** Loop configuration for multi-turn execution */ loopConfig?: AgentLoopConfig; }; type SavedAgentConfig = { /** Persisted Runtype agent ID to execute. */ agentId: string; }; /** * Options for agent execution requests. */ type AgentRequestOptions = { /** Whether to stream the response (should be true for widget usage) */ streamResponse?: boolean; /** Record mode: 'virtual' for no persistence, 'existing'/'create' for database records */ recordMode?: 'virtual' | 'existing' | 'create'; /** Whether to store results server-side */ storeResults?: boolean; /** Enable debug mode for additional event data */ debugMode?: boolean; }; /** * Request payload for agent execution mode. */ type AgentWidgetAgentRequestPayload = { agent: AgentConfig | SavedAgentConfig; messages: AgentWidgetRequestPayloadMessage[]; options: AgentRequestOptions; context?: Record; metadata?: Record; /** * Per-turn page-discovered tools (WebMCP): same shape as * `AgentWidgetRequestPayload.clientTools`. */ clientTools?: ClientToolDefinition[]; }; /** * Wire shape for a single client-discovered tool sent on `dispatch.clientTools[]`. * * Mirrors the SDK's `ClientToolDefinition` in `@runtypelabs/sdk`. Only the * JSON-serializable surface of a WebMCP tool: the `execute` function stays * client-side; the server merges these into the agent's tool catalog under * the `webmcp:` namespace. */ type ClientToolDefinition = { /** Bare tool name; the server prepends `webmcp:` on the wire. */ name: string; description: string; /** JSON Schema (per WebMCP spec): passed through as-is. */ parametersSchema?: object; /** * `'webmcp'` for tools discovered via the polyfill (server prepends the * `webmcp:` wire prefix); `'sdk'` for widget/SDK-provided tools (name stays * bare on the wire). Matches the server's accepted enum: any other value * fails dispatch validation. */ origin?: 'webmcp' | 'sdk'; /** Origin of the page that registered the tool: for server-side audit. */ pageOrigin?: string; /** * WebMCP `Tool.annotations` (spec). Not used for gating server-side; the * widget reads these client-side. Forwarded so traces/dashboards can show * `readOnlyHint` / `untrustedContentHint` on tool-call records. */ annotations?: { readOnlyHint?: boolean; untrustedContentHint?: boolean; }; }; /** * Information passed to the confirm-bubble handler before a `webmcp:*` tool * call executes. Every WebMCP tool routes through this single gate. */ type WebMcpConfirmInfo = { /** Bare tool name (no `webmcp:` prefix). */ toolName: string; args: unknown; description?: string; /** * Display title the tool declared via the WebMCP spec's * `ToolDescriptor.title` (e.g. `"Add to Cart"`). Absent when the tool * didn't declare one. */ title?: string; annotations?: { readOnlyHint?: boolean; untrustedContentHint?: boolean; }; /** * Why the confirm was requested. Currently always `'gate'`: the default * confirm-by-default gate that fires before every `webmcp:*` call. (The * `@mcp-b/webmcp-polyfill` owns the spec's `requestUserInteraction` callback * internally, so Persona no longer surfaces a nested in-tool confirm.) */ reason: 'gate'; }; /** * Resolves to `true` if the user approves the tool call; `false` to decline. */ type WebMcpConfirmHandler = (info: WebMcpConfirmInfo) => Promise; /** * Persona's normalized tool-result shape sent back to the agent on `/resume`. * Mirrors the MCP `CallToolResult` content shape; arbitrary `execute()` return * values are wrapped as a single text block at the bridge boundary. */ type WebMcpToolResult = { content: Array<{ type: 'text'; text: string; } | { type: string; [key: string]: unknown; }>; isError?: boolean; /** Pass-through of the tool's `annotations.untrustedContentHint`. */ annotations?: { untrustedContentHint?: boolean; }; }; /** * Widget-level WebMCP configuration. Set `enabled: true` to opt in. The * surface's server-side `webmcp` policy is the source of truth for which * tools are accepted: these client-side options are convenience filters. */ type AgentWidgetWebMcpConfig = { /** Master switch. Default: `false` (widget never installs the polyfill). */ enabled?: boolean; /** * Glob-ish name patterns to include client-side. `'*'` matches any chars * except `:`. Patterns are matched against the bare tool name (no `webmcp:` * prefix). If unset, all registered tools are included. */ allowlist?: string[]; /** * Per-tool gate policy. Called before the confirm gate for every * `webmcp:*` call; return `true` to approve immediately and skip the * confirmation UI entirely. Use this to auto-allow read-only tools (e.g. * a catalog search) while still gating mutating ones. Only consulted on * the default-UI path: a custom `onConfirm` takes full control instead. */ autoApprove?: (info: WebMcpConfirmInfo) => boolean; /** * Confirm gate handler. When omitted, Persona renders its native in-panel * approval bubble (the same chrome used for server-driven tool approvals) * and resolves on the user's Approve/Deny click. Supply this to override * with a custom confirmer (e.g. a route-level modal). The legacy * `window.confirm` fallback only applies when no widget UI is attached. */ onConfirm?: WebMcpConfirmHandler; }; /** * Agent execution state tracking. */ type AgentExecutionState = { executionId: string; agentId: string; agentName: string; status: 'running' | 'complete' | 'error'; currentIteration: number; maxTurns: number; startedAt?: number; completedAt?: number; stopReason?: 'complete' | 'end_turn' | 'max_turns' | 'max_cost' | 'timeout' | 'error'; }; /** * Metadata attached to messages created during agent execution. */ type AgentMessageMetadata = { executionId?: string; iteration?: number; turnId?: string; agentName?: string; /** * When this message was produced by a step inside a nested flow executed * as a tool, identifies the parent tool call id. Enables renderers to * visually group or indent nested-flow output under its parent tool. */ parentToolId?: string; /** * Nested flow step id that produced this message (e.g. a `send-stream` * or `prompt` step inside the nested flow). Stable key for that step. */ parentStepId?: string; /** * Set to `true` on a tool-variant message produced from a `step_await` * event (`awaitReason: "local_tool_required"`). Signals to UI code that * the tool call is a LOCAL tool and the server is paused waiting for a * `POST /v1/dispatch/resume` with the user's answer keyed by tool name. */ awaitingLocalTool?: boolean; /** * The provider per-call id (`toolu_…`) carried on the `step_await` / * `flow_await` events for a LOCAL tool (core#3878). Present only when the * server emits it. Two PARALLEL calls to the same tool in one turn share a * `toolName` (and a collapsed `toolId`) but get DISTINCT `webMcpToolCallId`s, * so this is the key the widget batches a single `/resume` on: preferred * over tool name, which collides for same-tool parallel calls. Absent → * fall back to the legacy name-keyed resume contract. */ webMcpToolCallId?: string; /** * Set to `true` once the user has picked / typed / dismissed an answer for * an `ask_user_question` tool call, so renderers stop re-mounting the * answer-pill sheet for this tool call on subsequent render passes. */ askUserQuestionAnswered?: boolean; /** * In-progress answers for a multi-question `ask_user_question` payload, * keyed by question text. Persisted across refresh so the user lands back * where they were if the page reloads mid-flow. Cleared once * `askUserQuestionAnswered` flips to `true`. */ askUserQuestionAnswers?: Record; /** * Current page index for a multi-question `ask_user_question` payload's * paginated stepper. Persists alongside `askUserQuestionAnswers`. */ askUserQuestionIndex?: number; /** * Set to `true` once a `suggest_replies` tool call's fire-and-forget * `/resume` has been accepted by the server. Persisted belt-and-suspenders * mirror of the in-memory resolved-key dedupe, so hydration/re-emit paths * never re-resume the call. */ suggestRepliesResolved?: boolean; }; type AgentWidgetRequestMiddlewareContext = { payload: AgentWidgetRequestPayload; config: AgentWidgetConfig; }; type AgentWidgetRequestMiddleware = (context: AgentWidgetRequestMiddlewareContext) => AgentWidgetRequestPayload | void | Promise; type AgentWidgetParsedAction = { type: string; payload: Record; raw?: unknown; }; type AgentWidgetActionParserInput = { text: string; message: AgentWidgetMessage; }; type AgentWidgetActionParser = (input: AgentWidgetActionParserInput) => AgentWidgetParsedAction | null | undefined; type AgentWidgetActionHandlerResult = { handled?: boolean; displayText?: string; persistMessage?: boolean; resubmit?: boolean; }; type AgentWidgetActionContext = { message: AgentWidgetMessage; metadata: Record; updateMetadata: (updater: (prev: Record) => Record) => void; document: Document | null; /** * Trigger automatic model continuation. * Call this AFTER completing async operations (e.g., injecting search results) * to have the model analyze the injected data. * * Use this instead of returning `resubmit: true` for handlers that do async work, * as it ensures the continuation happens after the data is available in context. * * @example * // In an action handler * const results = await fetchProducts(query); * session.injectAssistantMessage({ content: formatResults(results) }); * context.triggerResubmit(); */ triggerResubmit: () => void; }; type AgentWidgetActionHandler = (action: AgentWidgetParsedAction, context: AgentWidgetActionContext) => AgentWidgetActionHandlerResult | void; type AgentWidgetStoredState = { messages?: AgentWidgetMessage[]; metadata?: Record; artifacts?: PersonaArtifactRecord[]; selectedArtifactId?: string | null; }; interface AgentWidgetStorageAdapter { load?: () => AgentWidgetStoredState | null | Promise; save?: (state: AgentWidgetStoredState) => void | Promise; clear?: () => void | Promise; } type AgentWidgetVoiceStateEvent = { active: boolean; source: "user" | "auto" | "restore" | "system"; timestamp: number; }; /** * Fired on every voice `VoiceStatus` transition (listening / processing / * speaking / idle / …). Unlike `voice:state` (a coarse active on/off), this * exposes the granular status so consumers can render their own per-state UI * (e.g. a listening/speaking status dock). */ type AgentWidgetVoiceStatusEvent = { status: VoiceStatus; timestamp: number; }; type AgentWidgetActionEventPayload = { action: AgentWidgetParsedAction; message: AgentWidgetMessage; }; /** * Fired on every "Read aloud" (text-to-speech) state transition for a message: * `loading` (press) → `playing` → `paused`/`playing` → `idle` (finished or * stopped). On the terminal `idle` transition, `messageId`/`message` still * identify the message that just stopped. */ type AgentWidgetReadAloudEvent = { /** The message being read aloud (or that just finished/stopped), if known. */ messageId: string | null; /** The message object, when still present in the thread. */ message: AgentWidgetMessage | null; /** The new playback state for this message. */ state: ReadAloudState; timestamp: number; }; /** * Feedback event payload for upvote/downvote actions on messages */ type AgentWidgetMessageFeedback = { type: "upvote" | "downvote"; messageId: string; message: AgentWidgetMessage; }; /** * Configuration for message action buttons (copy, upvote, downvote) * * **Client Token Mode**: When using `clientToken`, feedback is automatically * sent to your Runtype backend. Just enable the buttons and you're done! * The `onFeedback` and `onCopy` callbacks are optional for additional local handling. * * @example * ```typescript * // With clientToken - feedback is automatic! * config: { * clientToken: 'ct_live_...', * messageActions: { * showUpvote: true, * showDownvote: true, * // No onFeedback needed - sent to backend automatically * } * } * ``` */ type AgentWidgetMessageActionsConfig = { /** * Enable/disable message actions entirely * @default true */ enabled?: boolean; /** * Show copy button * @default true */ showCopy?: boolean; /** * Show upvote button. * When using `clientToken`, feedback is sent to the backend automatically. * @default false */ showUpvote?: boolean; /** * Show downvote button. * When using `clientToken`, feedback is sent to the backend automatically. * @default false */ showDownvote?: boolean; /** * Show a "Read aloud" button that speaks the assistant message via * text-to-speech (play / pause / resume). Uses the browser Web Speech API by * default, or a hosted engine supplied via `textToSpeech.createEngine`. * Voice, rate and pitch are read from the `textToSpeech` config. * @default false */ showReadAloud?: boolean; /** * Visibility mode: 'always' shows buttons always, 'hover' shows on hover only * @default 'hover' */ visibility?: "always" | "hover"; /** * Horizontal alignment of action buttons * @default 'right' */ align?: "left" | "center" | "right"; /** * Layout style for action buttons * - 'pill-inside': Compact floating pill around just the buttons (default for hover) * - 'row-inside': Full-width row at the bottom of the message * @default 'pill-inside' */ layout?: "pill-inside" | "row-inside"; /** * Callback when user submits feedback (upvote/downvote). * * **Note**: When using `clientToken`, feedback is AUTOMATICALLY sent to your * backend via `/v1/client/feedback`. This callback is called IN ADDITION to * the automatic submission, useful for updating local UI or analytics. */ onFeedback?: (feedback: AgentWidgetMessageFeedback) => void; /** * Callback when user copies a message. * * **Note**: When using `clientToken`, copy events are AUTOMATICALLY tracked * via `/v1/client/feedback`. This callback is called IN ADDITION to the * automatic tracking. */ onCopy?: (message: AgentWidgetMessage) => void; }; type AgentWidgetStateEvent = { open: boolean; source: "user" | "auto" | "api" | "system"; timestamp: number; }; type AgentWidgetStateSnapshot = { open: boolean; launcherEnabled: boolean; voiceActive: boolean; streaming: boolean; }; type AgentWidgetControllerEventMap = { "user:message": AgentWidgetMessage; "assistant:message": AgentWidgetMessage; "assistant:complete": AgentWidgetMessage; "voice:state": AgentWidgetVoiceStateEvent; "voice:status": AgentWidgetVoiceStatusEvent; "action:detected": AgentWidgetActionEventPayload; "action:resubmit": AgentWidgetActionEventPayload; "widget:opened": AgentWidgetStateEvent; "widget:closed": AgentWidgetStateEvent; "widget:state": AgentWidgetStateSnapshot; "message:feedback": AgentWidgetMessageFeedback; "message:copy": AgentWidgetMessage; "message:read-aloud": AgentWidgetReadAloudEvent; "eventStream:opened": { timestamp: number; }; "eventStream:closed": { timestamp: number; }; "approval:requested": { approval: AgentWidgetApproval; message: AgentWidgetMessage; }; "approval:resolved": { approval: AgentWidgetApproval; decision: string; }; }; /** * Layout for the artifact split / drawer (CSS lengths unless noted). * * **Close behavior:** In desktop split mode, the artifact chrome `Close` control uses the same * dismiss path as the mobile drawer (`onDismiss` on the artifact pane): the pane is hidden until * new artifact content arrives or the host calls `showArtifacts()` on the widget handle. */ type AgentWidgetArtifactsLayoutConfig = { /** Flex gap between chat column and artifact pane. @default 0.5rem */ splitGap?: string; /** Artifact column width in split mode. @default 40% */ paneWidth?: string; /** Max width of artifact column. @default 28rem */ paneMaxWidth?: string; /** Min width of artifact column (optional). */ paneMinWidth?: string; /** * When the floating panel is at most this wide (px), use in-panel drawer for artifacts * instead of a side-by-side split (viewport can still be wide). * @default 520 */ narrowHostMaxWidth?: number; /** * When true (default), widen the launcher panel while artifacts are visible and not user-dismissed. * No-op for inline embed (`launcher.enabled === false`). */ expandLauncherPanelWhenOpen?: boolean; /** Panel width when expanded (launcher + artifacts visible). @default min(720px, calc(100vw - 24px)) */ expandedPanelWidth?: string; /** * When true, shows a drag handle between chat and artifact columns in desktop split mode only * (hidden in narrow-host drawer and viewport ≤640px). Width is not persisted across reloads. */ resizable?: boolean; /** Min artifact column width while resizing. Only `px` strings are supported. @default 200px */ resizableMinWidth?: string; /** Optional max artifact width cap while resizing (`px` only). Layout still bounds by chat min width. */ resizableMaxWidth?: string; /** * Visual treatment for the artifact column in split mode. * - `'panel'`: bordered sidebar with left border, gap, and shadow (default). * - `'seamless'`: flush with chat: no border or shadow, container background, zero gap. * @default 'panel' */ paneAppearance?: "panel" | "seamless"; /** Border radius on the artifact pane (CSS length). Works with any `paneAppearance`. */ paneBorderRadius?: string; /** CSS `box-shadow` on the artifact pane. Set `"none"` to suppress the default shadow. */ paneShadow?: string; /** * Full `border` shorthand for the artifact `