diff --git a/apps/desktop/src/app/chat/composer/controls.tsx b/apps/desktop/src/app/chat/composer/controls.tsx index 8bc1a2b7cf9..b79753804c1 100644 --- a/apps/desktop/src/app/chat/composer/controls.tsx +++ b/apps/desktop/src/app/chat/composer/controls.tsx @@ -9,6 +9,7 @@ import { formatCombo } from '@/lib/keybinds/combo' import { cn } from '@/lib/utils' import type { ConversationStatus } from './hooks/use-voice-conversation' +import { ModelPill } from './model-pill' import type { ChatBarState, VoiceStatus } from './types' export const ICON_BTN = 'size-(--composer-control-size) shrink-0 rounded-md' @@ -81,6 +82,7 @@ export function ComposerControls({ return (
+ {canSteer && ( diff --git a/apps/desktop/src/app/chat/composer/model-pill.tsx b/apps/desktop/src/app/chat/composer/model-pill.tsx new file mode 100644 index 00000000000..0ea963a3628 --- /dev/null +++ b/apps/desktop/src/app/chat/composer/model-pill.tsx @@ -0,0 +1,72 @@ +import { useStore } from '@nanostores/react' + +import { Button } from '@/components/ui/button' +import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { useI18n } from '@/i18n' +import { ChevronDown } from '@/lib/icons' +import { formatModelStatusLabel } from '@/lib/model-status-label' +import { cn } from '@/lib/utils' +import { + $currentFastMode, + $currentModel, + $currentProvider, + $currentReasoningEffort, + setModelPickerOpen +} from '@/store/session' + +import type { ChatBarState } from './types' + +const PILL = cn( + 'h-(--composer-control-size) max-w-40 shrink-0 gap-1 rounded-md px-2 text-xs font-normal', + 'text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground' +) + +/** + * Composer model selector — the relocated status-bar pill. Reuses the live + * `model.options` dropdown (`modelMenuContent`) verbatim; falls back to the + * full picker when the gateway is closed and no live menu exists. + */ +export function ModelPill({ disabled, model }: { disabled: boolean; model: ChatBarState['model'] }) { + const copy = useI18n().t.shell.statusbar + const currentModel = useStore($currentModel) + const currentProvider = useStore($currentProvider) + const fastMode = useStore($currentFastMode) + const reasoningEffort = useStore($currentReasoningEffort) + + const label = ( + <> + {formatModelStatusLabel(currentModel, { fastMode, reasoningEffort })} + + + ) + const title = currentProvider ? copy.modelTitle(currentProvider, currentModel || copy.modelNone) : copy.switchModel + + if (!model.modelMenuContent) { + return ( + + ) + } + + return ( + + + + + + {model.modelMenuContent} + + + ) +} diff --git a/apps/desktop/src/app/chat/composer/types.ts b/apps/desktop/src/app/chat/composer/types.ts index 36b3b8e6d3d..6d9444a6d93 100644 --- a/apps/desktop/src/app/chat/composer/types.ts +++ b/apps/desktop/src/app/chat/composer/types.ts @@ -1,3 +1,5 @@ +import type { ReactNode } from 'react' + import type { HermesGateway } from '@/hermes' import type { ComposerAttachment } from '@/store/composer' @@ -22,6 +24,8 @@ export interface ChatBarState { canSwitch: boolean loading?: boolean quickModels?: QuickModelOption[] + /** Reused status-bar dropdown (built with gateway + selectModel upstream). */ + modelMenuContent?: ReactNode } tools: { enabled: boolean; label: string; suggestions?: ContextSuggestion[] } voice: { enabled: boolean; active: boolean } diff --git a/apps/desktop/src/app/chat/index.tsx b/apps/desktop/src/app/chat/index.tsx index c9f525653e7..63983caaa1a 100644 --- a/apps/desktop/src/app/chat/index.tsx +++ b/apps/desktop/src/app/chat/index.tsx @@ -62,6 +62,7 @@ import { threadLoadingState } from './thread-loading' interface ChatViewProps extends Omit, 'onSubmit'> { gateway: HermesGateway | null + modelMenuContent?: React.ReactNode onToggleSelectedPin: () => void onDeleteSelectedSession: () => void onCancel: () => Promise | void @@ -250,6 +251,7 @@ function ChatRuntimeBoundary({ export function ChatView({ className, gateway, + modelMenuContent, onToggleSelectedPin, onDeleteSelectedSession, onCancel, @@ -346,6 +348,7 @@ export function ChatView({ provider: currentProvider, canSwitch: gatewayOpen, loading: !gatewayOpen || (!currentModel && !currentProvider), + modelMenuContent, quickModels }, tools: { @@ -358,7 +361,7 @@ export function ChatView({ active: false } }), - [contextSuggestions, currentModel, currentProvider, gatewayOpen, quickModels] + [contextSuggestions, currentModel, currentProvider, gatewayOpen, modelMenuContent, quickModels] ) // Drop files anywhere in the conversation area, not just on the composer diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 5ff162a2ca4..e071a2a0ce6 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -859,7 +859,6 @@ export function DesktopController() { gatewayLogLines, gatewayState, inferenceStatus, - modelMenuContent, openAgents, freshDraftReady, openCommandCenterSection, @@ -981,6 +980,7 @@ export function DesktopController() { composer.addContextRefAttachment(`@url:${formatRefValue(url)}`, url)} onAttachDroppedItems={composer.attachDroppedItems} diff --git a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx index 53ce2dcc150..b9a2d715454 100644 --- a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +++ b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx @@ -1,5 +1,4 @@ import { useStore } from '@nanostores/react' -import type { ReactNode } from 'react' import { useCallback, useMemo } from 'react' import type { CommandCenterSection } from '@/app/command-center' @@ -9,7 +8,6 @@ import { useI18n } from '@/i18n' import { Activity, AlertCircle, - ChevronDown, Clock, Command, Hash, @@ -19,7 +17,6 @@ import { Zap, ZapFilled } from '@/lib/icons' -import { formatModelStatusLabel } from '@/lib/model-status-label' import type { RuntimeReadinessResult } from '@/lib/runtime-readiness' import { contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar' import { cn } from '@/lib/utils' @@ -30,16 +27,11 @@ import { $activeSessionId, $busy, $connection, - $currentFastMode, - $currentModel, - $currentProvider, - $currentReasoningEffort, $currentUsage, $sessionStartedAt, $turnStartedAt, $workingSessionIds, $yoloActive, - setModelPickerOpen, setYoloActive } from '@/store/session' import { $subagentsBySession, activeSubagentCount } from '@/store/subagents' @@ -65,7 +57,6 @@ interface StatusbarItemsOptions { gatewayLogLines: readonly string[] gatewayState: string inferenceStatus: RuntimeReadinessResult | null - modelMenuContent?: ReactNode openAgents: () => void openCommandCenterSection: (section: CommandCenterSection) => void freshDraftReady: boolean @@ -83,7 +74,6 @@ export function useStatusbarItems({ gatewayLogLines, gatewayState, inferenceStatus, - modelMenuContent, openAgents, openCommandCenterSection, freshDraftReady, @@ -97,10 +87,6 @@ export function useStatusbarItems({ const terminalTakeover = useStore($terminalTakeover) const yoloActive = useStore($yoloActive) const busy = useStore($busy) - const currentFastMode = useStore($currentFastMode) - const currentModel = useStore($currentModel) - const currentProvider = useStore($currentProvider) - const currentReasoningEffort = useStore($currentReasoningEffort) const currentUsage = useStore($currentUsage) const desktopActionTasks = useStore($desktopActionTasks) const previewServerRestartStatus = useStore($previewServerRestartStatus) @@ -416,37 +402,6 @@ export function useStatusbarItems({ title: yoloActive ? copy.yoloOn : copy.yoloOff, variant: 'action' }, - { - id: 'model-summary', - label: ( - - - {formatModelStatusLabel(currentModel, { - fastMode: currentFastMode, - reasoningEffort: currentReasoningEffort - })} - - - - ), - ...(modelMenuContent - ? { - menuAlign: 'end' as const, - menuClassName: 'w-64', - menuContent: modelMenuContent, - title: currentProvider - ? copy.modelTitle(currentProvider, currentModel || copy.modelNone) - : copy.switchModel, - variant: 'menu' as const - } - : { - onSelect: () => setModelPickerOpen(true), - title: currentProvider - ? copy.providerModelTitle(currentProvider, currentModel || copy.noModel) - : copy.openModelPicker, - variant: 'action' as const - }) - }, { className: `w-7 justify-center px-0${terminalTakeover ? ' bg-accent/55 text-foreground' : ''}`, hidden: !chatOpen, @@ -465,11 +420,6 @@ export function useStatusbarItems({ contextBar, contextUsage, copy, - currentFastMode, - currentModel, - currentProvider, - currentReasoningEffort, - modelMenuContent, sessionStartedAt, showYoloToggle, terminalTakeover,