import { components } from '@ai-matrx/api/types'; import * as React from 'react'; import { ReactNode } from 'react'; import { LocalModelsPort, ModelAccessPort } from '@ai-matrx/agents/models/react'; /** * 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; } /** * `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). */ interface InitializeSettingsInput { agentId: string; source?: AgentSource; context: AgentContext; settings?: Partial | Record | null; variable_defaults?: AgentVariable[] | null; } 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; } /** * 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; } /** * 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]; /** * 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; } interface ModelSettingsPanelProps { /** The effective model's normalized controls (`null` until a model's controls are known). */ controls: Record | null; /** The effective settings (marks `hasValue` on each catalog row; the extra rows read them). */ values: Record | null; /** Where every change lands. */ target: SettingsWriteTarget; /** Unique per mount (ids for labels and controls). */ idPrefix: string; /** THE model row, drawn as the panel's first line. */ model?: ModelRowTarget | undefined; /** Drawn between the model row and the groups: notices, the issue list. */ notices?: ReactNode; /** The input capabilities group, under the settings groups. */ capabilities?: CapabilitiesWriteTarget | undefined; /** Said where the rows would be when the model declares none (or no model is chosen). */ emptyNote?: ReactNode; disabled?: boolean; /** * setting key → what this model is sent instead of the kept value (`sentAsNotes` over the * server's translation preview). A saved value is never changed by a model switch; this line * is how the panel says what it becomes. */ sentAs?: Record | undefined; } declare function ModelSettingsPanel({ controls, values, target, idPrefix, model, notices, capabilities, emptyNote, disabled, sentAs, }: ModelSettingsPanelProps): React.JSX.Element; /** Subscribe to a slice of a settings store. `select` must return a stable value for unchanged state. */ declare function useSettingsSelector(store: SettingsStore, select: (state: AgentSettingsState) => T): T; /** One agent's settings entry (undefined until the host seeds it). */ declare function useSettingsEntry(store: SettingsStore, agentId: string | null | undefined): AgentSettingsEntry | undefined; /** Defaults merged with overrides for one agent; recomputed only when the entry changes. */ declare function useEffectiveSettings(store: SettingsStore, agentId: string | null | undefined): Partial; export { ModelSettingsPanel, useEffectiveSettings, useSettingsEntry, useSettingsSelector }; export type { ModelSettingsPanelProps };