diff --git a/packages/ui/src/components/chat/ModelControls.tsx b/packages/ui/src/components/chat/ModelControls.tsx index 445c1255..2a7fa327 100644 --- a/packages/ui/src/components/chat/ModelControls.tsx +++ b/packages/ui/src/components/chat/ModelControls.tsx @@ -37,6 +37,7 @@ import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { Switch } from '@/components/ui/switch'; +import { TextLoop } from '@/components/ui/TextLoop'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs'; import { isDesktopShell } from '@/lib/desktop'; @@ -207,6 +208,28 @@ const formatCost = (value?: number | null) => { return CURRENCY_FORMATTER.format(value); }; +const formatCompactPrice = (metadata?: ModelMetadata): string | null => { + if (!metadata?.cost) { + return null; + } + + const inputCost = metadata.cost.input; + const outputCost = metadata.cost.output; + const hasInput = typeof inputCost === 'number' && Number.isFinite(inputCost); + const hasOutput = typeof outputCost === 'number' && Number.isFinite(outputCost); + + if (hasInput && hasOutput) { + return `In ${formatCost(inputCost)} ยท Out ${formatCost(outputCost)}`; + } + if (hasInput) { + return `In ${formatCost(inputCost)}`; + } + if (hasOutput) { + return `Out ${formatCost(outputCost)}`; + } + return null; +}; + const getCapabilityIcons = (metadata?: ModelMetadata) => { return CAPABILITY_DEFINITIONS.filter((definition) => definition.isActive(metadata)).map((definition) => ({ key: definition.key, @@ -1936,6 +1959,44 @@ export const ModelControls: React.FC = ({ const showProviderLogo = keyPrefix === 'fav' || keyPrefix === 'recent'; + // Build animated metadata slides for desktop + const priceText = formatCompactPrice(metadata); + const hasPrice = priceText !== null; + const hasCapabilities = indicatorIcons.length > 0; + + // Build slides array: price first, then capabilities + const slides: React.ReactNode[] = []; + if (hasPrice) { + slides.push( + + {priceText} + + ); + } + if (hasCapabilities) { + slides.push( +
+ {indicatorIcons.map(({ id, icon: Icon, label }) => ( + + + + ))} +
+ ); + } + + // Rotate metadata in interactive desktop-style pickers (web/desktop), keep VS Code static. + const supportsRotatingMetadata = !isVSCodeRuntime; + const shouldAnimate = supportsRotatingMetadata && slides.length > 1 && (isHighlighted || isSelected); + const staticSlideIndex = !supportsRotatingMetadata && hasCapabilities && hasPrice ? 1 : 0; + const staticMetadataSlide = slides[staticSlideIndex]; + return (
= ({ ) : null}
- {indicatorIcons.length > 0 && ( -
- {indicatorIcons.map(({ id, icon: Icon, label }) => ( - - - - ))} + {/* Metadata slot: animated TextLoop for desktop highlighted/selected rows, static otherwise */} + {slides.length > 0 && ( +
+ {shouldAnimate ? ( + + {slides} + + ) : ( + <> + {/* In static runtimes (VS Code), prefer capabilities over price when both exist. */} + {staticMetadataSlide} + + )}
)} {isSelected && (