From 80e4b8985ea971538462fe129e67ff510b3cec0a Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 16 Jun 2026 09:50:27 -0500 Subject: [PATCH] feat(desktop): tighten composer model picker interactions Clicking a model row in the composer dropdown now commits and closes the menu (via a close context); the hover-revealed reasoning/fast submenu stays open to tweak. The pill shows a quiet braille loader instead of literal "No model" until one resolves, and steer takes over the mic slot while typing into a running agent. --- .../src/app/chat/composer/controls.tsx | 8 ++++++-- .../src/app/chat/composer/model-pill.tsx | 20 ++++++++++++++++--- .../src/app/shell/model-menu-panel.tsx | 13 +++++++++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/chat/composer/controls.tsx b/apps/desktop/src/app/chat/composer/controls.tsx index b79753804c1..6d748c73b5f 100644 --- a/apps/desktop/src/app/chat/composer/controls.tsx +++ b/apps/desktop/src/app/chat/composer/controls.tsx @@ -67,6 +67,7 @@ export function ComposerControls({ const c = t.composer const steerCombo = formatCombo('mod+enter') const steerLabel = `${c.steer} (${steerCombo})` + const steerTip = ( {c.steer} @@ -83,8 +84,9 @@ export function ComposerControls({ return (
- - {canSteer && ( + {/* While the agent runs and the user is typing, steer takes over the mic's + slot rather than crowding the row with an extra button. */} + {canSteer ? ( + ) : ( + )} {showVoicePrimary ? ( diff --git a/apps/desktop/src/app/chat/composer/model-pill.tsx b/apps/desktop/src/app/chat/composer/model-pill.tsx index 0ea963a3628..f04b6e2302b 100644 --- a/apps/desktop/src/app/chat/composer/model-pill.tsx +++ b/apps/desktop/src/app/chat/composer/model-pill.tsx @@ -1,7 +1,10 @@ import { useStore } from '@nanostores/react' +import { useState } from 'react' +import { ModelMenuCloseContext } from '@/app/shell/model-menu-panel' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { GlyphSpinner } from '@/components/ui/glyph-spinner' import { useI18n } from '@/i18n' import { ChevronDown } from '@/lib/icons' import { formatModelStatusLabel } from '@/lib/model-status-label' @@ -32,13 +35,22 @@ export function ModelPill({ disabled, model }: { disabled: boolean; model: ChatB const currentProvider = useStore($currentProvider) const fastMode = useStore($currentFastMode) const reasoningEffort = useStore($currentReasoningEffort) + const [open, setOpen] = useState(false) + // The model resolves a beat after the gateway/session comes up. Rather than + // flash a literal "No model", show a quiet loader (inherits the pill text + // color at half opacity) until a model lands. const label = ( <> - {formatModelStatusLabel(currentModel, { fastMode, reasoningEffort })} + {currentModel.trim() ? ( + {formatModelStatusLabel(currentModel, { fastMode, reasoningEffort })} + ) : ( + + )} ) + const title = currentProvider ? copy.modelTitle(currentProvider, currentModel || copy.modelNone) : copy.switchModel if (!model.modelMenuContent) { @@ -58,14 +70,16 @@ export function ModelPill({ disabled, model }: { disabled: boolean; model: ChatB } return ( - + - {model.modelMenuContent} + setOpen(false)}> + {model.modelMenuContent} + ) diff --git a/apps/desktop/src/app/shell/model-menu-panel.tsx b/apps/desktop/src/app/shell/model-menu-panel.tsx index b87b1a030d1..a9795564aab 100644 --- a/apps/desktop/src/app/shell/model-menu-panel.tsx +++ b/apps/desktop/src/app/shell/model-menu-panel.tsx @@ -1,6 +1,6 @@ import { useStore } from '@nanostores/react' import { useQuery } from '@tanstack/react-query' -import { useMemo, useState } from 'react' +import { createContext, useContext, useMemo, useState } from 'react' import { Codicon } from '@/components/ui/codicon' import { @@ -41,6 +41,11 @@ import type { ModelOptionProvider, ModelOptionsResponse } from '@/types/hermes' import { ModelEditSubmenu, resolveFastControl } from './model-edit-submenu' +// Lets the host dropdown (model-pill) hand the panel a way to dismiss itself so +// clicking a model row commits + closes, while the hover-revealed edit submenu +// (reasoning/fast) stays open to play with (its items preventDefault on select). +export const ModelMenuCloseContext = createContext<() => void>(() => {}) + interface ModelMenuPanelProps { gateway?: HermesGateway onSelectModel: (selection: { model: string; provider: string }) => Promise | void @@ -55,6 +60,7 @@ interface ProviderGroup { export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: ModelMenuPanelProps) { const { t } = useI18n() const copy = t.shell.modelMenu + const closeMenu = useContext(ModelMenuCloseContext) const [search, setSearch] = useState('') // Reactive session state is read from the stores here (not drilled in), so // toggling effort/fast/model re-renders this panel in place without forcing @@ -209,10 +215,15 @@ export function ModelMenuPanel({ gateway, onSelectModel, requestGateway }: Model // restores its preset; the Fast toggle inside swaps to the -fast // sibling (or flips the speed param). The sub-trigger has no // `onSelect`, so wire both click and Enter/Space for keyboard parity. + // Clicking the row commits the model and closes the picker; the + // edit submenu (reasoning/fast) is reached by HOVER, so you can + // still tweak those without the click dismissing everything. const activate = () => { if (!isCurrent) { void selectFamily(family, group.provider) } + + closeMenu() } return (