diff --git a/apps/desktop/src/app/shell/model-menu-panel.test.tsx b/apps/desktop/src/app/shell/model-menu-panel.test.tsx index e68d3b99a07..7da7c4af3d3 100644 --- a/apps/desktop/src/app/shell/model-menu-panel.test.tsx +++ b/apps/desktop/src/app/shell/model-menu-panel.test.tsx @@ -178,7 +178,7 @@ describe('ModelMenuPanel provider collapse', () => { }) }) - it('auto-expands the active provider even when collapsed', async () => { + it('collapses the active provider too (no forced auto-expand)', async () => { $currentProvider.set('deepseek') $currentModel.set('deepseek-v4-pro') const { content } = renderPanel() @@ -186,8 +186,11 @@ describe('ModelMenuPanel provider collapse', () => { const header = await content.findByText('DeepSeek') fireEvent.click(header) - // Should still show models because it's the active provider - expect(content.queryByText('Deepseek V4 Pro')).not.toBeNull() + // The current provider is collapsible like any other — clicking its header + // hides its models rather than forcing them to stay open. + await vi.waitFor(() => { + expect(content.queryByText('Deepseek V4 Pro')).toBeNull() + }) }) it('bypasses collapse when search is active', async () => { diff --git a/apps/desktop/src/app/shell/model-menu-panel.tsx b/apps/desktop/src/app/shell/model-menu-panel.tsx index 883b9c18063..5fd4c9691c8 100644 --- a/apps/desktop/src/app/shell/model-menu-panel.tsx +++ b/apps/desktop/src/app/shell/model-menu-panel.tsx @@ -4,6 +4,7 @@ import { createContext, useContext, useMemo, useState } from 'react' import { useSessionView } from '@/app/chat/session-view' import { Codicon } from '@/components/ui/codicon' +import { DisclosureCaret } from '@/components/ui/disclosure-caret' import { DropdownMenuGroup, DropdownMenuItem, @@ -18,7 +19,6 @@ import { import { Skeleton } from '@/components/ui/skeleton' import type { HermesGateway } from '@/hermes' import { useI18n } from '@/i18n' -import { ChevronDown, ChevronRight } from '@/lib/icons' import { modelOptionsQueryKey, requestModelOptions } from '@/lib/model-options' import { currentPickerSelection, displayModelName, modelDisplayParts } from '@/lib/model-status-label' import { DEFAULT_REASONING_EFFORT, reasoningEffortLabel } from '@/lib/reasoning-effort' @@ -244,25 +244,22 @@ export function ModelMenuPanel({ gateway, onSelectModel, profile = 'default', re {groups.map(group => { const slug = group.provider.slug - // Collapsed when stored + no active search + not the current provider. - const collapsed = collapsedProviders.includes(slug) && !search && slug !== optionsProvider + // Collapsed when the user stored it (and not while searching, which + // spans every model regardless of collapse state). + const collapsed = collapsedProviders.includes(slug) && !search return ( { event.preventDefault() toggleCollapsedProvider(slug) }} textValue="" > - {collapsed ? ( - - ) : ( - - )} - {group.provider.name} + {group.provider.name} + {!collapsed && group.families.map(family => { diff --git a/apps/desktop/src/components/model-visibility-dialog.tsx b/apps/desktop/src/components/model-visibility-dialog.tsx index 764239021e8..995e1d9adbd 100644 --- a/apps/desktop/src/components/model-visibility-dialog.tsx +++ b/apps/desktop/src/components/model-visibility-dialog.tsx @@ -3,11 +3,14 @@ import { useQuery } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { Button } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { DisclosureCaret } from '@/components/ui/disclosure-caret' import { GlyphSpinner } from '@/components/ui/glyph-spinner' import { Switch } from '@/components/ui/switch' import type { HermesGateway } from '@/hermes' import { useI18n } from '@/i18n' +import { Search } from '@/lib/icons' import { modelOptionsQueryKey, requestModelOptions } from '@/lib/model-options' import { displayModelName, modelDisplayParts } from '@/lib/model-status-label' import { normalize } from '@/lib/text' @@ -16,9 +19,11 @@ import { collapseModelFamilies, effectiveVisibleKeys, modelVisibilityKey, + setProviderVisibility, setVisibleModels, toggleModelVisibility } from '@/store/model-visibility' +import { $collapsedProviders, toggleCollapsedProvider } from '@/store/provider-collapse' import type { ModelOptionProvider, ModelOptionsResponse } from '@/types/hermes' interface ModelVisibilityDialogProps { @@ -42,6 +47,7 @@ export function ModelVisibilityDialog({ const copy = t.modelVisibility const [search, setSearch] = useState('') const stored = useStore($visibleModels) + const collapsedProviders = useStore($collapsedProviders) const modelOptions = useQuery({ queryKey: modelOptionsQueryKey(profile, sessionId), @@ -60,6 +66,10 @@ export function ModelVisibilityDialog({ setVisibleModels(toggleModelVisibility($visibleModels.get(), providers, provider.slug, model)) } + const toggleProvider = (provider: ModelOptionProvider, next: boolean) => { + setVisibleModels(setProviderVisibility($visibleModels.get(), providers, provider.slug, next)) + } + const q = normalize(search) const matches = (provider: ModelOptionProvider, model: string) => @@ -72,7 +82,8 @@ export function ModelVisibilityDialog({ {copy.title} -
+
+ + visible.has(modelVisibilityKey(provider.slug, family.id)) + ).length + const checkState = onCount === 0 ? false : onCount === allFamilies.length ? true : 'indeterminate' + + const collapsed = collapsedProviders.includes(provider.slug) && !q + return (
-
- {provider.name} +
+ + toggleProvider(provider, next !== false)} />
- {models.map(family => { - const { name, tag } = modelDisplayParts(family.id) - const key = modelVisibilityKey(provider.slug, family.id) + {!collapsed && + models.map(family => { + const { name, tag } = modelDisplayParts(family.id) + const key = modelVisibilityKey(provider.slug, family.id) - return ( - - ) - })} + return ( + + ) + })}
) })