/** * Shared provider and model field components for agent dialogs. * * Both CreateAgentDialog (local mode) and AgentInstanceEditDialog import these * instead of duplicating the picker logic. */ import * as React from "react"; import { Check, ChevronDown, Search } from "lucide-react"; import type { AcpRuntimeCatalogEntry } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; import { Input } from "@/shared/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { AUTO_MODEL_DROPDOWN_VALUE, AUTO_PROVIDER_DROPDOWN_VALUE, CUSTOM_MODEL_DROPDOWN_VALUE, CUSTOM_PROVIDER_DROPDOWN_VALUE, getDefaultLlmModelLabel, getModelSelectValue, getPersonaProviderOptions, hasPersonaModelOption, PERSONA_FIELD_CONTROL_CLASS, PERSONA_FIELD_SHELL_CLASS, providerDisplayLabel, type PersonaModelOption, } from "./agentConfigOptions"; import { MODEL_DISCOVERY_LOADING_VALUE } from "./usePersonaModelDiscovery"; import type { PersonaModelDiscoveryStatus } from "./personaModelDiscoveryStatus"; export const MODEL_NO_MODELS_VALUE = "__no_models__"; export type AgentDropdownOption = { disabled?: boolean; label: React.ReactNode; value: string; }; /** * Ensures an opened model dropdown never renders as a blank white bar. * When `options` is empty and discovery has finished, appends a single * disabled "No models found" sentinel row in place. Returns `options` * for convenient chaining in tests. */ export function appendNoModelsSentinel( options: AgentDropdownOption[], loading: boolean, ): AgentDropdownOption[] { if (options.length === 0 && !loading) { options.push({ disabled: true, label: "No models found", value: MODEL_NO_MODELS_VALUE, }); } return options; } export function resolveModelFieldStatusMessage({ discoveredModelOptions, loading, status, }: { discoveredModelOptions: readonly PersonaModelOption[] | null; loading: boolean; status: PersonaModelDiscoveryStatus | null; }): string | null { if (loading) return "Loading models..."; if (status !== null) return status.message; return discoveredModelOptions !== null ? "Saved changes take effect on the next start." : null; } function optionTestId(testId: string | undefined, value: string) { if (!testId) return undefined; return `${testId}-option-${value || "empty"}`; } export function AgentConfigTextInput({ className, usePersonaInputStyle = false, ...props }: React.ComponentProps & { usePersonaInputStyle?: boolean; }) { const input = ( ); return usePersonaInputStyle ? (
{input}
) : ( input ); } export function AgentDropdownSelect({ ariaRequired, className, disabled = false, emptyOptionsLabel = "No options available", id, onValueChange, options, placeholder = "Select", placeholderClassName, placeholderValue, searchable = false, selectedLabel, testId, value, }: { ariaRequired?: boolean; className?: string; disabled?: boolean; /** Shown when the option list is empty (not a search filter miss). */ emptyOptionsLabel?: string; id: string; onValueChange: (value: string) => void; options: readonly AgentDropdownOption[]; placeholder?: string; placeholderClassName?: string; placeholderValue?: string; /** Show a filter input above the options when the list is long. */ searchable?: boolean; selectedLabel?: React.ReactNode; testId?: string; value: string; }) { const [open, setOpen] = React.useState(false); const [query, setQuery] = React.useState(""); const selectedOption = options.find((option) => option.value === value); const isPlaceholderSelection = selectedLabel === undefined && (selectedOption === undefined || (placeholderValue !== undefined && selectedOption.value === placeholderValue)); const showSearch = searchable && options.length > 1; const filteredOptions = React.useMemo(() => { const trimmed = query.trim().toLowerCase(); if (!showSearch || trimmed === "") return options; return options.filter((option) => (typeof option.label === "string" ? option.label : option.value) .toLowerCase() .includes(trimmed), ); }, [options, query, showSearch]); function handleOpenChange(next: boolean) { setOpen(next); if (!next) setQuery(""); } return (
{showSearch ? (
) : null} {filteredOptions.length === 0 ? (

{showSearch && query.trim().length > 0 ? "No matches" : emptyOptionsLabel}

) : null} {filteredOptions.map((option) => { const selected = option.value === value; return ( ); })}
); } export function RequiredFieldLabel({ className, children, htmlFor, isRequired, }: { className?: string; children: React.ReactNode; htmlFor: string; isRequired: boolean; }) { return ( ); } export function resolveDefaultModelLabel({ defaultModelLabel, discoveredModelOptions, globalModel, isSharedCompute, }: { defaultModelLabel?: string; discoveredModelOptions: readonly PersonaModelOption[] | null; globalModel?: string; isSharedCompute: boolean; }) { return ( defaultModelLabel ?? discoveredModelOptions?.find((option) => option.id.trim() === "")?.label ?? (isSharedCompute ? "Auto (collective when available)" : getDefaultLlmModelLabel(globalModel)) ); } export function AgentModelField({ disabled, discoveredModelOptions, globalModel, allowDefaultModel = true, defaultModelLabel, disableSelectDuringDiscovery = true, keepSelectedModelValueLabel = false, id = "agent-model", isCustomModelEditing, isRequired, model, modelDiscoveryLoading, modelDiscoveryStatus, onIsCustomModelEditingChange, onModelChange, placeholder = "Select model", placeholderClassName, provider, fieldClassName, labelClassName, selectClassName, testId, useCustomSelect = false, showStatusMessage = true, showCustomModelOption = true, useChevronIcon = false, usePersonaInputStyle = false, }: { disabled: boolean; discoveredModelOptions: readonly PersonaModelOption[] | null; /** Known lower-precedence model used when this field is left empty. */ globalModel?: string; /** Hide the empty/default option when no valid lower-precedence model exists. */ allowDefaultModel?: boolean; /** Source-aware label for the empty/default option. */ defaultModelLabel?: string; /** Disable the trigger while live model discovery refreshes the option list. */ disableSelectDuringDiscovery?: boolean; /** Keep the closed trigger from swapping to discovered display labels. */ keepSelectedModelValueLabel?: boolean; /** DOM id for the model select. Defaults to `"agent-model"`. Override in * contexts where multiple instances coexist on the same page (e.g. the * global-config settings card) to avoid duplicate DOM ids. */ id?: string; isCustomModelEditing: boolean; isRequired: boolean; model: string; modelDiscoveryLoading: boolean; modelDiscoveryStatus: PersonaModelDiscoveryStatus | null; onIsCustomModelEditingChange: (value: boolean) => void; onModelChange: (value: string) => void; /** Trigger placeholder shown when there is no selected model option. */ placeholder?: string; /** Optional class override for placeholder text. */ placeholderClassName?: string; provider?: string; /** Optional class override for the field wrapper. */ fieldClassName?: string; /** Optional class override for the label. */ labelClassName?: string; /** Optional class override for contexts with custom visual treatments. */ selectClassName?: string; /** Optional test id for custom dropdown trigger/options. */ testId?: string; /** Render the polished app dropdown instead of the native select. */ useCustomSelect?: boolean; /** Hide the helper/status line in compact presentations. */ showStatusMessage?: boolean; /** Hide the explicit custom-model escape hatch in compact presentations. */ showCustomModelOption?: boolean; /** Render a controlled chevron instead of the native select indicator. */ useChevronIcon?: boolean; /** Match custom agent text inputs when this field is shown in that flow. */ usePersonaInputStyle?: boolean; }) { const trimmedModel = model.trim(); const isSharedCompute = provider?.trim() === "relay-mesh"; const discoveredDefaultOption = discoveredModelOptions?.find( (option) => option.id.trim() === "", ); // Model discovery can report the harness's current/default model while the // persisted value remains empty ("let the harness choose"). Treat that as a // real, displayable option instead of a blank select state. Explicit labels // from a lower-precedence/baked default still win when present. const defaultOption: PersonaModelOption = { id: "", label: resolveDefaultModelLabel({ defaultModelLabel, discoveredModelOptions, globalModel, isSharedCompute, }), }; const discoveredWithoutDefault = (discoveredModelOptions ?? []).filter( (option) => option.id.trim() !== "", ); const baseModelOptions = isSharedCompute ? [defaultOption, ...discoveredWithoutDefault] : [ ...(allowDefaultModel || discoveredDefaultOption ? [defaultOption] : []), ...discoveredWithoutDefault, ]; const shouldShowPendingModelOption = !isSharedCompute && !isCustomModelEditing && modelDiscoveryLoading && discoveredModelOptions === null && trimmedModel.length > 0; const effectiveModelOptions = shouldShowPendingModelOption && !baseModelOptions.some((option) => option.id === trimmedModel) ? [...baseModelOptions, { id: trimmedModel, label: trimmedModel }] : baseModelOptions; // isModelCustom: true when the current model isn't in any known option set. // We check discovered options (when available) or runtime-static options so // a previously-saved custom model stays in custom mode even before discovery. const isModelCustom = !isSharedCompute && !hasPersonaModelOption(effectiveModelOptions, trimmedModel); const modelSelectValue = isSharedCompute && (trimmedModel === "" || trimmedModel === "auto") ? AUTO_MODEL_DROPDOWN_VALUE : getModelSelectValue({ isCustomModelEditing, isModelCustom, model, }); // The select is only disabled for mutation pending — never for missing discovery. // Default/custom options remain usable regardless of discovery state. const selectDisabled = disabled || (disableSelectDuringDiscovery && modelDiscoveryLoading); // Show the custom model input whenever custom mode is active or the current // model is already custom — not gated on discovery having returned. const showCustomModelInput = !isSharedCompute && (isCustomModelEditing || isModelCustom); const handleModelSelectChange = (nextValue: string) => { if (nextValue === AUTO_MODEL_DROPDOWN_VALUE) { onIsCustomModelEditingChange(false); onModelChange(isSharedCompute ? "auto" : ""); return; } if (nextValue === CUSTOM_MODEL_DROPDOWN_VALUE) { onIsCustomModelEditingChange(true); return; } onIsCustomModelEditingChange(false); onModelChange(nextValue); }; const modelOptions: AgentDropdownOption[] = [ ...effectiveModelOptions.map((option) => ({ label: option.label, value: option.id || AUTO_MODEL_DROPDOWN_VALUE, })), ...(modelDiscoveryLoading && discoveredModelOptions === null ? [ { disabled: true, label: "Loading models...", value: MODEL_DISCOVERY_LOADING_VALUE, }, ] : []), ...(!isSharedCompute && (showCustomModelOption || modelSelectValue === CUSTOM_MODEL_DROPDOWN_VALUE) ? [{ label: "Custom model...", value: CUSTOM_MODEL_DROPDOWN_VALUE }] : []), ]; // An opened dropdown must never show a blank popover. When all the above // yields an empty list and discovery has finished, add a disabled sentinel // row so the user sees "No models found" instead of a bare white bar. appendNoModelsSentinel(modelOptions, modelDiscoveryLoading); const stableSelectedModelLabel = keepSelectedModelValueLabel && modelSelectValue === trimmedModel && trimmedModel.length > 0 ? trimmedModel : undefined; // While discovery is in flight with nothing selected, the closed field // reads "Loading models…" instead of a select-prompt — the field isn't // waiting on the user, it's waiting on the harness. const restingPlaceholder = modelDiscoveryLoading && discoveredModelOptions === null && trimmedModel.length === 0 && !isCustomModelEditing ? "Loading models..." : placeholder; const statusMessage = resolveModelFieldStatusMessage({ discoveredModelOptions, loading: modelDiscoveryLoading, status: modelDiscoveryStatus, }); const modelSelect = useCustomSelect ? ( ) : ( ); return (
Model {!useCustomSelect && useChevronIcon ? (
{modelSelect}
) : ( modelSelect )} {showCustomModelInput ? ( onModelChange(event.target.value)} placeholder="Custom model ID" usePersonaInputStyle={usePersonaInputStyle} value={model} /> ) : null} {showStatusMessage && statusMessage ? (

{statusMessage}

) : null}
); } export function AgentProviderField({ disabled, globalProvider, isCustomProviderEditing, isRequired, onProviderChange, provider, selectedRuntime, }: { disabled: boolean; globalProvider?: string; isCustomProviderEditing: boolean; isRequired: boolean; onProviderChange: (value: string) => void; provider: string; selectedRuntime: AcpRuntimeCatalogEntry | undefined; }) { const trimmedProvider = provider.trim(); const providerOptions = getPersonaProviderOptions( trimmedProvider, selectedRuntime?.id ?? "", globalProvider, ); const providerSelectValue = isCustomProviderEditing ? CUSTOM_PROVIDER_DROPDOWN_VALUE : trimmedProvider || AUTO_PROVIDER_DROPDOWN_VALUE; return (
LLM provider {isCustomProviderEditing ? ( onProviderChange(event.target.value)} placeholder="Custom provider ID" value={provider} /> ) : null}

Changing the provider updates the available model list immediately.

); }