diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index c0ae1e19..a249ee72 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -1527,8 +1527,10 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo
{attachmentsControls}
-
+
+
+
{actionButton}
diff --git a/packages/ui/src/components/chat/ModelControls.tsx b/packages/ui/src/components/chat/ModelControls.tsx index e65ff04c..d1f2440d 100644 --- a/packages/ui/src/components/chat/ModelControls.tsx +++ b/packages/ui/src/components/chat/ModelControls.tsx @@ -436,7 +436,7 @@ export const ModelControls: React.FC = ({ const editToggleIconClass = sizeVariant === 'mobile' ? 'h-5 w-5' : sizeVariant === 'vscode' ? 'h-4 w-4' : 'h-4 w-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 inlineGapClass = sizeVariant === 'mobile' ? 'gap-x-1' : sizeVariant === 'vscode' ? 'gap-x-1' : '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); diff --git a/packages/ui/src/components/chat/StatusChip.tsx b/packages/ui/src/components/chat/StatusChip.tsx index ebc10836..97d0a9eb 100644 --- a/packages/ui/src/components/chat/StatusChip.tsx +++ b/packages/ui/src/components/chat/StatusChip.tsx @@ -3,7 +3,6 @@ import { cn } from '@/lib/utils'; import { useConfigStore } from '@/stores/useConfigStore'; import { useSessionStore } from '@/stores/useSessionStore'; import { useContextStore } from '@/stores/contextStore'; -import { useIsTextTruncated } from '@/hooks/useIsTextTruncated'; import { formatEffortLabel, getAgentDisplayName, getModelDisplayName } from './mobileControlsUtils'; interface StatusChipProps { @@ -13,7 +12,6 @@ interface StatusChipProps { export const StatusChip: React.FC = ({ onClick, className }) => { const { - currentProviderId, currentModelId, currentVariant, currentAgentName, @@ -33,28 +31,35 @@ export const StatusChip: React.FC = ({ onClick, className }) => const modelLabel = getModelDisplayName(currentProvider, currentModelId); const hasEffort = getCurrentModelVariants().length > 0; const effortLabel = hasEffort ? formatEffortLabel(currentVariant) : null; - const segments = [agentLabel, modelLabel, effortLabel].filter((segment): segment is string => Boolean(segment)); - const label = segments.join(' · '); - const textRef = React.useRef(null); - const isTruncated = useIsTextTruncated(textRef, [label, currentProviderId, currentModelId, currentVariant, uiAgentName]); + const fullLabel = [agentLabel, modelLabel, effortLabel].filter(Boolean).join(' · '); return ( ); }; diff --git a/packages/ui/src/components/chat/UnifiedControlsDrawer.tsx b/packages/ui/src/components/chat/UnifiedControlsDrawer.tsx index 9e8f582c..57457db3 100644 --- a/packages/ui/src/components/chat/UnifiedControlsDrawer.tsx +++ b/packages/ui/src/components/chat/UnifiedControlsDrawer.tsx @@ -1,21 +1,38 @@ import React from 'react'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; +import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { cn } from '@/lib/utils'; import { useConfigStore } from '@/stores/useConfigStore'; import { useSessionStore } from '@/stores/useSessionStore'; import { useContextStore } from '@/stores/contextStore'; import { useUIStore } from '@/stores/useUIStore'; import { useModelLists } from '@/hooks/useModelLists'; -import { useIsTextTruncated } from '@/hooks/useIsTextTruncated'; import { formatEffortLabel, getAgentDisplayName, - getModelDisplayName, getQuickEffortOptions, isPrimaryMode, parseEffortVariant, } from './mobileControlsUtils'; +const MAX_QUICK_AGENTS = 3; + +const COMPACT_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', { + notation: 'compact', + maximumFractionDigits: 1, +}); + +const formatTokens = (value?: number | null) => { + if (typeof value !== 'number' || Number.isNaN(value)) { + return null; + } + if (value === 0) { + return '0'; + } + const formatted = COMPACT_NUMBER_FORMATTER.format(value); + return formatted.endsWith('.0') ? formatted.slice(0, -2) : formatted; +}; + interface UnifiedControlsDrawerProps { open: boolean; onClose: () => void; @@ -31,36 +48,6 @@ export const UnifiedControlsDrawer: React.FC = ({ onOpenModel, onOpenEffort, }) => { - const AgentRow: React.FC<{ - name: string; - isSelected: boolean; - description?: string | null; - onSelect: () => void; - }> = ({ name, isSelected, description, onSelect }) => { - const descriptionRef = React.useRef(null); - const shouldScroll = useIsTextTruncated(descriptionRef, [description, name]); - - return ( - - ); - }; const { providers, currentProviderId, @@ -71,9 +58,9 @@ export const UnifiedControlsDrawer: React.FC = ({ setProvider, setModel, setCurrentVariant, - getCurrentProvider, getCurrentModelVariants, getVisibleAgents, + getModelMetadata, } = useConfigStore(); const { addRecentModel, addRecentAgent, addRecentEffort, recentAgents, recentEfforts } = useUIStore(); const { recentModelsList } = useModelLists(); @@ -100,14 +87,14 @@ export const UnifiedControlsDrawer: React.FC = ({ const quickAgentNames = React.useMemo(() => { const fallback = primaryAgents.length > 0 ? primaryAgents.map((agent) => agent.name) : agents.map((agent) => agent.name); - const base = fallback.slice(0, 4); + const base = fallback.slice(0, MAX_QUICK_AGENTS); const orderedRecents = recentAgentNames.slice().reverse(); for (const recent of orderedRecents) { if (!recent || base.includes(recent)) { continue; } base.unshift(recent); - base.splice(4); + base.splice(MAX_QUICK_AGENTS); } if (uiAgentName && !base.includes(uiAgentName)) { if (base.length > 0) { @@ -121,20 +108,25 @@ export const UnifiedControlsDrawer: React.FC = ({ const hasAgentOverflow = agents.some((agent) => !quickAgentNames.includes(agent.name)); - const currentProvider = getCurrentProvider(); - const currentModelLabel = getModelDisplayName(currentProvider, currentModelId); - const recentModels = recentModelsList.slice(0, 4); - const hasCurrentInRecents = recentModels.some( + const recentModelsBase = recentModelsList.slice(0, 4); + const hasCurrentInRecents = recentModelsBase.some( (entry) => entry.providerID === currentProviderId && entry.modelID === currentModelId ); - const currentModelRow = currentProviderId && currentModelId - ? { - providerID: currentProviderId, - modelID: currentModelId, - providerName: currentProvider?.name || currentProviderId, - modelName: currentModelLabel, + // If current model not in recents, prepend it so it's always visible + const recentModels = React.useMemo(() => { + if (hasCurrentInRecents || !currentProviderId || !currentModelId) { + return recentModelsBase; } - : null; + const currentProvider = providers.find((p) => p.id === currentProviderId); + const currentModel = currentProvider?.models?.find((m) => m.id === currentModelId); + if (!currentModel) { + return recentModelsBase; + } + return [ + { providerID: currentProviderId, modelID: currentModelId, provider: currentProvider, model: currentModel }, + ...recentModelsBase.slice(0, 3), + ]; + }, [recentModelsBase, hasCurrentInRecents, currentProviderId, currentModelId, providers]); const variants = getCurrentModelVariants(); const hasEffort = variants.length > 0; @@ -221,100 +213,92 @@ export const UnifiedControlsDrawer: React.FC = ({
Agent
- {agents.length === 0 ? ( -
- No agents configured -
- ) : ( -
- {quickAgentNames.map((agentName) => { - const agent = agents.find((entry) => entry.name === agentName); +
+ {agents.length === 0 ? ( +
+ No agents configured +
+ ) : ( + quickAgentNames.map((agentName) => { const displayName = getAgentDisplayName(agents, agentName); + const isSelected = agentName === uiAgentName; return ( - handleAgentSelect(agentName)} - /> + type="button" + onClick={() => handleAgentSelect(agentName)} + className={cn( + 'flex min-h-[44px] w-full items-center border-b border-border/30 px-3 py-2 text-left last:border-b-0', + isSelected ? 'bg-primary/10' : '' + )} + aria-pressed={isSelected} + > + + {displayName} + + ); - })} - {hasAgentOverflow && ( - - )} -
- )} + }) + )} + {hasAgentOverflow && ( + + )} +
Model
-
- {!hasCurrentInRecents && currentModelRow && ( - +
+ {recentModels.length === 0 && !hasCurrentInRecents && ( +
+ No recent models +
)} - - {recentModels.map(({ providerID, modelID, provider, model }) => { + {recentModels.map(({ providerID, modelID, model }) => { const isSelected = providerID === currentProviderId && modelID === currentModelId; const modelName = typeof model?.name === 'string' && model.name.trim().length > 0 ? model.name : modelID; + const metadata = getModelMetadata(providerID, modelID); + const ctxTokens = formatTokens(metadata?.limit?.context); + const outTokens = formatTokens(metadata?.limit?.output); return ( ); })} - - {recentModels.length === 0 && !currentModelRow && ( -
- Not selected -
- )} -