// Model + reasoning/speed composer menus, including per-target recents and // favorites chrome state. Extracted from AgentComposerSettingsMenus.tsx to // keep both files under the 800-line budget. import { CheckIcon, ChevronDown, Star, ZapIcon } from "lucide-react"; import { Fragment, cloneElement, useState, type HTMLAttributes, type ReactElement } from "react"; import { translate } from "../../i18n/index"; import styles from "./AgentGUINode.styles"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Tooltip, TooltipContent, TooltipTrigger, cn } from "@tutti-os/ui-system"; import type { AgentGUIComposerSettingsVM } from "./model/agentGuiNodeTypes"; import type { AgentComposerSettingsMenuLabels } from "./model/composerSettingsMenuModel"; import { ComposerOptionInfoTooltip } from "./AgentComposerModelReasoningDropdown"; import { buildComposerModelMenuModel, type ComposerMenuOption } from "./model/composerSettingsMenuModel"; import { useComposerModelChoiceHistory } from "./controller/useComposerModelChoiceHistory"; export function AgentModelReasoningDropdown({ composerSettings, disabled = false, labels, modelHistoryTargetId, onSettingsChange }: { composerSettings: AgentGUIComposerSettingsVM; disabled?: boolean; labels: AgentComposerSettingsMenuLabels; /** * Optional test/embedding override. Omission uses the controller-projected * exact target; explicit null disables model history. */ modelHistoryTargetId?: string | null; onSettingsChange: (patch: { model?: string; reasoningEffort?: string; speed?: string; }) => void; }): React.JSX.Element { "use memo"; const [menuOpen, setMenuOpen] = useState(false); const [modelSearchQuery, setModelSearchQuery] = useState(""); const projectedModelHistory = composerSettings.modelChoiceHistory; const modelHistory = useComposerModelChoiceHistory({ targetId: modelHistoryTargetId === undefined ? (projectedModelHistory?.targetId ?? null) : modelHistoryTargetId, catalog: projectedModelHistory?.catalog ?? null }); const { enabled: modelHistoryEnabled, favoriteModelIds, recentModelIds } = modelHistory; const handleMenuOpenChange = (open: boolean): void => { if (open) { // Pick up writes from other windows and clear the previous filter. modelHistory.refreshFromStorage(); setModelSearchQuery(""); } setMenuOpen(open); }; const menu = buildComposerModelMenuModel(composerSettings, labels, { favoriteModelIds, recentModelIds, searchQuery: modelSearchQuery }); const menuDisabled = disabled || menu.disabled; // While the model list is still loading the trigger shows a placeholder // ("Default") that reads like a real selection. Surface a hover hint so the // user knows the list is still loading rather than already resolved. const isModelLoading = composerSettings.isModelOptionsLoading || composerSettings.isSettingsLoading; const applySettingsChange = (patch: { model?: string; reasoningEffort?: string; speed?: string; }): void => { onSettingsChange(patch); setMenuOpen(false); }; const applyModelSelection = (value: string): void => { modelHistory.recordRecentModel(value); applySettingsChange({ model: value }); }; const handleToggleFavoriteModel = (value: string): void => { modelHistory.toggleFavoriteModel(value); }; const toggleFavoriteModel = modelHistoryEnabled ? handleToggleFavoriteModel : undefined; const favoriteValueSet = new Set(menu.model.favoriteValues); const modelDescriptionPresentation = menu.model.optionDescriptionInline ? ("inline" as const) : ("model-tooltip" as const); const modelListEmpty = menu.model.options.length === 0 && menu.model.favoriteOptions.length === 0 && menu.model.recentOptions.length === 0; const trigger = ( ); return ( {isModelLoading ? ( // The trigger is disabled while loading, so pointer events never reach // it. Target the tooltip at a focusable wrapper span (Radix's pattern // for disabled triggers) so hover/focus reliably surfaces the hint. {trigger} {labels.loadingOptions} ) : ( {trigger} )} {menu.model.show ? ( <> {labels.modelSelectionLabel} {menu.model.plan ? ( {translate("agentHost.agentGui.composerModelPlanBadge", { name: menu.model.plan.name })} ) : null} {menu.model.searchEnabled ? (
event.stopPropagation()} > setModelSearchQuery(event.target.value)} />
) : null} {menu.model.favoriteOptions.length > 0 ? ( <> {translate("agentHost.agentGui.composerModelFavoritesGroup")} ) : null} {menu.model.recentOptions.length > 0 ? ( <> {translate("agentHost.agentGui.composerModelRecentsGroup")} ) : null} {(menu.model.favoriteOptions.length > 0 || menu.model.recentOptions.length > 0) && menu.model.options.length > 0 ? ( ) : null} {menu.model.groups.length > 0 ? ( menu.model.groups.map((group, index) => ( {group.label !== null ? ( {group.label} ) : null} )) ) : ( )} {menu.model.searchEnabled && menu.model.searchQuery && modelListEmpty ? (
{translate("agentHost.agentGui.composerModelSearchEmpty")}
) : null} ) : null} {menu.model.show && (menu.reasoning.show || menu.speed.show) ? ( ) : null} {menu.reasoning.show ? ( svg]:!ml-0.5")} data-agent-reasoning-submenu-trigger="true" > {labels.reasoningLabel} {menu.reasoning.selectedLabel} applySettingsChange({ reasoningEffort: value }) } /> ) : null} {menu.speed.show ? ( svg]:!ml-0.5")} data-agent-speed-submenu-trigger="true" > {labels.speedLabel} {menu.speed.selectedLabel} applySettingsChange({ speed: value })} /> ) : null} {menu.switchEffectHint ? ( <>
{translate("agentHost.agentGui.composerModelSwitchNextTurnHint")}
) : null}
); } // Renders a list of pick-to-apply menu items. Pointer activation applies // directly because runtime evidence showed submenu items can receive // pointerdown while Radix onSelect never fires in this embedded menu. onSelect // remains for keyboard activation and normal Radix paths. function ComposerMenuOptionItems({ options, selectedValue, descriptionPresentation = "none", tooltipsEnabled = true, favoriteValues, onToggleFavorite, onSelect }: { options: ComposerMenuOption[]; selectedValue: string; descriptionPresentation?: "inline" | "model-tooltip" | "none" | "tooltip"; tooltipsEnabled?: boolean; /** Values currently favorited; enables the per-option star toggle. */ favoriteValues?: ReadonlySet; onToggleFavorite?: (value: string) => void; onSelect: (value: string) => void; }): React.JSX.Element { return ( <> {options.map((option) => { const hasDescription = Boolean(option.description); const showInlineDescription = descriptionPresentation === "inline" && hasDescription; const showModelTooltip = descriptionPresentation === "model-tooltip"; const showTooltipDescription = descriptionPresentation === "tooltip" && hasDescription; const isFavorite = favoriteValues?.has(option.value) ?? false; const favoriteToggle = onToggleFavorite ? ( ) : null; const item = ( { if (event.button === 0 && !event.ctrlKey) { event.preventDefault(); onSelect(option.value); } }} onSelect={() => { onSelect(option.value); }} > {showModelTooltip ? ( {option.label} {option.summary && option.summary.length > 0 ? ( {option.summary.map((summary) => ( {summary} ))} ) : null} ) : ( {option.label} {showTooltipDescription && option.description ? ( ) : null} {showInlineDescription && option.description ? ( {option.description} ) : null} )} {favoriteToggle} ); return showModelTooltip ? ( {item} ) : ( item ); })} ); } function ComposerModelOptionTooltip({ children, option, tooltipsEnabled = true }: { children: ReactElement>; option: ComposerMenuOption; tooltipsEnabled?: boolean; }): React.JSX.Element { if (!tooltipsEnabled || !option.tooltip) { return children; } return ( {cloneElement(children, { "data-agent-model-option-tooltip-trigger": "true" } as Partial> & Record<"data-agent-model-option-tooltip-trigger", string>)} {option.tooltip.title} {option.tooltip.description ? ( {option.tooltip.description} ) : null} {option.tooltip.contextWindow ? ( {option.tooltip.contextWindow} ) : null} {option.tooltip.version ? ( {option.tooltip.version} ) : null} ); }