import { memo, useCallback, useMemo, useRef, useState, type ReactElement, type ReactNode, type RefObject, } from "react"; import { useTranslation } from "react-i18next"; import { View, Text, Pressable, Keyboard, type PressableStateCallbackType, type StyleProp, type ViewStyle, } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useShallow } from "zustand/shallow"; import { Brain, ListTodo, Settings2, ShieldCheck, Zap } from "lucide-react-native"; import { DropdownTrigger } from "@/components/ui/dropdown-trigger"; import { ComboboxTrigger } from "@/components/ui/combobox-trigger"; import { getProviderIcon } from "@/components/provider-icons"; import { CombinedModelSelector } from "@/components/combined-model-selector"; import { buildProviderSelectorProviders, buildSelectableProviderSelectorProviders, type ProviderSelectorProvider, } from "@/provider-selection/provider-selection"; import { useSessionStore } from "@/stores/session-store"; import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; import { resolveProviderDefinition } from "@/utils/provider-definitions"; import { buildFavoriteModelKey, mergeProviderPreferences, toggleFavoriteModel, useFormPreferences, } from "@/hooks/use-form-preferences"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox"; import { DraftAgentModeControl, AgentModeControl } from "@/composer/agent-controls/mode-control"; import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import type { AgentFeature, AgentMode, AgentModelDefinition, AgentProvider, } from "@getpaseo/protocol/agent-types"; import type { AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest"; import { getFeatureHighlightColor, getFeatureTooltip, getAgentControlHintKey, formatThinkingOptionLabel, resolveAgentModelSelection, } from "@/composer/agent-controls/utils"; import { useIsCompactFormFactor } from "@/constants/layout"; import { useToast } from "@/contexts/toast-context"; import { toErrorMessage } from "@/utils/error-messages"; import { showProviderNoticeToast } from "@/utils/provider-notice-toast"; interface AgentControlOption { id: string; label: string; } type AgentControlSelector = "provider" | "mode" | "model" | "thinking" | `feature-${string}`; interface ControlledAgentControlsProps { provider: string; providerOptions?: AgentControlOption[]; selectedProviderId?: string; onSelectProvider?: (providerId: string) => void; modelOptions?: AgentControlOption[]; selectedModelId?: string; onSelectModel?: (modelId: string) => void; onSelectProviderAndModel?: (provider: string, modelId: string) => void; thinkingOptions?: AgentControlOption[]; selectedThinkingOptionId?: string; onSelectThinkingOption?: (thinkingOptionId: string) => void; disabled?: boolean; isModelLoading?: boolean; modelSelectorProviders?: ProviderSelectorProvider[]; favoriteKeys?: Set; onToggleFavoriteModel?: (provider: string, modelId: string) => void; features?: AgentFeature[]; onSetFeature?: (featureId: string, value: unknown) => void; onDropdownClose?: () => void; onModelSelectorOpen?: () => void; onRetryModelProvider?: (provider: AgentProvider) => void; isRetryingModelProvider?: boolean; /** Extra elements rendered inline with the agent controls (desktop only). */ desktopExtras?: ReactNode; modelSelectorServerId?: string | null; isCompactLayout?: boolean; } export interface DraftAgentControlsProps { providerDefinitions: AgentProviderDefinition[]; selectedProvider: AgentProvider | null; onSelectProvider: (provider: AgentProvider) => void; modeOptions: AgentMode[]; selectedMode: string; onSelectMode: (modeId: string) => void; models: AgentModelDefinition[]; selectedModel: string; onSelectModel: (modelId: string) => void; isModelLoading: boolean; modelSelectorProviders: ProviderSelectorProvider[]; isAllModelsLoading: boolean; onSelectProviderAndModel: (provider: AgentProvider, modelId: string) => void; thinkingOptions: NonNullable; selectedThinkingOptionId: string; onSelectThinkingOption: (thinkingOptionId: string) => void; features?: AgentFeature[]; onSetFeature?: (featureId: string, value: unknown) => void; onDropdownClose?: () => void; onModelSelectorOpen?: () => void; onRetryModelProvider?: (provider: AgentProvider) => void; isRetryingModelProvider?: boolean; disabled?: boolean; modelSelectorServerId?: string | null; isCompactLayout?: boolean; } interface AgentControlsProps { agentId: string; serverId: string; onDropdownClose?: () => void; isCompactLayout?: boolean; } function findOptionLabel( options: AgentControlOption[] | undefined, selectedId: string | undefined, fallback: string, ) { if (!options || options.length === 0) { return fallback; } const selected = options.find((option) => option.id === selectedId); return selected?.label ?? fallback; } const FEATURE_ICONS: Record = { "list-todo": ListTodo, "shield-check": ShieldCheck, zap: Zap, }; function getFeatureIcon(icon?: string) { return (icon && FEATURE_ICONS[icon]) || Settings2; } function getFeatureIconColor( featureId: string, enabled: boolean, palette: { blue: { 400: string }; green: { 400: string }; yellow: { 400: string }; }, foregroundMuted: string, ): string { if (!enabled) { return foregroundMuted; } switch (getFeatureHighlightColor(featureId)) { case "blue": return palette.blue[400]; case "green": return palette.green[400]; case "yellow": return palette.yellow[400]; default: return foregroundMuted; } } // Mobile agent controls only — strip namespace prefix so providers like OpenCode // show "gpt-5.5" instead of "openrouter/gpt-5.5". Full label still appears in // the model picker. function shortModelLabel(label: string): string { const i = label.lastIndexOf("/"); return i === -1 ? label : label.slice(i + 1); } type ActiveSheet = "thinking" | "features" | null; function resolveHasAnyControl({ providerOptions, canSelectModel, thinkingOptions, features, hasDesktopExtras, }: { providerOptions: AgentControlOption[] | undefined; canSelectModel: boolean; thinkingOptions: AgentControlOption[] | undefined; features: AgentFeature[] | undefined; hasDesktopExtras: boolean; }) { return ( Boolean(providerOptions?.length) || canSelectModel || Boolean(thinkingOptions?.length) || Boolean(features?.length) || hasDesktopExtras ); } function toComboboxOptions(options: AgentControlOption[] | undefined): ComboboxOption[] { return (options ?? []).map((o) => ({ id: o.id, label: o.label })); } function toThinkingControlOptions(options: AgentControlOption[] | undefined): AgentControlOption[] { return (options ?? []).map((option) => ({ id: option.id, label: formatThinkingOptionLabel(option), })); } function buildFallbackModelSelectorProviders( provider: string, modelOptions: AgentControlOption[] | undefined, ): ProviderSelectorProvider[] { if (!modelOptions || modelOptions.length === 0) { return []; } return [ { id: provider, label: provider, modelSelection: { kind: "models", rows: modelOptions.map((option) => ({ favoriteKey: buildFavoriteModelKey({ provider, modelId: option.id }), provider, providerLabel: provider, modelId: option.id, modelLabel: option.label, })), }, }, ]; } function makeBadgePressableStyle( baseStyle: StyleProp, disabledStyle: StyleProp, disabled: boolean, isOpen: boolean, ) { return ({ pressed, hovered }: PressableStateCallbackType) => [ baseStyle, hovered && styles.modeBadgeHovered, (pressed || isOpen) && styles.modeBadgePressed, disabled && disabledStyle, ]; } function pickSheetModel({ nextProviderId, modelId, currentProvider, onSelectProviderAndModel, onSelectProvider, onSelectModel, }: { nextProviderId: string; modelId: string; currentProvider: string; onSelectProviderAndModel?: (provider: string, modelId: string) => void; onSelectProvider?: (providerId: string) => void; onSelectModel?: (modelId: string) => void; }) { if (onSelectProviderAndModel) { onSelectProviderAndModel(nextProviderId, modelId); return; } if (nextProviderId !== currentProvider) { onSelectProvider?.(nextProviderId); } onSelectModel?.(modelId); } function pickDesktopModel({ nextProviderId, modelId, currentProvider, onSelectModel, }: { nextProviderId: string; modelId: string; currentProvider: string; onSelectModel?: (modelId: string) => void; }) { if (nextProviderId === currentProvider) { onSelectModel?.(modelId); } } function resolveProviderIcon(provider: string) { if (provider.trim().length === 0) { return null; } return getProviderIcon(provider); } type AgentControlsSlice = { provider: string; cwd: string | null; runtimeModelId: string | null; model: string | null | undefined; features: AgentFeature[] | undefined; thinkingOptionId: string | null | undefined; lastUsage: unknown; } | null; function selectAgentControlsSlice( state: ReturnType, serverId: string, agentId: string, ): AgentControlsSlice { const currentAgent = state.sessions[serverId]?.agents?.get(agentId) ?? null; if (!currentAgent) { return null; } return { provider: currentAgent.provider, cwd: currentAgent.cwd, runtimeModelId: currentAgent.runtimeInfo?.model ?? null, model: currentAgent.model, features: currentAgent.features, thinkingOptionId: currentAgent.thinkingOptionId, lastUsage: currentAgent.lastUsage, }; } function resolveSnapshotSelectedEntry( snapshotEntries: ReturnType["entries"], agentProvider: string | undefined, ) { if (!snapshotEntries || !agentProvider) { return null; } return snapshotEntries.find((e) => e.provider === agentProvider) ?? null; } function buildAgentProviderDefinitions( agentProvider: string | undefined, snapshotEntries: ReturnType["entries"], ): AgentProviderDefinition[] { const definition = agentProvider ? resolveProviderDefinition(agentProvider, snapshotEntries) : undefined; return definition ? [definition] : []; } function buildAgentProviderModels( agentProvider: string | undefined, models: AgentModelDefinition[] | null, ): Map { const map = new Map(); if (agentProvider && models) { map.set(agentProvider, models); } return map; } function buildOpenChangeHandler( selector: AgentControlSelector, setOpenSelector: (next: AgentControlSelector | null) => void, onDropdownClose?: () => void, ) { return (nextOpen: boolean) => { setOpenSelector(nextOpen ? selector : null); if (!nextOpen) { onDropdownClose?.(); } }; } function ControlledAgentControls({ provider, providerOptions, selectedProviderId, onSelectProvider, modelOptions, selectedModelId, onSelectModel, onSelectProviderAndModel, thinkingOptions, selectedThinkingOptionId, onSelectThinkingOption, disabled = false, isModelLoading = false, modelSelectorProviders, favoriteKeys = new Set(), onToggleFavoriteModel, features, onSetFeature, onDropdownClose, onModelSelectorOpen, onRetryModelProvider, isRetryingModelProvider = false, desktopExtras, modelSelectorServerId = null, isCompactLayout, }: ControlledAgentControlsProps) { const { theme } = useUnistyles(); const { t } = useTranslation(); const isCompactFormFactor = useIsCompactFormFactor(); const isCompact = isCompactLayout ?? isCompactFormFactor; const [activeSheet, setActiveSheet] = useState(null); const [openSelector, setOpenSelector] = useState(null); const providerAnchorRef = useRef(null); const _modelAnchorRef = useRef(null); const thinkingAnchorRef = useRef(null); const canSelectProvider = Boolean( onSelectProvider && providerOptions && providerOptions.length > 0, ); const canSelectModel = Boolean(onSelectModel); const canSelectThinking = Boolean( onSelectThinkingOption && thinkingOptions && thinkingOptions.length > 0, ); const displayProvider = findOptionLabel( providerOptions, selectedProviderId, t("agentControls.provider.fallback"), ); const formattedThinkingOptions = useMemo( () => toThinkingControlOptions(thinkingOptions), [thinkingOptions], ); const displayThinking = findOptionLabel( formattedThinkingOptions, selectedThinkingOptionId, formattedThinkingOptions[0]?.label ?? t("agentControls.thinking.unknown"), ); const ProviderIcon = resolveProviderIcon(provider); const hasAnyControl = resolveHasAnyControl({ providerOptions, canSelectModel, thinkingOptions, features, hasDesktopExtras: desktopExtras !== null && desktopExtras !== undefined, }); const modelDisabled = disabled; const comboboxProviderOptions = useMemo( () => toComboboxOptions(providerOptions), [providerOptions], ); const fallbackModelSelectorProviders = useMemo( () => buildFallbackModelSelectorProviders(provider, modelOptions), [modelOptions, provider], ); const effectiveModelSelectorProviders = modelSelectorProviders ?? fallbackModelSelectorProviders; const comboboxThinkingOptions = useMemo( () => toComboboxOptions(formattedThinkingOptions), [formattedThinkingOptions], ); const renderThinkingOption = useCallback( (args: { option: ComboboxOption; selected: boolean; active: boolean; onPress: () => void }) => ( ), [theme.colors.foreground], ); const handleOpenChange = useCallback( (selector: AgentControlSelector) => buildOpenChangeHandler(selector, setOpenSelector, onDropdownClose), [onDropdownClose], ); const handleProviderPress = useCallback(() => { handleOpenChange("provider")(openSelector !== "provider"); }, [handleOpenChange, openSelector]); const handleThinkingPress = useCallback(() => { handleOpenChange("thinking")(openSelector !== "thinking"); }, [handleOpenChange, openSelector]); const handleProviderOpenChange = useMemo(() => handleOpenChange("provider"), [handleOpenChange]); const handleThinkingOpenChange = useMemo(() => handleOpenChange("thinking"), [handleOpenChange]); const handleProviderSelect = useCallback( (id: string) => onSelectProvider?.(id), [onSelectProvider], ); const handleThinkingSelect = useCallback( (id: string) => onSelectThinkingOption?.(id), [onSelectThinkingOption], ); const handleDesktopModelSelect = useCallback( (nextProviderId: string, modelId: string) => { pickDesktopModel({ nextProviderId, modelId, currentProvider: provider, onSelectModel }); }, [onSelectModel, provider], ); const providerPressableStyle = useMemo( () => makeBadgePressableStyle( styles.modeBadge, styles.disabledBadge, disabled || !canSelectProvider, openSelector === "provider", ), [canSelectProvider, disabled, openSelector], ); const thinkingPressableStyle = useMemo( () => makeBadgePressableStyle( styles.modeBadge, styles.disabledBadge, disabled || !canSelectThinking, openSelector === "thinking", ), [canSelectThinking, disabled, openSelector], ); const handleOpenSheet = useCallback((sheet: Exclude) => { Keyboard.dismiss(); setActiveSheet(sheet); }, []); const handleCloseSheet = useCallback(() => { setActiveSheet(null); }, []); const handleSelectThinkingAndClose = useCallback( (thinkingOptionId: string) => { onSelectThinkingOption?.(thinkingOptionId); setActiveSheet(null); }, [onSelectThinkingOption], ); const handleSheetModelSelect = useCallback( (nextProviderId: string, modelId: string) => { pickSheetModel({ nextProviderId, modelId, currentProvider: provider, onSelectProviderAndModel, onSelectProvider, onSelectModel, }); }, [onSelectModel, onSelectProvider, onSelectProviderAndModel, provider], ); if (!hasAnyControl) { return null; } return ( {!isCompact ? ( ) : ( )} ); } interface DesktopAgentControlsContentProps { provider: string; providerOptions?: AgentControlOption[]; selectedProviderId?: string; modelOptions?: AgentControlOption[]; selectedModelId?: string; thinkingOptions?: AgentControlOption[]; selectedThinkingOptionId?: string; features?: AgentFeature[]; onSetFeature?: (featureId: string, value: unknown) => void; onToggleFavoriteModel?: (provider: string, modelId: string) => void; onDropdownClose?: () => void; onModelSelectorOpen?: () => void; onRetryModelProvider?: (provider: AgentProvider) => void; isRetryingModelProvider: boolean; favoriteKeys: Set; disabled: boolean; isModelLoading: boolean; canSelectProvider: boolean; canSelectModel: boolean; canSelectThinking: boolean; modelSelectorProviders: ProviderSelectorProvider[]; modelDisabled: boolean; comboboxProviderOptions: ComboboxOption[]; comboboxThinkingOptions: ComboboxOption[]; displayProvider: string; displayThinking: string; openSelector: AgentControlSelector | null; providerAnchorRef: RefObject; thinkingAnchorRef: RefObject; providerPressableStyle: (state: PressableStateCallbackType) => StyleProp; thinkingPressableStyle: (state: PressableStateCallbackType) => StyleProp; handleProviderPress: () => void; handleThinkingPress: () => void; handleProviderSelect: (id: string) => void; handleThinkingSelect: (id: string) => void; handleDesktopModelSelect: (providerId: string, modelId: string) => void; handleProviderOpenChange: (open: boolean) => void; handleThinkingOpenChange: (open: boolean) => void; handleOpenChange: (selector: AgentControlSelector) => (nextOpen: boolean) => void; renderThinkingOption: (args: { option: ComboboxOption; selected: boolean; active: boolean; onPress: () => void; }) => ReactElement; extras?: ReactNode; modelSelectorServerId: string | null; } const DESKTOP_SEARCH_THRESHOLD = 6; function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) { const { theme } = useUnistyles(); const { t } = useTranslation(); const { provider, providerOptions, selectedProviderId, selectedModelId, thinkingOptions, selectedThinkingOptionId, features, onSetFeature, onToggleFavoriteModel, onDropdownClose, onModelSelectorOpen, onRetryModelProvider, isRetryingModelProvider, favoriteKeys, disabled, isModelLoading, canSelectProvider, canSelectModel, canSelectThinking, modelSelectorProviders, modelDisabled, comboboxProviderOptions, comboboxThinkingOptions, displayProvider, displayThinking, openSelector, providerAnchorRef, thinkingAnchorRef, providerPressableStyle, thinkingPressableStyle, handleProviderPress, handleThinkingPress, handleProviderSelect, handleThinkingSelect, handleDesktopModelSelect, handleProviderOpenChange, handleThinkingOpenChange, handleOpenChange, renderThinkingOption, extras, modelSelectorServerId, } = props; return ( <> {providerOptions && providerOptions.length > 0 ? ( <> {displayProvider} DESKTOP_SEARCH_THRESHOLD} open={openSelector === "provider"} onOpenChange={handleProviderOpenChange} anchorRef={providerAnchorRef} desktopPlacement="top-start" /> ) : null} {canSelectModel ? ( {t(getAgentControlHintKey("model"))} ) : null} {thinkingOptions && thinkingOptions.length > 0 ? ( <> {displayThinking} {t(getAgentControlHintKey("thinking"))} DESKTOP_SEARCH_THRESHOLD} open={openSelector === "thinking"} onOpenChange={handleThinkingOpenChange} anchorRef={thinkingAnchorRef} desktopPlacement="top-start" renderOption={renderThinkingOption} /> ) : null} {extras} {features?.map((feature) => ( ))} ); } interface SheetAgentControlsContentProps { provider: string; selectedModelId?: string; selectedThinkingOptionId?: string; features?: AgentFeature[]; onSetFeature?: (featureId: string, value: unknown) => void; onToggleFavoriteModel?: (provider: string, modelId: string) => void; onDropdownClose?: () => void; onModelSelectorOpen?: () => void; onRetryModelProvider?: (provider: AgentProvider) => void; isRetryingModelProvider: boolean; favoriteKeys: Set; disabled: boolean; isModelLoading: boolean; canSelectModel: boolean; canSelectThinking: boolean; modelSelectorProviders: ProviderSelectorProvider[]; modelDisabled: boolean; comboboxThinkingOptions: ComboboxOption[]; openSelector: AgentControlSelector | null; ProviderIcon: ReturnType | null; activeSheet: ActiveSheet; handleOpenSheet: (sheet: Exclude) => void; handleCloseSheet: () => void; handleSheetModelSelect: (providerId: string, modelId: string) => void; handleSelectThinkingAndClose: (thinkingOptionId: string) => void; handleOpenChange: (selector: AgentControlSelector) => (nextOpen: boolean) => void; renderThinkingOption: (args: { option: ComboboxOption; selected: boolean; active: boolean; onPress: () => void; }) => ReactElement; modelSelectorServerId: string | null; } function SheetAgentControlsContent(props: SheetAgentControlsContentProps) { const { theme } = useUnistyles(); const { t } = useTranslation(); const { provider, selectedModelId, selectedThinkingOptionId, features, onSetFeature, onToggleFavoriteModel, onDropdownClose, onModelSelectorOpen, onRetryModelProvider, isRetryingModelProvider, favoriteKeys, disabled, isModelLoading, canSelectModel, canSelectThinking, modelSelectorProviders, modelDisabled, comboboxThinkingOptions, openSelector, ProviderIcon, activeSheet, handleOpenSheet, handleCloseSheet, handleSheetModelSelect, handleSelectThinkingAndClose, handleOpenChange, renderThinkingOption, modelSelectorServerId, } = props; const thinkingAnchorRef = useRef(null); const hasThinking = comboboxThinkingOptions.length > 0; const hasFeatures = Boolean(features && features.length > 0); const featuresSheetHeader = useMemo( () => ({ title: t("agentControls.features.title") }), [t], ); const handleOpenThinking = useCallback(() => handleOpenSheet("thinking"), [handleOpenSheet]); const handleOpenFeatures = useCallback(() => handleOpenSheet("features"), [handleOpenSheet]); const handleThinkingSheetOpenChange = useCallback( (nextOpen: boolean) => { if (nextOpen) { handleOpenSheet("thinking"); } else { handleCloseSheet(); } }, [handleCloseSheet, handleOpenSheet], ); const renderModelTrigger = useCallback( ({ selectedModelLabel, }: { selectedModelLabel: string; onPress: () => void; disabled: boolean; isOpen: boolean; }) => ( {ProviderIcon ? ( ) : null} {shortModelLabel(selectedModelLabel)} ), [ProviderIcon, theme.iconSize.lg, theme.colors.foregroundMuted], ); const thinkingButtonStyle = makeBadgePressableStyle( styles.modeIconBadge, styles.disabledBadge, disabled || !canSelectThinking, activeSheet === "thinking", ); const featuresButtonStyle = makeBadgePressableStyle( styles.modeIconBadge, styles.disabledBadge, disabled, activeSheet === "features", ); return ( <> {canSelectModel ? ( ) : null} {hasThinking ? ( ) : null} {hasFeatures ? ( ) : null} {hasThinking ? ( ) : null} {(features ?? []).map((feature) => ( ))} ); } function DesktopFeatureItem({ feature, disabled, openSelector, handleOpenChange, onSetFeature, }: { feature: AgentFeature; disabled: boolean; openSelector: AgentControlSelector | null; handleOpenChange: (selector: AgentControlSelector) => (nextOpen: boolean) => void; onSetFeature?: (featureId: string, value: unknown) => void; }) { const { theme } = useUnistyles(); const featureSelector: AgentControlSelector = `feature-${feature.id}`; const handleFeatureOpenChange = useMemo( () => handleOpenChange(featureSelector), [handleOpenChange, featureSelector], ); const handleTogglePress = useCallback(() => { if (feature.type === "toggle") { onSetFeature?.(feature.id, !feature.value); } }, [feature, onSetFeature]); const handleSelectOption = useCallback( (optionId: string) => { onSetFeature?.(feature.id, optionId); }, [feature.id, onSetFeature], ); const togglePressableStyle = useCallback( ({ pressed, hovered }: PressableStateCallbackType) => [ styles.modeIconBadge, hovered && styles.modeBadgeHovered, pressed && styles.modeBadgePressed, disabled && styles.disabledBadge, ], [disabled], ); const selectPressableStyle = useCallback( ({ pressed, hovered }: PressableStateCallbackType) => [ styles.modeBadge, hovered && styles.modeBadgeHovered, (pressed || openSelector === featureSelector) && styles.modeBadgePressed, disabled && styles.disabledBadge, ], [disabled, openSelector, featureSelector], ); if (feature.type === "toggle") { const FeatureIcon = getFeatureIcon(feature.icon); return ( {getFeatureTooltip(feature)} ); } if (feature.type === "select") { const FeatureIcon = getFeatureIcon(feature.icon); const selectedOption = feature.options.find((o) => o.id === feature.value); return ( {selectedOption?.label ?? feature.label} {getFeatureTooltip(feature)} {feature.options.map((option) => ( ))} ); } return null; } function SheetFeatureItem({ feature, disabled, openSelector, handleOpenChange, onSetFeature, }: { feature: AgentFeature; disabled: boolean; openSelector: AgentControlSelector | null; handleOpenChange: (selector: AgentControlSelector) => (nextOpen: boolean) => void; onSetFeature?: (featureId: string, value: unknown) => void; }) { const { theme } = useUnistyles(); const { t } = useTranslation(); const featureSelector: AgentControlSelector = `feature-${feature.id}`; const handleFeatureOpenChange = useMemo( () => handleOpenChange(featureSelector), [handleOpenChange, featureSelector], ); const handleTogglePress = useCallback(() => { if (feature.type === "toggle") { onSetFeature?.(feature.id, !feature.value); } }, [feature, onSetFeature]); const handleSelectOption = useCallback( (optionId: string) => { onSetFeature?.(feature.id, optionId); }, [feature.id, onSetFeature], ); const togglePressableStyle = useCallback( ({ pressed }: PressableStateCallbackType) => [ styles.sheetSelect, pressed && styles.sheetSelectPressed, disabled && styles.disabledSheetSelect, ], [disabled], ); if (feature.type === "toggle") { const FeatureIcon = getFeatureIcon(feature.icon); return ( {feature.label} {feature.value ? t("agentControls.features.on") : t("agentControls.features.off")} ); } if (feature.type === "select") { const selectedOption = feature.options.find((o) => o.id === feature.value); return ( {selectedOption?.label ?? feature.label} {feature.options.map((option) => ( ))} ); } return null; } function FeatureOptionMenuItem({ option, selected, onSelect, }: { option: { id: string; label: string }; selected: boolean; onSelect: (optionId: string) => void; }) { const handleSelect = useCallback(() => { onSelect(option.id); }, [onSelect, option.id]); return ( {option.label} ); } function ThinkingComboboxOption({ option, selected, active, onPress, iconColor, }: { option: ComboboxOption; selected: boolean; active: boolean; onPress: () => void; iconColor: string; }) { const leadingSlot = useMemo(() => , [iconColor]); return ( ); } export const AgentControls = memo(function AgentControls({ agentId, serverId, onDropdownClose, isCompactLayout, }: AgentControlsProps) { const { preferences, updatePreferences } = useFormPreferences(); const agent = useSessionStore( useShallow((state) => selectAgentControlsSlice(state, serverId, agentId)), ); const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null); const toast = useToast(); const { entries: snapshotEntries, isLoading: snapshotIsLoading, isRefreshing: snapshotIsRefreshing, refresh: refreshSnapshot, refetchIfStale: refetchSnapshotIfStale, } = useProvidersSnapshot(serverId, { cwd: agent?.cwd }); const snapshotSelectedEntry = useMemo( () => resolveSnapshotSelectedEntry(snapshotEntries, agent?.provider), [snapshotEntries, agent?.provider], ); const models = snapshotSelectedEntry?.models ?? null; const selectedProviderIsLoading = snapshotSelectedEntry?.status === "loading"; const agentProviderDefinitions = useMemo( () => buildAgentProviderDefinitions(agent?.provider, snapshotEntries), [agent?.provider, snapshotEntries], ); const agentProviderModels = useMemo( () => buildAgentProviderModels(agent?.provider, models), [agent?.provider, models], ); const agentModelSelectorProviders = useMemo(() => { if (snapshotSelectedEntry) { return buildSelectableProviderSelectorProviders([snapshotSelectedEntry]); } return buildProviderSelectorProviders({ providerDefinitions: agentProviderDefinitions, modelsByProvider: agentProviderModels, }); }, [agentProviderDefinitions, agentProviderModels, snapshotSelectedEntry]); const modelSelection = resolveAgentModelSelection({ models, runtimeModelId: agent?.runtimeModelId, configuredModelId: agent?.model, explicitThinkingOptionId: agent?.thinkingOptionId, }); const modelOptions = useMemo(() => { return (models ?? []).map((model) => ({ id: model.id, label: model.label })); }, [models]); const favoriteKeys = useMemo( () => new Set( (preferences.favoriteModels ?? []).map((favorite) => buildFavoriteModelKey(favorite)), ), [preferences.favoriteModels], ); const thinkingOptions = useMemo(() => { return (modelSelection.thinkingOptions ?? []).map((option) => ({ id: option.id, label: formatThinkingOptionLabel(option), })); }, [modelSelection.thinkingOptions]); const agentProvider = agent?.provider; const activeModelId = modelSelection.activeModelId; const handleSelectModel = useCallback( (modelId: string) => { if (!client || !agentProvider) { return; } void updatePreferences((current) => mergeProviderPreferences({ preferences: current, provider: agentProvider, updates: { model: modelId, }, }), ).catch((error) => { console.warn("[AgentControls] persist model preference failed", error); }); void client.setAgentModel(agentId, modelId).catch((error) => { console.warn("[AgentControls] setAgentModel failed", error); toast.error(toErrorMessage(error)); }); }, [agentId, agentProvider, client, toast, updatePreferences], ); const handleToggleFavoriteModel = useCallback( (provider: string, modelId: string) => { void updatePreferences((current) => toggleFavoriteModel({ preferences: current, provider, modelId }), ).catch((error) => { console.warn("[AgentControls] toggle favorite model failed", error); }); }, [updatePreferences], ); const handleSelectThinkingOption = useCallback( (thinkingOptionId: string) => { if (!client || !agentProvider) { return; } if (activeModelId) { void updatePreferences((current) => mergeProviderPreferences({ preferences: current, provider: agentProvider, updates: { model: activeModelId, thinkingByModel: { [activeModelId]: thinkingOptionId, }, }, }), ).catch((error) => { console.warn("[AgentControls] persist thinking preference failed", error); }); } void client .setAgentThinkingOption(agentId, thinkingOptionId) .then((notice) => showProviderNoticeToast(toast, notice)) .catch((error) => { console.warn("[AgentControls] setAgentThinkingOption failed", error); toast.error(toErrorMessage(error)); }); }, [activeModelId, agentId, agentProvider, client, toast, updatePreferences], ); const handleSetFeature = useCallback( (featureId: string, value: unknown) => { if (!client || !agentProvider) { return; } void updatePreferences((current) => mergeProviderPreferences({ preferences: current, provider: agentProvider, updates: { featureValues: { [featureId]: value, }, }, }), ).catch((error) => { console.warn("[AgentControls] persist feature preference failed", error); }); void client.setAgentFeature(agentId, featureId, value).catch((error) => { console.warn("[AgentControls] setAgentFeature failed", error); toast.error(toErrorMessage(error)); }); }, [agentId, agentProvider, client, toast, updatePreferences], ); const handleModelSelectorOpen = useCallback(() => { refetchSnapshotIfStale(agentProvider); }, [agentProvider, refetchSnapshotIfStale]); const handleRetryModelProvider = useCallback( (provider: AgentProvider) => { void refreshSnapshot([provider]); }, [refreshSnapshot], ); const modeChip = useMemo( () => ( ), [serverId, agentId, isCompactLayout], ); if (!agent) { return null; } return ( 1 ? thinkingOptions : undefined} selectedThinkingOptionId={modelSelection.selectedThinkingId ?? undefined} onSelectThinkingOption={handleSelectThinkingOption} features={agent.features} onSetFeature={handleSetFeature} isModelLoading={snapshotIsLoading || selectedProviderIsLoading} onModelSelectorOpen={handleModelSelectorOpen} onRetryModelProvider={handleRetryModelProvider} isRetryingModelProvider={snapshotIsRefreshing} onDropdownClose={onDropdownClose} disabled={!client} desktopExtras={modeChip} modelSelectorServerId={serverId} isCompactLayout={isCompactLayout} /> ); }); export function DraftAgentControls({ providerDefinitions, selectedProvider, onSelectProvider: _onSelectProvider, modeOptions, selectedMode, onSelectMode, models, selectedModel, onSelectModel, isModelLoading: _isModelLoading, modelSelectorProviders, isAllModelsLoading, onSelectProviderAndModel, thinkingOptions, selectedThinkingOptionId, onSelectThinkingOption, features, onSetFeature, onDropdownClose, onModelSelectorOpen, onRetryModelProvider, isRetryingModelProvider = false, disabled = false, modelSelectorServerId = null, isCompactLayout, }: DraftAgentControlsProps) { const { preferences, updatePreferences } = useFormPreferences(); const isCompactFormFactor = useIsCompactFormFactor(); const isCompact = isCompactLayout ?? isCompactFormFactor; const mappedThinkingOptions = useMemo(() => { return toThinkingControlOptions(thinkingOptions); }, [thinkingOptions]); const favoriteKeys = useMemo( () => new Set( (preferences.favoriteModels ?? []).map((favorite) => buildFavoriteModelKey(favorite)), ), [preferences.favoriteModels], ); const effectiveSelectedThinkingOption = selectedThinkingOptionId || mappedThinkingOptions[0]?.id || undefined; const modelOptions = useMemo( () => models.map((model) => ({ id: model.id, label: model.label, })), [models], ); const handleToggleFavorite = useCallback( (provider: string, modelId: string) => { void updatePreferences((current) => toggleFavoriteModel({ preferences: current, provider, modelId }), ).catch((error) => { console.warn("[DraftAgentControls] toggle favorite model failed", error); }); }, [updatePreferences], ); const draftModeChip = useMemo( () => ( ), [ selectedProvider, providerDefinitions, modeOptions, selectedMode, onSelectMode, disabled, isCompactLayout, ], ); if (!isCompact) { return ( {selectedProvider ? ( 0 ? mappedThinkingOptions : undefined} selectedThinkingOptionId={effectiveSelectedThinkingOption} onSelectThinkingOption={onSelectThinkingOption} features={features} onSetFeature={onSetFeature} onDropdownClose={onDropdownClose} onRetryModelProvider={onRetryModelProvider} isRetryingModelProvider={isRetryingModelProvider} disabled={disabled} desktopExtras={draftModeChip} isCompactLayout={isCompactLayout} /> ) : null} ); } return ( 0 ? mappedThinkingOptions : undefined} selectedThinkingOptionId={effectiveSelectedThinkingOption} onSelectThinkingOption={onSelectThinkingOption} features={features} onSetFeature={onSetFeature} onModelSelectorOpen={onModelSelectorOpen} onRetryModelProvider={onRetryModelProvider} isRetryingModelProvider={isRetryingModelProvider} disabled={disabled} modelSelectorServerId={modelSelectorServerId} isCompactLayout={isCompactLayout} /> ); } const styles = StyleSheet.create((theme) => ({ container: { flexDirection: "row", alignItems: "flex-end", gap: theme.spacing[1], }, modeBadge: { height: 28, flexDirection: "row", alignItems: "center", backgroundColor: "transparent", gap: theme.spacing[1], paddingHorizontal: theme.spacing[2], borderRadius: theme.borderRadius["2xl"], }, modeIconBadge: { width: 28, height: 28, alignItems: "center", justifyContent: "center", backgroundColor: "transparent", borderRadius: theme.borderRadius.full, }, modeBadgeHovered: { backgroundColor: theme.colors.surface2, }, modeBadgePressed: { backgroundColor: theme.colors.surface0, }, disabledBadge: { opacity: 0.5, }, modeBadgeText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, }, tooltipText: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, lineHeight: theme.fontSize.sm * 1.4, }, prefsButton: { height: 28, minWidth: 0, flexShrink: 1, flexDirection: "row", alignItems: "center", gap: theme.spacing[1], paddingHorizontal: theme.spacing[2], borderRadius: theme.borderRadius["2xl"], }, prefsButtonText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, flexShrink: 1, }, sheetSection: { gap: theme.spacing[2], }, sheetSelect: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: theme.spacing[3], paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[3], borderRadius: theme.borderRadius.lg, borderWidth: 1, borderColor: theme.colors.surface2, backgroundColor: theme.colors.surface0, }, sheetSelectPressed: { backgroundColor: theme.colors.surface2, }, disabledSheetSelect: { opacity: 0.5, }, sheetSelectText: { flex: 1, color: theme.colors.foreground, fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.semibold, }, }));