Reduce React Doctor diagnostics in ModelControls (#1265)

* fix: reduce React Doctor diagnostics in ModelControls (69/88)

* Move EditModeIcon and IconBadge to module scope to prevent remount on every render

EditModeIcon and IconBadge were defined as React.FC inside ModelControls
function body, producing a new component type reference on every render
and forcing React to unmount/remount subtrees. Moves both to module
scope; EditModeIcon no longer closes over editToggleIconClass since all
callers already pass an explicit className prop.
This commit is contained in:
Bohdan Triapitsyn
2026-05-14 19:12:33 +03:00
committed by GitHub
parent 5e3afa3aa7
commit 8056fae275
+218 -174
View File
@@ -191,19 +191,19 @@ const getModalityIcons = (metadata: ModelMetadata | undefined, direction: 'input
const uniqueValues = Array.from(new Set(modalityList.map((item) => normalizeModality(item)))); const uniqueValues = Array.from(new Set(modalityList.map((item) => normalizeModality(item))));
return uniqueValues const result: ModalityIcon[] = [];
.map((modality) => { for (const modality of uniqueValues) {
const definition = MODALITY_ICON_MAP[modality]; const definition = MODALITY_ICON_MAP[modality];
if (!definition) { if (!definition) {
return null; continue;
} }
return { result.push({
key: modality, key: modality,
icon: definition.icon, icon: definition.icon,
label: definition.label, label: definition.label,
} satisfies ModalityIcon; });
}) }
.filter((entry): entry is ModalityIcon => Boolean(entry)); return result;
}; };
const COMPACT_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', { const COMPACT_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', {
@@ -220,8 +220,45 @@ const CURRENCY_FORMATTER = new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2, minimumFractionDigits: 2,
}); });
const KNOWLEDGE_DATE_FORMATTER = new Intl.DateTimeFormat('en-US', { month: 'short', year: 'numeric' });
const DATE_FORMATTER = new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
const ADD_PROVIDER_ID = '__add_provider__'; const ADD_PROVIDER_ID = '__add_provider__';
const IconBadge: React.FC<{ iconName: IconComponent; label: string }> = ({ iconName, label }) => (
<span
className="flex size-5 items-center justify-center rounded-xl bg-muted/60 text-muted-foreground"
title={label}
aria-label={label}
role="img"
>
<Icon name={iconName} className="size-3.5" />
</span>
);
const EditModeIcon: React.FC<{ mode: EditPermissionMode; className?: string }> = ({ mode, className }) => {
const combinedClassName = cn(className, 'flex-shrink-0');
const modeColors = getEditModeColors(mode);
const iconColor = modeColors ? modeColors.text : 'var(--foreground)';
const iconStyle = { color: iconColor };
if (mode === 'full') {
return <Icon name="pencil-ai" className={combinedClassName} style={iconStyle} />;
}
if (mode === 'allow') {
return <Icon name="checkbox-circle" className={combinedClassName} style={iconStyle} />;
}
if (mode === 'deny') {
return <Icon name="close-circle" className={combinedClassName} style={iconStyle} />;
}
return <Icon name="question" className={combinedClassName} style={iconStyle} />;
};
const formatTokens = (value?: number | null) => { const formatTokens = (value?: number | null) => {
if (typeof value !== 'number' || Number.isNaN(value)) { if (typeof value !== 'number' || Number.isNaN(value)) {
return '—'; return '—';
@@ -266,11 +303,13 @@ const formatCompactPrice = (metadata?: ModelMetadata): string | null => {
}; };
const getCapabilityIcons = (metadata?: ModelMetadata) => { const getCapabilityIcons = (metadata?: ModelMetadata) => {
return CAPABILITY_DEFINITIONS.filter((definition) => definition.isActive(metadata)).map((definition) => ({ const result: { key: string; icon: IconComponent; label: string }[] = [];
key: definition.key, for (const definition of CAPABILITY_DEFINITIONS) {
icon: definition.icon, if (definition.isActive(metadata)) {
label: definition.label, result.push({ key: definition.key, icon: definition.icon, label: definition.label });
})); }
}
return result;
}; };
const formatKnowledge = (knowledge?: string) => { const formatKnowledge = (knowledge?: string) => {
@@ -284,7 +323,7 @@ const formatKnowledge = (knowledge?: string) => {
const monthIndex = Number.parseInt(match[2], 10) - 1; const monthIndex = Number.parseInt(match[2], 10) - 1;
const knowledgeDate = new Date(Date.UTC(year, monthIndex, 1)); const knowledgeDate = new Date(Date.UTC(year, monthIndex, 1));
if (!Number.isNaN(knowledgeDate.getTime())) { if (!Number.isNaN(knowledgeDate.getTime())) {
return new Intl.DateTimeFormat('en-US', { month: 'short', year: 'numeric' }).format(knowledgeDate); return KNOWLEDGE_DATE_FORMATTER.format(knowledgeDate);
} }
} }
@@ -301,11 +340,7 @@ const formatDate = (value?: string) => {
return value; return value;
} }
return new Intl.DateTimeFormat('en-US', { return DATE_FORMATTER.format(parsedDate);
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(parsedDate);
}; };
interface ModelControlsProps { interface ModelControlsProps {
@@ -394,10 +429,16 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen); const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
const setSettingsPage = useUIStore((state) => state.setSettingsPage); const setSettingsPage = useUIStore((state) => state.setSettingsPage);
const hiddenModels = useUIStore((state) => state.hiddenModels); const hiddenModels = useUIStore((state) => state.hiddenModels);
const collapsedProviderSet = React.useMemo( const collapsedProviderSet = React.useMemo(() => {
() => new Set(collapsedModelProviders.map((providerId) => providerId.trim()).filter(Boolean)), const result = new Set<string>();
[collapsedModelProviders] for (const providerId of collapsedModelProviders) {
); const trimmed = providerId.trim();
if (trimmed) {
result.add(trimmed);
}
}
return result;
}, [collapsedModelProviders]);
// Separate state for agent selector to avoid conflict with model selector // Separate state for agent selector to avoid conflict with model selector
const [isAgentSelectorOpen, setIsAgentSelectorOpen] = React.useState(false); const [isAgentSelectorOpen, setIsAgentSelectorOpen] = React.useState(false);
@@ -473,13 +514,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
React.useEffect(() => { React.useEffect(() => {
if (activeMobilePanel !== 'model') { if (activeMobilePanel !== 'model') {
setMobileModelQuery(''); setMobileModelQuery('');
setExpandedMobileModelKey(null);
} }
}, [activeMobilePanel]); }, [activeMobilePanel]);
React.useEffect(() => {
setExpandedMobileModelKey(null);
}, [mobileModelQuery]);
// Handle model selector close behavior (separate from agent selector) // Handle model selector close behavior (separate from agent selector)
const prevModelSelectorOpenRef = React.useRef(isModelSelectorOpen); const prevModelSelectorOpenRef = React.useRef(isModelSelectorOpen);
React.useEffect(() => { React.useEffect(() => {
@@ -552,44 +590,28 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
const sizeVariant: 'mobile' | 'vscode' | 'default' = isMobile ? 'mobile' : isVSCodeRuntime ? 'vscode' : 'default'; const sizeVariant: 'mobile' | 'vscode' | 'default' = isMobile ? 'mobile' : isVSCodeRuntime ? 'vscode' : 'default';
const buttonHeight = sizeVariant === 'mobile' ? 'h-9' : sizeVariant === 'vscode' ? 'h-6' : 'h-8'; const buttonHeight = sizeVariant === 'mobile' ? 'h-9' : sizeVariant === 'vscode' ? 'h-6' : 'h-8';
const editToggleIconClass = sizeVariant === 'mobile' ? 'h-5 w-5' : sizeVariant === 'vscode' ? 'h-4 w-4' : 'h-4 w-4'; const controlIconSize = sizeVariant === 'mobile' ? 'size-5' : sizeVariant === 'vscode' ? 'size-4' : 'size-4';
const controlIconSize = sizeVariant === 'mobile' ? 'h-5 w-5' : sizeVariant === 'vscode' ? 'h-4 w-4' : 'h-4 w-4';
const controlTextSize = isCompact ? 'typography-micro' : 'typography-meta'; const controlTextSize = isCompact ? 'typography-micro' : 'typography-meta';
const inlineGapClass = sizeVariant === 'mobile' ? 'gap-x-1' : sizeVariant === 'vscode' ? 'gap-x-2' : 'gap-x-3'; const inlineGapClass = sizeVariant === 'mobile' ? 'gap-x-1' : sizeVariant === 'vscode' ? 'gap-x-2' : 'gap-x-3';
const renderEditModeIcon = React.useCallback((mode: EditPermissionMode, iconClass = editToggleIconClass) => {
const combinedClassName = cn(iconClass, 'flex-shrink-0');
const modeColors = getEditModeColors(mode);
const iconColor = modeColors ? modeColors.text : 'var(--foreground)';
const iconStyle = { color: iconColor };
if (mode === 'full') {
return <Icon name="pencil-ai" className={combinedClassName} style={iconStyle} />;
}
if (mode === 'allow') {
return <Icon name="checkbox-circle" className={combinedClassName} style={iconStyle} />;
}
if (mode === 'deny') {
return <Icon name="close-circle" className={combinedClassName} style={iconStyle} />;
}
return <Icon name="question" className={combinedClassName} style={iconStyle} />;
}, [editToggleIconClass]);
const currentProvider = getCurrentProvider(); const currentProvider = getCurrentProvider();
const models = Array.isArray(currentProvider?.models) ? currentProvider.models : []; const models = Array.isArray(currentProvider?.models) ? currentProvider.models : [];
const visibleProviders = React.useMemo(() => { const visibleProviders = React.useMemo(() => {
return providers const result: typeof providers = [];
.map((provider) => { for (const provider of providers) {
const providerModels = Array.isArray(provider.models) ? provider.models : []; const providerModels = Array.isArray(provider.models) ? provider.models : [];
const visibleModels = providerModels.filter((model: ProviderModel) => { const visibleModels = providerModels.filter((model: ProviderModel) => {
const modelId = typeof model?.id === 'string' ? model.id : ''; const modelId = typeof model?.id === 'string' ? model.id : '';
return !hiddenModels.some( return !hiddenModels.some(
(item) => item.providerID === String(provider.id) && item.modelID === modelId (item) => item.providerID === String(provider.id) && item.modelID === modelId
); );
}); });
return { ...provider, models: visibleModels }; if (visibleModels.length > 0) {
}) result.push({ ...provider, models: visibleModels });
.filter((provider) => provider.models.length > 0); }
}
return result;
}, [providers, hiddenModels]); }, [providers, hiddenModels]);
const normalizeModelSearchValue = React.useCallback((value: string) => { const normalizeModelSearchValue = React.useCallback((value: string) => {
@@ -632,11 +654,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
if (!normalizedQuery) return true; if (!normalizedQuery) return true;
return matchesModelSearch(modelName, normalizedQuery) || matchesModelSearch(providerName, normalizedQuery); return matchesModelSearch(modelName, normalizedQuery) || matchesModelSearch(providerName, normalizedQuery);
}; };
const providersById = new Map(providers.map((p) => [p.id, p]));
let flatIndex = 0; let flatIndex = 0;
for (const { model, providerID, modelID } of favoriteModelsList) { for (const { model, providerID, modelID } of favoriteModelsList) {
const provider = providers.find((entry) => entry.id === providerID); const provider = providersById.get(providerID);
const providerName = provider?.name || providerID; const providerName = provider?.name || providerID;
const modelName = getModelDisplayName(model); const modelName = getModelDisplayName(model);
if (!matchesQuery(modelName, providerName)) { if (!matchesQuery(modelName, providerName)) {
@@ -649,7 +672,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
} }
for (const { model, providerID, modelID } of recentModelsList) { for (const { model, providerID, modelID } of recentModelsList) {
const provider = providers.find((entry) => entry.id === providerID); const provider = providersById.get(providerID);
const providerName = provider?.name || providerID; const providerName = provider?.name || providerID;
const modelName = getModelDisplayName(model); const modelName = getModelDisplayName(model);
if (!matchesQuery(modelName, providerName)) { if (!matchesQuery(modelName, providerName)) {
@@ -1123,6 +1146,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
if (!contextHydrated) { if (!contextHydrated) {
return; return;
} }
const abortController = new AbortController();
const handleAgentSwitch = async () => { const handleAgentSwitch = async () => {
try { try {
@@ -1130,7 +1154,17 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
prevAgentNameRef.current = currentAgentName; prevAgentNameRef.current = currentAgentName;
if (currentAgentName && currentSessionId) { if (currentAgentName && currentSessionId) {
await new Promise(resolve => setTimeout(resolve, 50)); await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 50);
abortController.signal.addEventListener('abort', () => {
clearTimeout(timer);
resolve();
});
});
if (abortController.signal.aborted) {
return;
}
const persistedChoice = getAgentModelForSession(currentSessionId, currentAgentName); const persistedChoice = getAgentModelForSession(currentSessionId, currentAgentName);
@@ -1152,6 +1186,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
}; };
handleAgentSwitch(); handleAgentSwitch();
return () => {
abortController.abort();
};
}, [currentAgentName, currentSessionId, getAgentModelForSession, tryApplyModelSelection, contextHydrated]); }, [currentAgentName, currentSessionId, getAgentModelForSession, tryApplyModelSelection, contextHydrated]);
React.useEffect(() => { React.useEffect(() => {
@@ -1335,18 +1373,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
return name.charAt(0).toUpperCase() + name.slice(1); return name.charAt(0).toUpperCase() + name.slice(1);
}; };
const renderIconBadge = (iconName: IconComponent, label: string, key: string) => (
<span
key={key}
className="flex h-5 w-5 items-center justify-center rounded-xl bg-muted/60 text-muted-foreground"
title={label}
aria-label={label}
role="img"
>
<Icon name={iconName} className="h-3.5 w-3.5" />
</span>
);
const toggleMobileProviderExpansion = React.useCallback((providerId: string) => { const toggleMobileProviderExpansion = React.useCallback((providerId: string) => {
setExpandedMobileProviders((prev) => { setExpandedMobileProviders((prev) => {
const next = new Set(prev); const next = new Set(prev);
@@ -1405,7 +1431,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{currentCapabilityIcons.map(({ key, icon, label }) => ( {currentCapabilityIcons.map(({ key, icon, label }) => (
<div key={key} className="flex items-center gap-1.5"> <div key={key} className="flex items-center gap-1.5">
{renderIconBadge(icon, label, `cap-${key}`)} <IconBadge key={`cap-${key}`} iconName={icon} label={label} />
<span className="typography-meta text-foreground">{label}</span> <span className="typography-meta text-foreground">{label}</span>
</div> </div>
))} ))}
@@ -1422,7 +1448,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="typography-meta text-muted-foreground/80 w-12">{t('chat.modelControls.input')}</span> <span className="typography-meta text-muted-foreground/80 w-12">{t('chat.modelControls.input')}</span>
<div className="flex gap-1"> <div className="flex gap-1">
{inputModalityIcons.map(({ key, icon, label }) => renderIconBadge(icon, `${label} input`, `input-${key}`))} {inputModalityIcons.map(({ key, icon, label }) => <IconBadge key={`input-${key}`} iconName={icon} label={`${label} input`} />)}
</div> </div>
</div> </div>
)} )}
@@ -1430,7 +1456,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="typography-meta text-muted-foreground/80 w-12">{t('chat.modelControls.output')}</span> <span className="typography-meta text-muted-foreground/80 w-12">{t('chat.modelControls.output')}</span>
<div className="flex gap-1"> <div className="flex gap-1">
{outputModalityIcons.map(({ key, icon, label }) => renderIconBadge(icon, `${label} output`, `output-${key}`))} {outputModalityIcons.map(({ key, icon, label }) => <IconBadge key={`output-${key}`} iconName={icon} label={`${label} output`} />)}
</div> </div>
</div> </div>
)} )}
@@ -1560,7 +1586,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.edit')}</span> <span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.edit')}</span>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{renderEditModeIcon(editPermissionSummary.mode, 'h-3.5 w-3.5')} <EditModeIcon mode={editPermissionSummary.mode} className="size-3.5" />
<span className="typography-meta font-medium text-foreground"> <span className="typography-meta font-medium text-foreground">
{editPermissionSummary.label} {editPermissionSummary.label}
</span> </span>
@@ -1569,7 +1595,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.bash')}</span> <span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.bash')}</span>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{renderEditModeIcon(bashPermissionSummary.mode, 'h-3.5 w-3.5')} <EditModeIcon mode={bashPermissionSummary.mode} className="size-3.5" />
<span className="typography-meta font-medium text-foreground"> <span className="typography-meta font-medium text-foreground">
{bashPermissionSummary.label} {bashPermissionSummary.label}
</span> </span>
@@ -1578,7 +1604,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.webFetch')}</span> <span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.webFetch')}</span>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{renderEditModeIcon(webfetchPermissionSummary.mode, 'h-3.5 w-3.5')} <EditModeIcon mode={webfetchPermissionSummary.mode} className="size-3.5" />
<span className="typography-meta font-medium text-foreground"> <span className="typography-meta font-medium text-foreground">
{webfetchPermissionSummary.label} {webfetchPermissionSummary.label}
</span> </span>
@@ -1592,7 +1618,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5"> <div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span> <span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span>
<Icon name="checkbox-circle" className="h-4 w-4 text-foreground" /> <Icon name="checkbox-circle" className="size-4 text-foreground" />
</div> </div>
</div> </div>
)} )}
@@ -1623,26 +1649,28 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|| matchesModelSearch(providerName, normalizedQuery); || matchesModelSearch(providerName, normalizedQuery);
}); });
const filteredProviders = visibleProviders const filteredProviders: {
.map((provider) => { provider: (typeof visibleProviders)[number];
const providerModels = Array.isArray(provider.models) ? provider.models : []; providerModels: ProviderModel[];
const matchesProvider = normalizedQuery.length === 0 matchesProvider: boolean;
? true }[] = [];
: matchesModelSearch(provider.name, normalizedQuery) || matchesModelSearch(provider.id, normalizedQuery); for (const provider of visibleProviders) {
const matchingModels = normalizedQuery.length === 0 const providerModels = Array.isArray(provider.models) ? provider.models : [];
? providerModels const matchesProvider = normalizedQuery.length === 0
: providerModels.filter((model: ProviderModel) => { ? true
const name = getModelDisplayName(model); : matchesModelSearch(provider.name, normalizedQuery) || matchesModelSearch(provider.id, normalizedQuery);
const id = typeof model.id === 'string' ? model.id : ''; const matchingModels = normalizedQuery.length === 0
return matchesModelSearch(name, normalizedQuery) || matchesModelSearch(id, normalizedQuery); ? providerModels
}); : providerModels.filter((model: ProviderModel) => {
return { const name = getModelDisplayName(model);
provider, const id = typeof model.id === 'string' ? model.id : '';
providerModels: matchesProvider && normalizedQuery.length > 0 ? providerModels : matchingModels, return matchesModelSearch(name, normalizedQuery) || matchesModelSearch(id, normalizedQuery);
matchesProvider, });
}; const resolvedModels = matchesProvider && normalizedQuery.length > 0 ? providerModels : matchingModels;
}) if (matchesProvider || resolvedModels.length > 0) {
.filter(({ matchesProvider, providerModels }) => matchesProvider || providerModels.length > 0); filteredProviders.push({ provider, providerModels: resolvedModels, matchesProvider });
}
}
const focusMobileComposer = () => { const focusMobileComposer = () => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -1724,14 +1752,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
)} )}
> >
{showProviderLogo ? ( {showProviderLogo ? (
<ProviderLogo providerId={providerId} className="mt-0.5 h-3.5 w-3.5 flex-shrink-0" /> <ProviderLogo providerId={providerId} className="mt-0.5 size-3.5 flex-shrink-0" />
) : null} ) : null}
<div className="flex min-w-0 flex-1 flex-col gap-0.5"> <div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex min-w-0 items-start gap-2"> <div className="flex min-w-0 items-start gap-2">
<span className="typography-meta font-medium text-foreground truncate"> <span className="typography-meta font-medium text-foreground truncate">
{getModelDisplayName(model)} {getModelDisplayName(model)}
</span> </span>
{isSelected ? <Icon name="check" className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" /> : null} {isSelected ? <Icon name="check" className="mt-0.5 size-4 flex-shrink-0 text-primary" /> : null}
</div> </div>
{contextText || indicatorIcons.length > 0 ? ( {contextText || indicatorIcons.length > 0 ? (
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden typography-micro text-muted-foreground"> <div className="flex min-w-0 items-center gap-1.5 overflow-hidden typography-micro text-muted-foreground">
@@ -1748,11 +1776,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{indicatorIcons.map(({ key, icon: iconName, label }) => ( {indicatorIcons.map(({ key, icon: iconName, label }) => (
<span <span
key={`meta-${providerId}-${modelId}-${key}`} key={`meta-${providerId}-${modelId}-${key}`}
className="flex h-4 w-4 flex-shrink-0 items-center justify-center text-muted-foreground" className="flex size-4 flex-shrink-0 items-center justify-center text-muted-foreground"
title={label} title={label}
aria-label={label} aria-label={label}
> >
<Icon name={iconName} className="h-3 w-3" /> <Icon name={iconName} className="size-3" />
</span> </span>
))} ))}
</div> </div>
@@ -1770,7 +1798,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
aria-label={isExpanded ? t('chat.modelControls.hideThinkingModes') : t('chat.modelControls.showThinkingModes')} aria-label={isExpanded ? t('chat.modelControls.hideThinkingModes') : t('chat.modelControls.showThinkingModes')}
> >
<span className="whitespace-nowrap">{variantLabel}</span> <span className="whitespace-nowrap">{variantLabel}</span>
{isExpanded ? <Icon name="arrow-down-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-right-s" className="h-3.5 w-3.5" />} {isExpanded ? <Icon name="arrow-down-s" className="size-3.5" /> : <Icon name="arrow-right-s" className="size-3.5" />}
</button> </button>
) : null} ) : null}
<div className="flex flex-shrink-0 items-start gap-1.5"> <div className="flex flex-shrink-0 items-start gap-1.5">
@@ -1782,7 +1810,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
toggleFavoriteModel(providerId, modelId); toggleFavoriteModel(providerId, modelId);
}} }}
className={cn( className={cn(
'model-favorite-button flex h-5 w-5 items-center justify-center hover:text-primary/80 flex-shrink-0', 'model-favorite-button flex size-5 items-center justify-center hover:text-primary/80 flex-shrink-0',
isFavoriteModel(providerId, modelId) ? 'text-primary' : 'text-muted-foreground' isFavoriteModel(providerId, modelId) ? 'text-primary' : 'text-muted-foreground'
)} )}
aria-label={isFavoriteModel(providerId, modelId) aria-label={isFavoriteModel(providerId, modelId)
@@ -1793,15 +1821,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
: t('chat.modelControls.addToFavorites')} : t('chat.modelControls.addToFavorites')}
> >
{isFavoriteModel(providerId, modelId) ? ( {isFavoriteModel(providerId, modelId) ? (
<Icon name="star-fill" className="h-4 w-4" /> <Icon name="star-fill" className="size-4" />
) : ( ) : (
<Icon name="star" className="h-4 w-4" /> <Icon name="star" className="size-4" />
)} )}
</button> </button>
</div> </div>
</div> </div>
{isExpanded && hasVariants ? ( {isExpanded && hasVariants ? (
<div className="border-t border-border/30 px-2 py-2"> <div className="border-t border-border/30 p-2">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{inlineVariantOptions.map((variantOption) => { {inlineVariantOptions.map((variantOption) => {
const isVariantSelected = variantOption === resolvedVariant || (!variantOption && !resolvedVariant); const isVariantSelected = variantOption === resolvedVariant || (!variantOption && !resolvedVariant);
@@ -1850,21 +1878,27 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div> <div>
<div className="relative"> <div className="relative">
<Icon name="search" className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" /> <Icon name="search" className="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
<Input <Input
value={mobileModelQuery} value={mobileModelQuery}
onChange={(event) => setMobileModelQuery(event.target.value)} onChange={(event) => {
setMobileModelQuery(event.target.value);
setExpandedMobileModelKey(null);
}}
placeholder={t('chat.modelControls.searchProvidersOrModels')} placeholder={t('chat.modelControls.searchProvidersOrModels')}
className="pl-7 h-9 rounded-xl border-border/40 bg-[var(--surface-elevated)] typography-meta" className="pl-7 h-9 rounded-xl border-border/40 bg-[var(--surface-elevated)] typography-meta"
/> />
{mobileModelQuery && ( {mobileModelQuery && (
<button <button
type="button" type="button"
onClick={() => setMobileModelQuery('')} onClick={() => {
setMobileModelQuery('');
setExpandedMobileModelKey(null);
}}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={t('chat.modelControls.clearSearch')} aria-label={t('chat.modelControls.clearSearch')}
> >
<Icon name="close-circle" className="h-4 w-4" /> <Icon name="close-circle" className="size-4" />
</button> </button>
)} )}
</div> </div>
@@ -1880,7 +1914,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{filteredFavorites.length > 0 && ( {filteredFavorites.length > 0 && (
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden"> <div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider"> <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
<Icon name="star-fill" className="h-3 w-3 inline-block mr-1.5 text-primary" /> <Icon name="star-fill" className="size-3 inline-block mr-1.5 text-primary" />
{t('chat.modelControls.favorites')} {t('chat.modelControls.favorites')}
</div> </div>
<div className="flex flex-col border-t border-border/30"> <div className="flex flex-col border-t border-border/30">
@@ -1898,7 +1932,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{filteredRecents.length > 0 && ( {filteredRecents.length > 0 && (
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden"> <div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider"> <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
<Icon name="time" className="h-3 w-3 inline-block mr-1.5" /> <Icon name="time" className="size-3 inline-block mr-1.5" />
{t('chat.modelControls.recent')} {t('chat.modelControls.recent')}
</div> </div>
<div className="flex flex-col border-t border-border/30"> <div className="flex flex-col border-t border-border/30">
@@ -1936,7 +1970,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ProviderLogo <ProviderLogo
providerId={provider.id} providerId={provider.id}
className="h-3.5 w-3.5" className="size-3.5"
/> />
<span className="typography-meta font-medium text-foreground"> <span className="typography-meta font-medium text-foreground">
{provider.name} {provider.name}
@@ -1946,9 +1980,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
)} )}
</div> </div>
{isExpanded ? ( {isExpanded ? (
<Icon name="arrow-down-s" className="h-3 w-3 text-muted-foreground" /> <Icon name="arrow-down-s" className="size-3 text-muted-foreground" />
) : ( ) : (
<Icon name="arrow-right-s" className="h-3 w-3 text-muted-foreground" /> <Icon name="arrow-right-s" className="size-3 text-muted-foreground" />
)} )}
</button> </button>
@@ -2012,7 +2046,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onClick={handleBack} onClick={handleBack}
className="flex items-center gap-1 rounded-lg px-1.5 py-1 typography-meta text-muted-foreground hover:bg-interactive-hover" className="flex items-center gap-1 rounded-lg px-1.5 py-1 typography-meta text-muted-foreground hover:bg-interactive-hover"
> >
<Icon name="arrow-go-back" className="h-4 w-4" /> <Icon name="arrow-go-back" className="size-4" />
<span>{t('onboarding.common.actions.back')}</span> <span>{t('onboarding.common.actions.back')}</span>
</button> </button>
<h2 className="typography-ui-label font-semibold text-foreground">{t('chat.modelControls.thinking')}</h2> <h2 className="typography-ui-label font-semibold text-foreground">{t('chat.modelControls.thinking')}</h2>
@@ -2031,7 +2065,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onClick={() => handleSelect(undefined)} onClick={() => handleSelect(undefined)}
> >
<span className="typography-meta font-medium text-foreground">{t('chat.modelControls.default')}</span> <span className="typography-meta font-medium text-foreground">{t('chat.modelControls.default')}</span>
{isDefault && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />} {isDefault && <Icon name="check" className="size-4 text-primary flex-shrink-0" />}
</button> </button>
{targetVariants.map((variant) => { {targetVariants.map((variant) => {
@@ -2050,7 +2084,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onClick={() => handleSelect(variant)} onClick={() => handleSelect(variant)}
> >
<span className="typography-meta font-medium text-foreground">{label}</span> <span className="typography-meta font-medium text-foreground">{label}</span>
{selected && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />} {selected && <Icon name="check" className="size-4 text-primary flex-shrink-0" />}
</button> </button>
); );
})} })}
@@ -2089,7 +2123,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onClick={() => handleAgentChange(agent.name)} onClick={() => handleAgentChange(agent.name)}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className={cn('h-2.5 w-2.5 rounded-full flex-shrink-0', agentColor.class)} /> <div className={cn('size-2.5 rounded-full flex-shrink-0', agentColor.class)} />
<span <span
className="typography-ui-label font-semibold" className="typography-ui-label font-semibold"
style={isSelected ? { color: `var(${agentColor.var})` } : undefined} style={isSelected ? { color: `var(${agentColor.var})` } : undefined}
@@ -2097,7 +2131,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{capitalizeAgentName(agent.name)} {capitalizeAgentName(agent.name)}
</span> </span>
{isSelected && ( {isSelected && (
<Icon name="check" className="h-4 w-4 text-primary ml-auto flex-shrink-0" /> <Icon name="check" className="size-4 text-primary ml-auto flex-shrink-0" />
)} )}
</div> </div>
{agent.description && ( {agent.description && (
@@ -2128,7 +2162,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex flex-wrap items-center gap-1.5"> <div className="flex flex-wrap items-center gap-1.5">
{currentCapabilityIcons.length > 0 ? ( {currentCapabilityIcons.length > 0 ? (
currentCapabilityIcons.map(({ key, icon, label }) => currentCapabilityIcons.map(({ key, icon, label }) =>
renderIconBadge(icon, label, `cap-${key}`) <IconBadge key={`cap-${key}`} iconName={icon} label={label} />
) )
) : ( ) : (
<span className="typography-meta text-muted-foreground">{t('chat.modelControls.modeValue.none')}</span> <span className="typography-meta text-muted-foreground">{t('chat.modelControls.modeValue.none')}</span>
@@ -2143,9 +2177,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{inputModalityIcons.length > 0 {inputModalityIcons.length > 0
? inputModalityIcons.map(({ key, icon, label }) => ? inputModalityIcons.map(({ key, icon, label }) =>
renderIconBadge(icon, `${label} input`, `input-${key}`) <IconBadge key={`input-${key}`} iconName={icon} label={`${label} input`} />
) )
: <span className="typography-meta text-muted-foreground"></span>} : <span className="typography-meta text-muted-foreground">-</span>}
</div> </div>
</div> </div>
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
@@ -2153,9 +2187,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{outputModalityIcons.length > 0 {outputModalityIcons.length > 0
? outputModalityIcons.map(({ key, icon, label }) => ? outputModalityIcons.map(({ key, icon, label }) =>
renderIconBadge(icon, `${label} output`, `output-${key}`) <IconBadge key={`output-${key}`} iconName={icon} label={`${label} output`} />
) )
: <span className="typography-meta text-muted-foreground"></span>} : <span className="typography-meta text-muted-foreground">-</span>}
</div> </div>
</div> </div>
</div> </div>
@@ -2266,12 +2300,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{indicatorIcons.map(({ id, icon: iconName, label }) => ( {indicatorIcons.map(({ id, icon: iconName, label }) => (
<span <span
key={id} key={id}
className="flex h-3.5 w-3.5 items-center justify-center text-muted-foreground" className="flex size-3.5 items-center justify-center text-muted-foreground"
aria-label={label} aria-label={label}
role="img" role="img"
title={label} title={label}
> >
<Icon name={iconName} className="h-2.5 w-2.5" /> <Icon name={iconName} className="size-2.5" />
</span> </span>
))} ))}
</div> </div>
@@ -2308,11 +2342,19 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div <div
key={`${keyPrefix}-${providerID}-${modelID}`} key={`${keyPrefix}-${providerID}-${modelID}`}
ref={(el) => { modelItemRefs.current[flatIndex] = el; }} ref={(el) => { modelItemRefs.current[flatIndex] = el; }}
role="option"
aria-selected={isSelected}
className={cn( className={cn(
"typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer", "typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
isHighlighted ? "bg-interactive-selection" : "hover:bg-interactive-hover/50" isHighlighted ? "bg-interactive-selection" : "hover:bg-interactive-hover/50"
)} )}
onClick={() => handleProviderAndModelChange(providerID, modelID)} onClick={() => handleProviderAndModelChange(providerID, modelID)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleProviderAndModelChange(providerID, modelID);
}
}}
onMouseEnter={handlePointerActivity} onMouseEnter={handlePointerActivity}
onMouseMove={handlePointerActivity} onMouseMove={handlePointerActivity}
> >
@@ -2326,16 +2368,16 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
}} }}
className="model-favorite-drag-handle flex h-4 w-4 flex-shrink-0 items-center justify-center text-muted-foreground hover:text-foreground" className="model-favorite-drag-handle flex size-4 flex-shrink-0 items-center justify-center text-muted-foreground hover:text-foreground"
aria-label={t('chat.modelControls.reorderFavoriteAria')} aria-label={t('chat.modelControls.reorderFavoriteAria')}
title={t('chat.modelControls.reorderFavoriteTitle')} title={t('chat.modelControls.reorderFavoriteTitle')}
> >
<Icon name="draggable" className="h-3.5 w-3.5" /> <Icon name="draggable" className="size-3.5" />
</button> </button>
) : null} ) : null}
<div className="flex items-center gap-1.5 flex-1 min-w-0"> <div className="flex items-center gap-1.5 flex-1 min-w-0">
{showProviderLogo && ( {showProviderLogo && (
<ProviderLogo providerId={providerID} className="h-3.5 w-3.5 flex-shrink-0" /> <ProviderLogo providerId={providerID} className="size-3.5 flex-shrink-0" />
)} )}
<span className="font-medium truncate"> <span className="font-medium truncate">
{getModelDisplayName(model)} {getModelDisplayName(model)}
@@ -2370,7 +2412,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
</div> </div>
) : null} ) : null}
{isSelected && ( {isSelected && (
<Icon name="check" className="h-4 w-4 text-primary" /> <Icon name="check" className="size-4 text-primary" />
)} )}
<button <button
onClick={(e) => { onClick={(e) => {
@@ -2379,7 +2421,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
toggleFavoriteModel(providerID, modelID); toggleFavoriteModel(providerID, modelID);
}} }}
className={cn( className={cn(
"model-favorite-button flex h-4 w-4 items-center justify-center hover:text-primary/80", "model-favorite-button flex size-4 items-center justify-center hover:text-primary/80",
isFavorite ? "text-primary" : "text-muted-foreground" isFavorite ? "text-primary" : "text-muted-foreground"
)} )}
aria-label={isFavorite aria-label={isFavorite
@@ -2390,9 +2432,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
: t('chat.modelControls.addToFavorites')} : t('chat.modelControls.addToFavorites')}
> >
{isFavorite ? ( {isFavorite ? (
<Icon name="star-fill" className="h-3.5 w-3.5" /> <Icon name="star-fill" className="size-3.5" />
) : ( ) : (
<Icon name="star" className="h-3.5 w-3.5" /> <Icon name="star" className="size-3.5" />
)} )}
</button> </button>
</div> </div>
@@ -2429,16 +2471,17 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
return filterByQuery(modelName, providerName, desktopModelQuery); return filterByQuery(modelName, providerName, desktopModelQuery);
}); });
const filteredProviders = visibleProviders const filteredProviders: typeof visibleProviders = [];
.map((provider) => { for (const provider of visibleProviders) {
const providerModels = Array.isArray(provider.models) ? provider.models : []; const providerModels = Array.isArray(provider.models) ? provider.models : [];
const filteredModels = providerModels.filter((model: ProviderModel) => { const filteredModels = providerModels.filter((model: ProviderModel) => {
const modelName = getModelDisplayName(model); const modelName = getModelDisplayName(model);
return filterByQuery(modelName, provider.name || provider.id || '', desktopModelQuery); return filterByQuery(modelName, provider.name || provider.id || '', desktopModelQuery);
}); });
return { ...provider, models: filteredModels }; if (filteredModels.length > 0) {
}) filteredProviders.push({ ...provider, models: filteredModels });
.filter((provider) => provider.models.length > 0); }
}
const providerSections = filteredProviders.map((provider) => { const providerSections = filteredProviders.map((provider) => {
const providerId = typeof provider.id === 'string' ? provider.id : ''; const providerId = typeof provider.id === 'string' ? provider.id : '';
@@ -2457,9 +2500,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
filteredRecents.length > 0 || filteredRecents.length > 0 ||
filteredProviders.length > 0; filteredProviders.length > 0;
const filteredProviderIds = filteredProviders const filteredProviderIds: string[] = [];
.map((provider) => (typeof provider.id === 'string' ? provider.id : '')) for (const provider of filteredProviders) {
.filter(Boolean); if (typeof provider.id === 'string' && provider.id) {
filteredProviderIds.push(provider.id);
}
}
const favoriteModelLookup = new Map( const favoriteModelLookup = new Map(
filteredFavorites.map(({ providerID, modelID }) => [buildModelRefKey(providerID, modelID), { providerID, modelID }]) filteredFavorites.map(({ providerID, modelID }) => [buildModelRefKey(providerID, modelID), { providerID, modelID }])
@@ -2687,7 +2733,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{/* Search Input */} {/* Search Input */}
<div className="p-2 border-b border-border/40"> <div className="p-2 border-b border-border/40">
<div className="relative"> <div className="relative">
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" /> <Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
<Input <Input
type="text" type="text"
placeholder={t('chat.modelControls.searchModels')} placeholder={t('chat.modelControls.searchModels')}
@@ -2695,7 +2741,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onChange={(e) => setDesktopModelQuery(e.target.value)} onChange={(e) => setDesktopModelQuery(e.target.value)}
onKeyDown={handleModelKeyDown} onKeyDown={handleModelKeyDown}
className="pl-8 h-8 typography-meta" className="pl-8 h-8 typography-meta"
autoFocus
/> />
</div> </div>
</div> </div>
@@ -2718,8 +2763,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
}} }}
className="typography-meta group flex items-center gap-1 rounded-md px-2 py-1.5 cursor-pointer hover:bg-interactive-hover/50" className="typography-meta group flex items-center gap-1 rounded-md px-2 py-1.5 cursor-pointer hover:bg-interactive-hover/50"
> >
<span className="flex h-4 w-4 items-center justify-center text-muted-foreground"> <span className="flex size-4 items-center justify-center text-muted-foreground">
<Icon name="add" className="h-4 w-4 -mr-0.5" /> <Icon name="add" className="size-4 -mr-0.5" />
</span> </span>
<span className="font-medium text-foreground">{t('chat.modelControls.addNewProvider')}</span> <span className="font-medium text-foreground">{t('chat.modelControls.addNewProvider')}</span>
</div> </div>
@@ -2738,7 +2783,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<DropdownMenuLabel <DropdownMenuLabel
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30" className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30"
> >
<Icon name="star-fill" className="h-4 w-4 text-primary" /> <Icon name="star-fill" className="size-4 text-primary" />
{t('chat.modelControls.favorites')} {t('chat.modelControls.favorites')}
</DropdownMenuLabel> </DropdownMenuLabel>
{favoriteSortingEnabled ? ( {favoriteSortingEnabled ? (
@@ -2788,7 +2833,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<DropdownMenuLabel <DropdownMenuLabel
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30" className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30"
> >
<Icon name="time" className="h-4 w-4" /> <Icon name="time" className="size-4" />
{t('chat.modelControls.recent')} {t('chat.modelControls.recent')}
</DropdownMenuLabel> </DropdownMenuLabel>
{filteredRecents.map(({ model, providerID, modelID }) => { {filteredRecents.map(({ model, providerID, modelID }) => {
@@ -2849,14 +2894,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<ProviderLogo <ProviderLogo
providerId={provider.id} providerId={provider.id}
className="h-4 w-4 flex-shrink-0" className="size-4 flex-shrink-0"
/> />
<span className="min-w-0 truncate">{provider.name}</span> <span className="min-w-0 truncate">{provider.name}</span>
<span className="flex h-4 w-4 flex-shrink-0 items-center justify-center text-muted-foreground"> <span className="flex size-4 flex-shrink-0 items-center justify-center text-muted-foreground">
{isExpanded ? ( {isExpanded ? (
<Icon name="arrow-down-s" className="h-4 w-4" /> <Icon name="arrow-down-s" className="size-4" />
) : ( ) : (
<Icon name="arrow-right-s" className="h-4 w-4" /> <Icon name="arrow-right-s" className="size-4" />
)} )}
</span> </span>
</div> </div>
@@ -3023,7 +3068,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.edit')}</span> <span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.edit')}</span>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{renderEditModeIcon(editPermissionSummary.mode, 'h-3.5 w-3.5')} <EditModeIcon mode={editPermissionSummary.mode} className="size-3.5" />
<span className="typography-meta font-medium text-foreground w-12"> <span className="typography-meta font-medium text-foreground w-12">
{editPermissionSummary.label} {editPermissionSummary.label}
</span> </span>
@@ -3032,7 +3077,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.bash')}</span> <span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.bash')}</span>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{renderEditModeIcon(bashPermissionSummary.mode, 'h-3.5 w-3.5')} <EditModeIcon mode={bashPermissionSummary.mode} className="size-3.5" />
<span className="typography-meta font-medium text-foreground w-12"> <span className="typography-meta font-medium text-foreground w-12">
{bashPermissionSummary.label} {bashPermissionSummary.label}
</span> </span>
@@ -3041,7 +3086,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.webFetch')}</span> <span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.webFetch')}</span>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{renderEditModeIcon(webfetchPermissionSummary.mode, 'h-3.5 w-3.5')} <EditModeIcon mode={webfetchPermissionSummary.mode} className="size-3.5" />
<span className="typography-meta font-medium text-foreground w-12"> <span className="typography-meta font-medium text-foreground w-12">
{webfetchPermissionSummary.label} {webfetchPermissionSummary.label}
</span> </span>
@@ -3052,7 +3097,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
{hasCustomPrompt && ( {hasCustomPrompt && (
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span> <span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span>
<Icon name="checkbox-circle" className="h-4 w-4 text-foreground" /> <Icon name="checkbox-circle" className="size-4 text-foreground" />
</div> </div>
)} )}
</div> </div>
@@ -3125,7 +3170,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<DropdownMenuItem className="typography-meta" onSelect={() => handleVariantSelect(undefined)}> <DropdownMenuItem className="typography-meta" onSelect={() => handleVariantSelect(undefined)}>
<div className="flex items-center justify-between gap-2 w-full min-w-0"> <div className="flex items-center justify-between gap-2 w-full min-w-0">
<span className="typography-meta font-medium text-foreground truncate min-w-0">{t('chat.modelControls.default')}</span> <span className="typography-meta font-medium text-foreground truncate min-w-0">{t('chat.modelControls.default')}</span>
{isDefault && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />} {isDefault && <Icon name="check" className="size-4 text-primary flex-shrink-0" />}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
{availableVariants.length > 0 && <DropdownMenuSeparator />} {availableVariants.length > 0 && <DropdownMenuSeparator />}
@@ -3140,7 +3185,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
> >
<div className="flex items-center justify-between gap-2 w-full min-w-0"> <div className="flex items-center justify-between gap-2 w-full min-w-0">
<span className="typography-meta font-medium text-foreground truncate min-w-0">{label}</span> <span className="typography-meta font-medium text-foreground truncate min-w-0">{label}</span>
{selected && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />} {selected && <Icon name="check" className="size-4 text-primary flex-shrink-0" />}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
); );
@@ -3213,7 +3258,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
<DropdownMenuContent align="end" alignOffset={-40} className="w-[min(280px,calc(100vw-2rem))] p-0 flex flex-col"> <DropdownMenuContent align="end" alignOffset={-40} className="w-[min(280px,calc(100vw-2rem))] p-0 flex flex-col">
<div className="p-2 border-b border-border/40"> <div className="p-2 border-b border-border/40">
<div className="relative"> <div className="relative">
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" /> <Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
<Input <Input
type="text" type="text"
placeholder={t('chat.modelControls.searchAgents')} placeholder={t('chat.modelControls.searchAgents')}
@@ -3223,7 +3268,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
e.stopPropagation(); e.stopPropagation();
}} }}
className="pl-8 h-8 typography-meta" className="pl-8 h-8 typography-meta"
autoFocus
/> />
</div> </div>
</div> </div>
@@ -3236,7 +3280,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
onSelect={() => handleAgentChange(defaultAgentName)} onSelect={() => handleAgentChange(defaultAgentName)}
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Icon name="arrow-go-back" className="h-3.5 w-3.5 text-muted-foreground" /> <Icon name="arrow-go-back" className="size-3.5 text-muted-foreground" />
<span className="font-medium">{t('chat.modelControls.resetToDefault')}</span> <span className="font-medium">{t('chat.modelControls.resetToDefault')}</span>
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>