import { components } from '@ai-matrx/api/types'; /** * Agent Settings — Type Definitions * * All field names are snake_case to match the Python backend exactly. * This is the single canonical type used across all contexts: * - Prompt Builder (defaults only, no override tracking) * - Chat (overrides tracked separately from agent defaults) * - Test (multiple agents simultaneously, all in memory) */ type NonNullableFields = { [K in keyof T]?: NonNullable; }; /** The generated server contract for model settings (nulls stripped, as the chat types did). */ type LLMParams = NonNullableFields; /** A variable's custom input component: only the discriminant is read here; hosts narrow it. */ interface VariableCustomComponentRef { type: string; } /** * Agent model settings are the generated server contract—never a handwritten * subset or frontend extension. UI-only input capabilities live in UiGates and * runtime attachment state; selected tools live in the client-tools slice. */ type AgentSettings = LLMParams; type ControlType = "number" | "integer" | "boolean" | "string" | "enum" | "array" | "string_array" | "object_array"; interface ControlDefinition { type: ControlType; min?: number | undefined; max?: number | undefined; default?: unknown | undefined; enum?: string[] | undefined; required?: boolean | undefined; /** True when the control is an "allowed" feature flag (not a submission param) */ isFeatureFlag?: boolean | undefined; } /** * Parsed capability map for a model. Keyed by AgentSettings field names (snake_case). * Always has rawControls and unmappedControls for debugging. */ type NormalizedControls = Partial> & { /** Frontend capability declarations from model controls, never API params. */ tools?: ControlDefinition; image_urls?: ControlDefinition; file_urls?: ControlDefinition; youtube_videos?: ControlDefinition; rawControls: Record; unmappedControls: Record; }; type ConflictReason = "unsupported_key" | "value_out_of_range" | "invalid_enum_value"; interface ConflictItem { key: keyof AgentSettings; currentValue: unknown; newModelDefault: unknown; reason: ConflictReason; description: string; /** If the new model uses a different parameter name for the same concept */ aliasHint?: string | undefined; } type ResolutionMode = "keep_all" | "auto_resolve" | "remove_only" | "custom"; type ConflictAction = "keep" | "reset"; /** * All state needed to present and resolve a pending model switch. * Lives in Redux state until the user confirms or cancels. */ interface PendingModelSwitch { prevModelId: string; prevModelName: string; newModelId: string; newModelName: string; conflicts: ConflictItem[]; /** Keys from current settings that are fully compatible with the new model */ supportedKeys: Array; newModelControls: NormalizedControls; mode: ResolutionMode; /** Per-conflict override when mode === 'custom' */ customActions: Partial>; /** * The CLASS (offering) picked with the new model, applied on confirm. * Absent = no class picked → the old model's pin is dropped on confirm (a * class belongs to exactly one model). Never written before confirm, so the * entry never pairs the old model with the new model's class. */ offeringId?: string | undefined; } interface AgentVariable { name: string; defaultValue: string; customComponent?: VariableCustomComponentRef; required?: boolean; helpText?: string; } type AgentSource = "prompt" | "builtin"; type AgentContext = "builder" | "chat" | "test"; interface AgentSettingsEntry { agentId: string; source: AgentSource; /** builder: edits go to `defaults`; chat/test: edits are `overrides` over the defaults. */ context: AgentContext; defaults: AgentSettings; overrides: Partial; variable_defaults: AgentVariable[]; variable_overrides: Record; pendingSwitch: PendingModelSwitch | null; isDirty: boolean; } /** The settings store's whole state: one entry per agent, the ONE holder of settings state. */ interface AgentSettingsState { entries: Record; activeAgentId: string | null; } /** * UI Gates — canonical model-gated UI flags. * * FE-ONLY. These flags NEVER reach the server. They gate what the chat / agent * builder UI exposes for the currently-selected model (e.g. whether to offer the * image-URL, file-URL or YouTube attachment inputs) — which in turn shapes the * request the client constructs. The flags themselves are not model parameters, * so the backend neither receives nor cares about them. * * Persisted in the dedicated `agx_agent.ui_gates` / `agx_version.ui_gates` jsonb * column — NEVER inside `settings` (which holds only server-consumed params). * * This module is the SINGLE SOURCE OF TRUTH that replaced the `UI_CAPABILITY_KEYS` * sets formerly copy-pasted across the execution selectors, the manual-execute * thunk, the validation rules/resolve-config, and the model-controls parser. * The concept is intentionally generic and open to growth: any future * "show X in the UI when the model supports it" affordance lands here. */ /** * UI-only agent/run configuration. This parser is the source of truth for the * persisted `ui_gates` JSONB boundary; never mirror it with an interface. * New product UI capabilities are added here, never to the Python parameter * contract. Unknown persisted keys are stripped at this boundary. */ interface UiGates { tools?: boolean; image_urls?: boolean; file_urls?: boolean; youtube_videos?: boolean; } /** Parse persisted/UI-boundary data. Invalid shapes fail loudly; unknown keys are stripped. */ declare function parseUiGates(value: unknown): UiGates; /** Every gate key the UI currently understands. Extend here, nowhere else. */ declare const UI_GATE_KEYS: readonly ["tools", "image_urls", "file_urls", "youtube_videos"]; type UiGateKey = (typeof UI_GATE_KEYS)[number]; /** * The gates a USER toggles per-agent (the chat-attachment affordances). * * `tools` is intentionally **excluded**: tool support is a MODEL capability * (resolved from `ai_model.controls.tools` via `supportsTools` in * `useModelControls`), not a per-agent UI gate. `tools` remains in * `UI_GATE_KEYS` so any legacy `ui_gates.tools` value is still recognized and * stripped before the API call — it is just never offered as an editable toggle. */ declare const UI_GATE_EDITABLE_KEYS: readonly ["image_urls", "file_urls", "youtube_videos"]; type UiGateEditableKey = (typeof UI_GATE_EDITABLE_KEYS)[number]; /** True when `key` is a model-gated UI flag (belongs in ui_gates, not settings). */ declare function isUiGateKey(key: string): boolean; /** The empty default — a record with no gates set. */ declare const EMPTY_UI_GATES: UiGates; /** Read a single gate as a strict boolean (absent ⇒ false). */ declare function gateEnabled(gates: UiGates | null | undefined, key: UiGateKey): boolean; /** * Model Settings Catalog — the single source of truth for the STANDARD * settings list, and the one chokepoint that decides which keys it contains. * * ── The model (two distinct surfaces) ──────────────────────────────────────── * 1. STANDARD list — ONLY the keys the selected model explicitly declares in * its `controls` config (i.e. the parameters this model actually supports). * `buildSettingsRows()` returns exactly these, grouped + ordered. Catalog * keys the model does not declare are NOT shown here. * 2. CAUTION surface — settings the agent has SET that the model does NOT * support. These are never hidden: they are flagged as "not valid for this * model" and made repairable by the validation layer (the IssueTable, fed * by the `unsupported_by_model` rule). They do NOT belong in the standard * list — that is exactly the "showing all possible keys" bug. * * The catalog exists so the standard list's labels, grouping and order live * in ONE place (previously copy-pasted as `textModelSettings` / * `booleanSettings` / ... into three components, each filtering by * `getControl(key)` inline — which drifted and regressed). Components map over * `buildSettingsRows()`; they must not filter by model themselves (ESLint * enforces). The original recurring bug — set values silently dropped when a * model didn't support them — is fixed by the CAUTION surface, not by dumping * every key into the standard list. * * Guardrail: `settings-catalogue.test.ts` asserts the standard list contains * ONLY supported keys (never the full catalog), and `unsupported-by-model. * test.ts` asserts set-but-unsupported keys surface in the caution layer. */ type SettingsGroupId = "text" | "featureFlags" | "imageVideo" | "audio" | "other"; interface CatalogueEntry { key: string; label: string; } interface CatalogueGroup { id: SettingsGroupId; /** Section header shown above the group. Empty string = no header (lead group). */ label: string; entries: CatalogueEntry[]; } declare const SETTINGS_CATALOGUE: CatalogueGroup[]; /** Flat set of every key the catalog declares. */ declare const CATALOGUE_KEYS: ReadonlySet; /** * 🚨 THE INTERNAL KEYS — the ONE list of settings keys that are plumbing, never a setting a person * reads or edits. Every settings panel (the builder, Chat Options › Model and overrides, every * override screen) skips them by this list and nothing else: identity (`model`, `model_id` — the * model row owns them), routing (`offering_id` the class pin, `served_via` the device a local * model answers on — L3 final walk 2026-10-24 drew it as a raw JSON box tagged "Not on this * model"), and the controls bookkeeping. A new routing key is added HERE, never special-cased * in a panel. Guard: `internal-keys-never-render.test.tsx`. */ declare const INTERNAL_SETTING_KEYS: ReadonlySet; /** True when `key` is plumbing the panels never draw (`INTERNAL_SETTING_KEYS`). */ declare function isInternalSettingKey(key: string): boolean; interface ModelSettingRow { key: string; label: string; group: SettingsGroupId; /** The model's control definition for this key. Standard rows are always * supported, so this is present; typed nullable for consumer convenience. */ control: ControlDefinition | null; /** Always true for rows returned by buildSettingsRows (standard = supported). */ supported: boolean; /** True when the current settings hold a non-null value for this key. */ hasValue: boolean; } interface RenderGroup { id: SettingsGroupId; label: string; rows: ModelSettingRow[]; } /** A minimal structural view of a model's normalized controls. Accepts either * NormalizedControls variant in the codebase (both are a key→ControlDefinition * map plus rawControls/unmappedControls). */ type ControlsLike = Record | null | undefined; /** The model's control for one setting key (`null` when the model declares none). */ declare function settingControl(controls: ControlsLike, key: string): ControlDefinition | null; /** * Build the STANDARD settings list for the selected model. * * CONTRACT: returns ONLY keys the model declares a control for (supported). * Catalog keys the model does not declare are omitted — they are not "shown" * in the standard list. Set-but-unsupported keys are NOT returned here either; * the caution layer (validation IssueTable) surfaces and repairs those. * * The catch-all "Other" group carries supported keys the model declares that * the catalog doesn't name yet — so a brand-new model control still appears * without a code change. Empty groups are returned (consumers skip them). */ declare function buildSettingsRows(controls: ControlsLike, settings: Record | null | undefined): RenderGroup[]; /** A setting's label: the catalog's own words, else the key humanized (the catch-all rule). */ declare function settingLabel(key: string): string; interface ExtraSettingKeys { /** * Set, a real model parameter, not declared by this model: KEPT and sent as the model's * closest equivalent — the server translates it (settings-translation K7, the platform rule). * THE panel draws these under "Translated for this model", never as a warning. */ translated: string[]; /** Set, but no model parameter at all (a typo, a retired key): "Not on this model". */ unsupported: string[]; } /** * The set values THE model-settings panel draws beside the model's own rows — one rule for the * builder and every override screen (owner, 2026-10-21). `alsoKeys` are keys a target changed * that hold no value now (an override that removes the agent's value): they stay listed so * their reset stays reachable. Empty until the model declares at least one control: a model * with no declared controls is a catalog gap, not an incompatibility. */ declare function extraSettingKeys(controls: ControlsLike, settings: Record | null | undefined, alsoKeys?: readonly string[]): ExtraSettingKeys; /** * Agent Settings — Internal Utilities * * Pure functions with NO Redux imports. No React hooks. * All logic that was previously scattered across: * - useModelControls hook * - switchModel thunk * - ModelChangeConflictModal component * - ConversationInput.handleSettingsChange * * is consolidated here and consumed by the slice's reducers/thunks. */ /** * Cross-provider parameter name synonyms. * When a setting key is unsupported by a new model, we check aliases * to suggest the equivalent parameter name the new model may use instead. */ declare const PARAM_ALIASES: Readonly>; /** * Parse raw JSONB controls from `ai_model.controls` into a typed NormalizedControls map. * * Absorbs the logic from `useModelControls` hook. * * Control JSONB shape per field: * { type?, min?, max?, default?, enum?, required?, allowed? } * * Special cases: * - `output_format` → `response_format` only for text response formats * (outputFormatControlKey); an image file format keeps its own key * - `allowed: true/false` → boolean feature flag * - enum values that are `{ type: "json_object" }` objects → flattened to string */ declare function parseModelControls(rawControls: Record | null | undefined): NormalizedControls; /** * Extract submission-ready default values from a model's raw controls JSONB. * * Absorbs the logic from `getModelDefaults` in `useModelControls`. * * Rules: * - Controls with `default: null` are SKIPPED (opt-in only controls) * - `tools: { allowed: true }` → `tools: []` (empty list, not a boolean) * - `response_format: "text"` → SKIP (backend treats absence as text) * - `response_format: "json_object"` → `{ type: "json_object" }` (dict form) * - UI-only capability flags (image_urls, file_urls, etc.) → kept as booleans */ declare function extractModelDefaults(rawControls: Record | null | undefined): Partial; /** * Detect which keys in `currentSettings` are incompatible with `newControls`. * * Returns: * - `conflicts`: settings that have problems (unsupported, out-of-range, wrong enum) * - `supportedKeys`: settings that carry over cleanly * * This is the NEW detection logic that was missing from the existing system * (the old ModelChangeConflictModal received conflicts pre-built from above). */ declare function detectConflicts(currentSettings: Partial, newControls: NormalizedControls, newDefaults: Partial): { conflicts: ConflictItem[]; supportedKeys: Array; }; /** * Apply a pending switch's resolution mode + custom actions to produce * the final settings after the model change is confirmed. * * Absorbs `computeResolvedSettings` from ModelChangeConflictModal. */ declare function resolveConflicts(currentSettings: Partial, _pending: PendingModelSwitch): Partial; /** * Determine the action ('keep' | 'reset') for a conflict given the current mode. * Absorbs `getActionForMode` from ModelChangeConflictModal. */ declare function getActionForMode(_mode: ResolutionMode, _conflict: ConflictItem, _customActions: Partial>): ConflictAction; /** * Compare `proposed` settings against `defaults` and return only the keys * that genuinely differ. Uses JSON-stringify for deep comparison. * * Absorbs the diff logic from ConversationInput.handleSettingsChange. * In chat/test contexts, only true overrides are stored — not redundant defaults. * * REMOVAL IS A DIFF TOO: a key the defaults carry that `proposed` dropped (or * set to null) is stored as an explicit `null` override. Without it a removal * was inexpressible — the default (e.g. the agent's class pin `offering_id`) * silently came back on the next merge and on the wire. `null` = "the API must * not use this key"; `mergeEffectiveSettings` deletes it, `buildApiPayload` * sends it (the server honors `offering_id: null`). */ declare function computeOverrideDiff(defaults: Partial, proposed: Partial): Partial; /** * The class pin (`offering_id`) an entry runs on: an override — including an * explicit `null` removal — wins over the defaults. Never `overrides ?? defaults` * (a removal is `null`, which `??` would fall through to the default's pin). */ declare function effectiveOfferingPinOf(entry: { defaults?: Partial; overrides?: Partial; } | null | undefined): string | undefined; /** * Build the payload to send to the Python backend for a given context. * * - 'builder': full effective settings minus UI-only flags * - 'chat': only the overrides delta minus UI-only flags * (backend already has the agent's defaults — only send what changed) * - 'test': same as builder (full effective settings) */ declare function buildApiPayload(effectiveSettings: Partial, overrides: Partial, context: AgentContext): Partial; /** * Merge defaults + overrides into the effective settings the UI should display. * Overrides take precedence field-by-field. */ declare function mergeEffectiveSettings(defaults: Partial, overrides: Partial): Partial; /** * Produce a flat name→value map from variable_defaults merged with variable_overrides. * Used by variable input components to read current values without prop drilling. */ declare function mergeVariableValues(variableDefaults: AgentVariable[], variableOverrides: Record): Record; /** * Strip null/undefined fields from a settings object. * Used when loading from DB to ensure the state is clean. */ declare function sanitizeSettings(raw: Record | null | undefined): Partial; /** * `createSettingsStore` — THE holder of agent settings state (agent core B4). * * Framework-free, the same shape as `createRunTierStore` / `createModelCatalog`: `getState` / * `subscribe`, immutable updates, no Redux, no fetch and NO WRITES. The host seeds an entry with * the defaults it already holds (the builder from its full definition, a run from its run tier) * and saves `entry.defaults` itself — the core never writes an agent row (guard G1). * * Contexts: `builder` edits go to `defaults`; `chat` / `test` edits are `overrides` over the * defaults (only true differences are stored; an explicit removal is a `null` override). */ /** What the store needs to know about a model to switch to it. */ interface SettingsModelInfo { id: string; name: string; controls: Record | null; } interface SettingsStoreConfig { /** A model by id (the host's model catalog). Required only for `requestModelSwitch`. */ lookupModel?: (modelId: string) => SettingsModelInfo | undefined; } interface InitializeSettingsInput { agentId: string; source?: AgentSource; context: AgentContext; settings?: Partial | Record | null; variable_defaults?: AgentVariable[] | null; } interface ConflictSummary { totalConflicts: number; unsupportedCount: number; invalidValueCount: number; supportedCount: number; willRemoveCount: number; willResetCount: number; willKeepCount: number; } interface SettingsStore { getState(): AgentSettingsState; subscribe(listener: () => void): () => void; /** Seed (or re-seed) one agent's entry. Re-seeding replaces defaults and clears edits. */ initialize(input: InitializeSettingsInput): void; /** Seed only when absent or when the defaults changed; keeps the person's overrides otherwise. */ ensure(input: InitializeSettingsInput): void; remove(agentId: string): void; setActive(agentId: string | null): void; setOverride(agentId: string, field: K, value: AgentSettings[K] | null): void; /** Apply a whole settings object (the dialog): builder → defaults, chat/test → override diff. */ applySettings(agentId: string, next: Partial): void; resetOverride(agentId: string, field: keyof AgentSettings): void; resetAllOverrides(agentId: string): void; updateDefaults(agentId: string, settings: Partial): void; /** After the host saved `defaults`: the entry is clean again. */ markSaved(agentId: string): void; addVariable(agentId: string, variable: AgentVariable): void; updateVariable(agentId: string, name: string, updates: Partial): void; removeVariable(agentId: string, name: string): void; setVariableOverride(agentId: string, name: string, value: string): void; resetVariableOverride(agentId: string, name: string): void; resetAllVariableOverrides(agentId: string): void; /** Open a model switch: conflicts against the new model's controls. Throws when either is unknown. */ requestModelSwitch(agentId: string, newModelId: string, options?: { offeringId?: string; }): void; setResolutionMode(agentId: string, mode: ResolutionMode): void; setCustomConflictAction(agentId: string, key: keyof AgentSettings, action: ConflictAction): void; confirmModelSwitch(agentId: string): void; cancelModelSwitch(agentId: string): void; } declare function createSettingsStore(config?: SettingsStoreConfig): SettingsStore; declare function readSettingsEntry(state: AgentSettingsState, agentId: string | null | undefined): AgentSettingsEntry | undefined; /** Defaults merged with overrides (an explicit `null` override removes the key). */ declare function readEffectiveSettings(entry: AgentSettingsEntry | undefined): Partial; /** The payload for the server: chat sends the override delta; builder/test the full effective set. */ declare function readApiPayload(entry: AgentSettingsEntry | undefined): Partial; /** The effective model id (override, else default), or null. */ declare function readEffectiveModelId(entry: AgentSettingsEntry | undefined): string | null; /** The previewed result of the open model switch under its current resolution mode. */ declare function readPreviewedResolution(entry: AgentSettingsEntry | undefined): Partial | null; declare function readConflictActions(entry: AgentSettingsEntry | undefined): Record; declare function readConflictSummary(entry: AgentSettingsEntry | undefined): ConflictSummary; /** Variable values the run sends: defaults with the person's overrides on top. */ declare function readEffectiveVariableValues(entry: AgentSettingsEntry | undefined): Record; /** * The three states of one model setting — THE definition every settings * surface reads (owner, 2026-10-02: "the difference between something being * set to none and something not being included at all"). * * unset — the key is ABSENT from the saved settings JSON. The model's own * default applies. A person can always return a setting here. * off — an explicit off value is saved: `false` for a boolean control, * `"none"` for an enum control that declares it (reasoning_effort, * thinking_level, …). The server translates it per model (K6 `off` * in common-docs/projects/settings-translation/CONTRACTS.md) — it is * never the same request as unset. * value — any other saved value. * * `null` in the saved JSON is treated as unset, and writing unset DELETES the * key — the client never stores `null` (or a sentinel like `-1`) to mean * "not set". */ type SettingState = "unset" | "off" | "value"; /** The enum value that means "off" when a control declares it. */ declare const ENUM_OFF_VALUE = "none"; /** * The explicit off value this control can save, or `undefined` when the * setting has no off (e.g. temperature — 0 is a value, not off). */ declare function offValueFor(control: ControlDefinition | null | undefined): boolean | string | undefined; /** True when `value` is an explicit off (boolean false or enum "none"). */ declare function isOffValue(value: unknown, control: ControlDefinition | null | undefined): boolean; declare function settingStateOf(settings: Record | null | undefined, key: string, control: ControlDefinition | null | undefined): SettingState; /** * Write one setting into a NEW settings object. * unset → the key is removed (never `null`) * off → the control's off value (no-op when the control has none) * value → `value` */ declare function withSettingState>(settings: T, key: string, state: SettingState, control: ControlDefinition | null | undefined, value?: unknown): T; /** * ONE MEANING PER STATE (settings-translation F-b, owner 2026-10-02: "the * difference between something being set to none and something not being * included at all"). * * For these keys the house "auto" posture IS the unset state on the server: * - visualization — the Google research agent reads an unset value as "auto" * (`providers/google/google_research_agent.py`). * So a control that offers "auto" for one of them must CLEAR the key, never * save the word. * * `reasoning_effort` is NOT here (owner ruling 2026-10-24): unset runs at the * platform default ("low"), and "auto" is a PERSON's explicit choice that the * server sends as the provider's own auto where it has one * (`matrx_ai/catalog/controls.py::apply_reasoning_default`). So a person picking * "auto" SAVES "auto"; it is never a starting value (`NEVER_IMPLICIT_AUTO_KEYS`). * * Every settings writer reads this one rule: the agent builder * (AgentSettingsCore), the conversation settings target (useConversationSettingsTarget), the * comparison column editor, and the wire selector. */ declare const AUTO_VALUE = "auto"; declare const AUTO_MEANS_UNSET_KEYS: ReadonlySet; /** Keys whose "auto" is a real choice that only a person makes — never a default. */ declare const NEVER_IMPLICIT_AUTO_KEYS: ReadonlySet; /** True when writing `value` to `key` means "not set" — the key is removed. */ declare function isUnsetChoice(key: string, value: unknown): boolean; /** True when `value` may not be chosen FOR a person (a starting value / default). */ declare function isNeverImplicitChoice(key: string, value: unknown): boolean; /** * THE starting value of a setting a person turns on (ticks its box). One rule * for every settings writer — the agent builder and every conversation / * binding override — so ticking "Max Output Tokens" stores the same value * wherever it is ticked. * * - the model's declared default when it has one, * - otherwise the control's natural empty value (min / "" / false / first * choice / []), * - never a choice that IS "not set" ("auto" effort): ticking SETS the * setting, so the first real choice is taken instead, * - `response_format` is stored in its canonical `{ type }` shape. * * `undefined` means nothing can be stored — the box stays unticked rather than * claiming a setting that does not exist. */ declare function startingValueFor(key: string, control: ControlDefinition): unknown; /** * THE fresh start (Arman, 2026-10-24): an agent with NO settings yet that gets a model * starts with that model's own declared defaults (Gemini 3.5 Flash-Lite: reasoning effort * "minimal"). Only a control's explicit `default` counts — never a capability flag, never * "auto". Returns `null` when the agent already holds any setting: a model switch never * changes, adds or removes a setting of an agent that has them. */ declare function freshSettingsFor(current: Record | null | undefined, controls: Record | null | undefined): Record | null; /** One model a host runs on the person's own computer. */ interface LocalModelEntry { /** The value `onValueChange` receives (the conversation's model override). */ id: string; name: string; /** Brand glyph key (e.g. "Meta"); absent draws the neutral glyph. */ maker?: string | null; contextWindow?: number | null; /** The computer cannot hold it today: the row is dimmed and says why, but stays pickable. */ needsMoreMemory?: boolean; /** * On the computer but it cannot run there, and why, in one line ("Part 2 of 3 is missing"). The * picker lists it under "Cannot run here" with this reason, dimmed and never pickable. */ unusable?: string; /** * The model is loaded and answering now. With a port `stop`, the row draws Stop; with `start` * and not running, Start. Absent = the host does not say (no control is drawn). */ running?: boolean; /** The host is loading it right now: the row's control shows its pending state. */ starting?: boolean; } /** * The host's local-model source (a store: `getModels` + `subscribe`, like the catalog). The picker * draws its models as one "On this computer" group marked local. Starting and stopping a model and * getting more of them are capabilities by presence: a host that supplies `start` / `stop` / `getMore` * gets a compact Start/Stop control per row and one "Get More Models..." row; a host that leaves them * out gets neither (its own screens stay the only way). */ interface LocalModelsPort { /** Must return the SAME array until the list changes (it is a store snapshot). */ getModels(): readonly LocalModelEntry[]; subscribe(listener: () => void): () => void; /** Group heading. Default "On this computer". */ groupLabel?: string; /** * Open the local model in its own private conversation (the host's Confidential Chat), instead * of switching this conversation to it. Present = a local pick calls ONLY this: the cloud value * and the class pin stay untouched, and no draft or context is carried. Absent = the pick reports * the id through `onValueChange` (the override path). */ open?(id: string): void; /** * Start the model (load it so it can answer). Present = a row that is not running draws Start. The * row shows a pending state until the returned promise settles; a rejection says why on the row. */ start?(id: string): void | Promise; /** Stop the running model. Present = a running row draws Stop (same pending state and failure line). */ stop?(id: string): void | Promise; /** Open the host's place to find and install more models. Present = one "Get More Models..." row ends the group. */ getMore?(): void; /** * Why the list could not be read just now, one line ("Your computers' models can't be read"), or null. * Set = the group shows that line, with Try Again when `retry` exists, instead of going quiet (the last * list stays under it). A change is announced through `subscribe`. */ readFailure?(): string | null; /** Read the list again: the failure line's Try Again. */ retry?(): void; /** True while the first read is in flight and nothing is listed yet: the group says "Checking the models". */ checking?(): boolean; /** * The line the group shows when the port lists no model at all ("No model on your computers yet"), with * Get More when the port has it. Absent = an empty port draws nothing. */ emptyLine?: string; } /** * How the person may reach a cloud model in THIS place (a chat composer passes it to its own picker, * like `localModels`). Both fields are optional; a host that supplies neither changes nothing. */ interface ModelAccessPort { /** * Set = every cloud model is listed disabled with this one-line reason (never hidden), e.g. a * confidential chat in an organization that requires its own hardware: "Blocked by your organization". */ cloudBlockedReason?: string | null; /** * True when the person's own API key serves this model: the picker and the chat's model chip say * "Your key" and show no model points. */ usesOwnKey?(modelId: string): boolean; } /** * THE SETTINGS WRITE TARGET — the one seam between THE model-settings panel * (`./react/ModelSettingsPanel`) and whatever a change is written to. * * Owner ruling, 2026-10-21: one settings experience. The agent builder and the * chat's "Model and overrides" render the SAME rows, controls, labels and * order; they differ ONLY in where a change lands. The builder's target writes * the agent; a conversation's (or a binding's, a mandate's, a shortcut's) * target writes that record's overrides and adds a per-row reset back to what * it inherits. The panel never knows which. * * Every method is the person's intent in the panel's words — never a store * action. Optional methods are optional ACTIONS: a target that cannot bind a * control to a run input simply has no `bind`, and the panel draws no button. */ /** What one row shows. Off is a `set` value the panel recognizes (setting-state). */ type SettingRowView = /** Key absent: the model's own default applies. */ { state: "unset"; modelDefault?: unknown; } /** A saved / effective value. */ | { state: "set"; value: unknown; } /** The setting is a run input: its value comes from `{{variableName}}`. */ | { state: "bound"; variableName: string; value: unknown; }; /** What the target knows about a row beyond its value. */ interface SettingRowFacts { /** True when THIS target changed the row (a conversation override). */ changedHere?: boolean; /** Where the shown value comes from, in the host's words ("Agent", "Organization"). */ source?: string; /** The row's first validation issue, when the target validates. */ /** `note`: a kept value the model takes differently — said quietly, never a warning. */ issue?: { message: string; fixable: boolean; note?: boolean; }; } type SettingRowRead = SettingRowView & SettingRowFacts; interface SettingsWriteTarget { read(key: string, control: ControlDefinition | null): SettingRowRead; /** The person edited the control. */ change(key: string, value: unknown, control: ControlDefinition | null): void; /** * Several settings that are one choice, written together (the Voice row's `tts_voice` + * `multi_speaker`). Without it the panel calls `change` once per key. */ changeMany?(changes: Record): void; /** The person ticked an unset row (store `startingValueFor`). */ turnOn(key: string, control: ControlDefinition): void; /** The person cleared the row back to "not set". */ clear(key: string): void; /** One-click repair of the row's issue. */ fix?(key: string): void; /** May this row become a run input? (Builder only.) */ canBind?(key: string): boolean; bind?(key: string, control: ControlDefinition): void; unbind?(key: string, control: ControlDefinition | null): void; /** Drop THIS target's change and show what it inherits again. */ resetToInherited?(key: string): void; /** * The keys THIS target changed. The panel keeps a changed key it has no row for (the model * does not declare it, and the change removed its value) on screen, so its reset stays * reachable. */ changedKeys?: readonly string[]; /** The reset button's words, in the host's noun. Default "Reset to agent". */ resetLabel?: string; } /** * THE model row — the panel's first line, drawn by the panel with THE model picker * (`ModelListDropdown`) in every host. Where a pick lands is the host's; the options below are * capabilities a target has or lacks, exactly like `bind` / `resetToInherited` on the rows. */ interface ModelRowTarget { /** The model shown (the effective one; a one-message pick while that is on). */ value: string | null; change(modelId: string): void; /** The pinned class (offering) of the shown model. */ pinnedOfferingId: string | null | undefined; /** A class pick (the picker reports the class before the model). */ changePin(offeringId: string | undefined): void; /** True when THIS target changed the model or its class. */ changedHere?: boolean; /** Drop this target's model choice (model and class together). */ resetToInherited?(): void; /** An explicit "no override" choice at the top of the list, in the host's words. */ emptyChoice?: { label: string; choose(): void; }; /** The person's local models (a chat host's port). */ localModels?: LocalModelsPort | undefined; /** The picker's cloud-access rules for this conversation (blocked cloud models, own-key labels). */ access?: ModelAccessPort | undefined; disabled?: boolean; /** Said on the read-only picker when `disabled`. */ disabledTitle?: string; /** "This message only": while on, a pick applies to the next message, then goes back. */ oneMessage?: { on: boolean; change(next: boolean): void; }; } /** The frontend input capabilities (ui gates) shown under the settings groups. */ interface CapabilitiesWriteTarget { values: UiGates; change(key: UiGateEditableKey, value: boolean): void; /** Keys THIS target changed; each gets its own reset. */ changedKeys?: ReadonlySet; resetToInherited?(key: UiGateEditableKey): void; /** Said under the rows when saving the capabilities failed. */ error?: string | null; } /** * THE "sent as" line (Arman, 2026-10-24): a saved value is never changed — on a * model that takes something else, the panel shows the value as set and, under it, * what this model is actually sent. The rows come from the server's own translation * engine (`POST /ai-models/translation-preview`); this only words them. * * A row the server marks `faithful` (the same value in the provider's own words or * shape) says nothing: there is nothing to tell. */ interface TranslationRowLike { key: string; action: string; to?: unknown; converted_from?: string | null | undefined; faithful?: boolean | undefined; } /** setting key → the one line said under it ("Sent as minimal", "Not sent on this model"). */ declare function sentAsNotes(rows: readonly TranslationRowLike[] | null | undefined): Record; export { AUTO_MEANS_UNSET_KEYS, AUTO_VALUE, CATALOGUE_KEYS, EMPTY_UI_GATES, ENUM_OFF_VALUE, INTERNAL_SETTING_KEYS, NEVER_IMPLICIT_AUTO_KEYS, PARAM_ALIASES, SETTINGS_CATALOGUE, UI_GATE_EDITABLE_KEYS, UI_GATE_KEYS, buildApiPayload, buildSettingsRows, computeOverrideDiff, createSettingsStore, detectConflicts, effectiveOfferingPinOf, extraSettingKeys, extractModelDefaults, freshSettingsFor, gateEnabled, getActionForMode, isInternalSettingKey, isNeverImplicitChoice, isOffValue, isUiGateKey, isUnsetChoice, mergeEffectiveSettings, mergeVariableValues, offValueFor, parseModelControls, parseUiGates, readApiPayload, readConflictActions, readConflictSummary, readEffectiveModelId, readEffectiveSettings, readEffectiveVariableValues, readPreviewedResolution, readSettingsEntry, resolveConflicts, sanitizeSettings, sentAsNotes, settingControl, settingLabel, settingStateOf, startingValueFor, withSettingState }; export type { AgentContext, AgentSettings, AgentSettingsEntry, AgentSettingsState, AgentSource, AgentVariable, CapabilitiesWriteTarget, CatalogueEntry, CatalogueGroup, ConflictAction, ConflictItem, ConflictReason, ConflictSummary, ControlDefinition, ControlType, ExtraSettingKeys, InitializeSettingsInput, LLMParams, ModelRowTarget, ModelSettingRow, NormalizedControls, PendingModelSwitch, RenderGroup, ResolutionMode, SettingRowFacts, SettingRowRead, SettingRowView, SettingState, SettingsGroupId, SettingsModelInfo, SettingsStore, SettingsStoreConfig, SettingsWriteTarget, TranslationRowLike, UiGateEditableKey, UiGateKey, UiGates, VariableCustomComponentRef };