/** * Simplified message shape used by the server follow-up generator - no AI SDK * types to import. `content` is the concatenated text of the message's text * parts. */ interface FollowUpMessage { role: string; content: string; } /** * Theming is exactly three colors, all required. Every other color in the * widget is derived from these (see the ramp in ChatWidget). There is no * mode: light vs dark is simply which colors you pick. Omit `theme` entirely * to get the stock palette. If any value is not valid hex, the whole theme * is ignored — a theme is never half-applied. */ interface ThemeConfig { /** * Chat background color (hex, e.g. "#171717"). */ backgroundColor: string; /** * Body text color (hex). Must contrast with `backgroundColor` — the widget * renders what you declare and does not second-guess it. */ textColor: string; /** * Primary accent color (hex) — send button, links, focus states. */ primaryColor: string; } interface FeatureConfig { /** * Enable file uploads. When true, the input toolbar shows a paperclip * button and accepts files matching `fileUploadAccept`. */ fileUpload?: boolean; /** * Comma-separated `accept` filter passed to the file picker when * `fileUpload` is true. Mirrors the HTML input[type=file] `accept` * syntax (e.g. `"image/*,application/pdf,.csv"`). Defaults to * `"image/*"` for backwards compatibility — consumers serving more * file types should set this to whatever their server / model * actually accepts. */ fileUploadAccept?: string; /** * Per-file size cap in bytes. The file picker / drag-and-drop * surface rejects files above this size before any network call, * showing the consumer's onError hook. Should mirror whatever the * upload endpoint enforces server-side. When unset, no client-side * size check is applied. */ fileUploadMaxBytes?: number; /** * Enable web search */ webSearch?: boolean; /** * Voice dictation: a microphone button in the composer that turns speech * into text in the textbox (the user still edits and presses Send — * nothing is auto-sent). Uses the browser's built-in speech recognition, * so there is no server, key, or audio path through the host or mordn; * Chrome and Safari may send audio to Google/Apple speech services. The * button renders only where the browser supports the API (Chrome, Edge, * Safari; not Firefox by default). Defaults to `true`. */ voiceInput?: boolean; /** * BCP-47 language tag for dictation (e.g. `"en-GB"`, `"de"`). When * unset, the page's `` is used, then the browser language. */ voiceInputLanguage?: string; } /** * Size presets for the chat widget * Each preset uses clamp() for responsive sizing: * - compact: clamp(300px, 24vw, 400px) - sidebars, minimal footprint * - default: clamp(320px, 28vw, 500px) - balanced for most use cases * - large: clamp(400px, 35vw, 700px) - content-heavy conversations * - full: 100% - fills entire screen */ type ChatWidgetSize = 'compact' | 'default' | 'large' | 'full'; /** * Layout shape the widget renders in. * * - `popup` : floating side panel, opened by a toggle button (default). * Best for ambient assistance available across pages. * - `inline` : renders in place inside the parent element. No toggle button, * no fixed positioning, fills its container. Best for dashboard * cards or dedicated chat sections of a page. * - `page` : full-viewport layout with a conversation list sidebar on the * left and the active chat on the right. Best for a dedicated * chat route (e.g. `/chat`). * * The `popup` value is the historical default and remains backward compatible. */ type ChatWidgetLayout = 'popup' | 'inline' | 'page'; interface DisplayConfig { /** * How the widget renders. * Default: `'popup'` (backward compatible). */ layout?: ChatWidgetLayout; /** * Preset size for the widget (recommended) * Uses clamp() for responsive sizing with min/max bounds * Default: 'default' * * Note: `size` only applies in `popup` layout. `inline` and `page` layouts * fill their container regardless. */ size?: ChatWidgetSize; /** * Custom width override (any CSS value) * Examples: * - '400px' (fixed) * - '30vw' (viewport-relative) * - 'clamp(300px, 25vw, 500px)' (responsive with bounds) * Overrides the size preset if provided */ width?: string; /** * Enable drag-to-resize functionality * Default: true */ resizable?: boolean; /** * Initial state (open or closed) * Default: false */ defaultOpen?: boolean; /** * How starter prompts are laid out in the empty state. * - `'list'` (default): full-width rows, good for descriptive prompts with * subtitles. * - `'grid'`: a 2-column chip grid, good for short, scannable prompts * (optionally with an `icon`). */ starterPromptsLayout?: 'list' | 'grid'; /** * Show toggle button to open the chat * Default: true */ showToggleButton?: boolean; /** * Custom toggle button position */ toggleButtonPosition?: { bottom?: string; right?: string; }; /** * Open (toggle) the widget with a keyboard shortcut, so docs sites and * dashboards can wire an "Ask AI" affordance into their OWN chrome — nav * button, search bar, `/` command — without holding a `ChatWidgetHandle` * ref (#193). Also enables two other zero-JS/zero-ref open routes on the * same widget instance regardless of this setting: `data-mordn-chat-open` * / `-toggle` / `-close` attributes on any element (one delegated document * click listener, works from static/markdown-generated HTML), and a * `document` CustomEvent API (`mordn-chat:open` / `:close` / `:toggle`) — * the same thing a script-tag embed dispatches internally. See the README * section "Opening the widget from your site" for copy-pasteable examples * of all three. * * **Format**: a `+`-separated combo of modifier tokens and exactly one * key, matched against `KeyboardEvent.key` case-insensitively, e.g.: * - `"mod+k"` — the primary modifier + K * - `"mod+i"` — the primary modifier + I * - `"ctrl+shift+/"` — three tokens, last one is the key * - `"/"` — a single bare key, no modifier * * `"mod"` resolves at match time to `metaKey` on Mac (Cmd) and `ctrlKey` * everywhere else (Ctrl) — the conventional cross-platform "primary * modifier" token, same convention as most command-palette libraries. * Matching is an EXACT modifier-set match: `"mod+k"` does not also fire on * Cmd+Shift+K. * * **Recommended default for docs sites**: `"mod+i"`. `Cmd/Ctrl+K` has * become the de facto convention for "open search" (used by the doc site's * own search-bar shortcut in most themes); `Cmd/Ctrl+I` is the emerging * convention for "open AI chat" and won't fight the search shortcut for * the same key. * * **Typing and host shortcuts**: ALL combos, including modifier combos, * are ignored inside `input`, `textarea`, `select`, or `contenteditable` * surfaces (including descendants and `plaintext-only`). This intentionally * leaves editor shortcuts such as Cmd/Ctrl+I for italic alone. Previously, * only bare keys were suppressed while typing. Repeated/composing keydowns * and events already claimed with `preventDefault()` are also ignored. * A host can reserve a conflicting shortcut with `preventDefault()` before * it reaches the widget's window listener, or with `stopPropagation()`. * * **Explicit intent**: shortcuts and `data-mordn-chat-open` / `-toggle` * clicks can reopen a dismissed panel even when `allowAutoReopen` is false, * just like the launcher. Ref methods and `mordn-chat:*` CustomEvents remain * programmatic: opening still requires `allowAutoReopen`. All paths share * controlled-mode delegation and persistence; closing is always allowed. * * **Multi-instance**: if a page mounts more than one ``, a * matching shortcut/button/event fires ALL of them (no cross-instance * coordination) — fine for the common one-widget-per-page case; avoid * configuring the same shortcut on multiple simultaneously-mounted * instances if you don't want that. * * Default: `undefined` (off). There is no implicit shortcut — the widget * NEVER silently hijacks a host page's keybindings. Set `false` explicitly * to make "no shortcut" visible in code/diffs if you want that documented. */ keyboardShortcut?: string | false; } /** * Tiny declarative field schema + reader used by the canonical AgentConfig * contract (`src/config.ts`). * * WHY THIS EXISTS (and why it isn't zod): the config contract is consumed by * three repos and, more importantly, by MANY DEPLOYED VERSIONS of this package * at once. A field that the dashboard publishes today is read tomorrow by a * customer's server that was installed six weeks ago. The one property the * validator must therefore have is *forward tolerance*: a document that carries * a field this build has never heard of must still load. A hand-rolled engine * lets us make that guarantee structural — every validator in the package is * derived from ONE descriptor, and the read mode (strict vs tolerant) is a * parameter, so "strict here, tolerant there" can never drift. * * Two modes, one descriptor: * * • `strict` — the WRITER contract. Unknown keys and invalid values are * errors. Used where a document is authored or accepted for * storage (publish, preview trust boundary). Strictness at the * writer is what keeps typos and junk out of the store. * • `tolerant` — the READER contract. Unknown keys are DROPPED (recorded, not * fatal). An OPTIONAL known field whose value this build cannot * interpret (a new enum member, a changed shape) is DROPPED so * the schema default applies. Only a missing/invalid REQUIRED * field is fatal. Used by every runtime consumer: the customer's * handler, the hosted config fetcher, the browser bootstrap. * * The invariant this buys: within one `schemaVersion`, an OLDER reader can * always load a NEWER document (additive fields are ignored; new enum members * fall back to defaults). A change that would break that — removing a field, * changing a field's type or meaning, making an optional field required — is a * new `schemaVersion`. See docs/config-evolution.md. * * The reader always returns a NEW value containing only known, valid fields. * Callers never observe unknown keys, so downstream code cannot accidentally * depend on something the contract does not define. */ type Primitive = string | number | boolean; type ReadIssueCode = 'unknown_key' | 'invalid_value' | 'missing_required'; interface ReadIssue { /** Dotted path from the document root, e.g. `client.features.voiceInput`. */ path: string; code: ReadIssueCode; message: string; } type ReadResult = { ok: true; value: T; /** * Fields the reader ignored. Empty in strict mode (anything here would * have been an error). Non-empty in tolerant mode means "this document * was written by a newer schema than this build knows" — worth logging * once, never worth failing over. */ dropped: ReadIssue[]; } | { ok: false; issues: ReadIssue[]; }; interface FieldDescription { path: string; kind: string; required: boolean; since: string; description: string; default?: Primitive; enum?: Primitive[]; min?: number; max?: number; integer?: boolean; literal?: Primitive; } /** * THE schema descriptor for the canonical AgentConfig contract (schema v1). * * Everything that validates, describes, or defaults an agent configuration in * this package is derived from the field maps below — `isAgentConfig` (strict * writer), `readAgentConfig` / `readAgentBootstrap` (tolerant readers), * `resolveFeatures` (defaults), `describeAgentConfigSchema()` (the * machine-readable contract the compatibility gate snapshots). There is no * second list of keys anywhere. Adding a field is ONE entry here plus the * matching optional property on the TypeScript interface. * * GATES (they fail the build / the test suite, on purpose): * * 1. Every field map is declared `satisfies Record, Field>`. * Add a property to `FeatureConfig` without a descriptor entry → type * error. Add a descriptor entry without the property → type error. The * type and the contract cannot drift. * 2. `test/config-evolution.test.ts` snapshots `describeAgentConfigSchema()` * and diffs it against the baseline of the LAST RELEASE. Removing a field, * changing its kind, tightening a range, dropping an enum member, or * making an optional field required fails the suite — those are * schema-major changes and need a new `schemaVersion`, not a PR. * * RULES OF THE ROAD (full text in docs/config-evolution.md): * * • New fields are OPTIONAL and carry `since` + a `default` where one exists. * • A field's meaning never changes. Need different semantics? New field. * • Enum members may be ADDED (old readers drop the unknown value and apply * the default) but never removed or renamed. * • `required: true` is reserved for the envelope — fields no reader can * default. New required fields cannot be added to an existing object. */ /** Schema major. Bumps only for breaking changes (see module doc). */ declare const AGENT_CONFIG_SCHEMA_VERSION: 1; /** Independent of `schemaVersion`: transport shape and config schema evolve separately. */ declare const BOOTSTRAP_PROTOCOL_VERSION: 1; interface SerializableStarterPrompt { title: string; subtitle?: string; } interface SerializableFollowUpConfig { enabled?: boolean; max?: number; timeoutMs?: number; } interface SerializableTitleConfig { enabled?: boolean; timeoutMs?: number; } interface SerializableMemoryConfig { enabled: boolean; /** Inject recalled memories before generation. */ inject: boolean; /** Extract and store memories after a completed turn. */ extract: boolean; /** Maximum memories injected per turn. */ limit: number; } /** Managed workspace per verified user within an agent/tenant; absent means off. */ interface SerializableSandboxConfig { enabled: boolean; } interface AgentRuntimeConfig { model: string; systemPrompt?: string; temperature?: number; maxOutputTokens?: number; followUps?: boolean | SerializableFollowUpConfig; /** * Smart thread titles: a lightweight post-response model call names the * conversation after its first exchange. Default ON — pass `false` to keep * the first-message-prefix placeholder titles. */ titles?: boolean | SerializableTitleConfig; memory?: SerializableMemoryConfig; /** Enable sandboxes: Mordn-managed, shared across this user's conversations. */ sandbox?: SerializableSandboxConfig; } interface AgentClientConfig { greeting?: string; /** Faint line under the greeting headline (free display text). */ subGreeting?: string; /** The assistant's name (identity, not the greeting sub line). */ assistantName?: string; theme?: ThemeConfig; features?: FeatureConfig; display?: DisplayConfig; starterPrompts?: SerializableStarterPrompt[]; capabilitiesPrompt?: string; feedback?: boolean; streamingThrottleMs?: number; persistState?: boolean; allowAutoReopen?: boolean; } interface AgentConfig { schemaVersion: typeof AGENT_CONFIG_SCHEMA_VERSION; runtime: AgentRuntimeConfig; client: AgentClientConfig; } /** Hosted control-plane record consumed by the handler. */ interface PublishedAgentConfig { agent: string; revision: string; config: AgentConfig; } /** Browser-safe projection returned from GET /bootstrap. */ interface AgentBootstrap { protocolVersion: typeof BOOTSTRAP_PROTOCOL_VERSION; agent: string; revision: string; client: AgentClientConfig; storageScope: string; } declare function mergeAgentClientConfig(published: AgentClientConfig | undefined, explicit: AgentClientConfig | undefined): AgentClientConfig; /** * Read a complete schema-v1 config the way a RUNTIME must: fields this build * doesn't know are dropped and reported in `dropped`, never fatal. Fails only * on a wrong `schemaVersion` or a missing/invalid required field. Use in the * handler, the hosted config fetcher, and any other consumer of a published * document. The returned `value` contains only known, valid fields. */ declare function readAgentConfig(value: unknown): ReadResult; /** Tolerant reader for the browser-safe bootstrap envelope (see `readAgentConfig`). */ declare function readAgentBootstrap(value: unknown): ReadResult; /** * Validate the complete, strict schema-v1 config before it is authored, * stored, or trusted as a preview. Unknown keys are rejected — this is what * keeps typos and junk out of published revisions. Runtime consumers must use * `readAgentConfig` instead, or a newer dashboard will break older deployments. */ declare function isAgentConfig(value: unknown): value is AgentConfig; /** Strict validator for the bootstrap envelope. Browsers should prefer `readAgentBootstrap`. */ declare function isAgentBootstrap(value: unknown): value is AgentBootstrap; /** `FeatureConfig` with every defaulted flag resolved. */ interface ResolvedFeatureConfig { fileUpload: boolean; fileUploadAccept: string; fileUploadMaxBytes?: number; webSearch: boolean; voiceInput: boolean; voiceInputLanguage?: string; } /** Feature defaults, sourced from the descriptor — the single place they live. */ declare const DEFAULT_FEATURES: ResolvedFeatureConfig; /** * Apply schema defaults to a (possibly partial or absent) `features` object. * Consumers read flags from the result instead of sprinkling `=== true` / * `?? 'image/*'` fallbacks through UI code, so a default changes in one place. */ declare function resolveFeatures(features?: FeatureConfig | null): ResolvedFeatureConfig; /** * Machine-readable description of the whole contract: every path with its * kind, requiredness, `since`, default and constraints. The compatibility gate * (`test/config-evolution.test.ts`) snapshots this and diffs it against the * last release; dashboards and docs can render it directly. */ declare function describeAgentConfigSchema(): { schemaVersion: 1; bootstrapProtocolVersion: 1; config: FieldDescription[]; bootstrap: FieldDescription[]; }; /** One-line summary of read issues for error messages and logs. */ declare function formatConfigIssues(issues: readonly ReadIssue[]): string; export { type AgentConfig as A, BOOTSTRAP_PROTOCOL_VERSION as B, DEFAULT_FEATURES as D, type FollowUpMessage as F, type PublishedAgentConfig as P, type ReadIssue as R, type SerializableMemoryConfig as S, type AgentRuntimeConfig as a, type AgentClientConfig as b, type AgentBootstrap as c, type SerializableSandboxConfig as d, type ReadResult as e, AGENT_CONFIG_SCHEMA_VERSION as f, formatConfigIssues as g, isAgentConfig as h, isAgentBootstrap as i, readAgentConfig as j, type SerializableStarterPrompt as k, type SerializableFollowUpConfig as l, type SerializableTitleConfig as m, mergeAgentClientConfig as n, type ResolvedFeatureConfig as o, resolveFeatures as p, describeAgentConfigSchema as q, readAgentBootstrap as r, type FieldDescription as s };