diff --git a/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.test.tsx b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.test.tsx
index 877703609..bcf744903 100644
--- a/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.test.tsx
+++ b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.test.tsx
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, type RenderOptions, screen } from '@testing-library/react';
+import { fireEvent, render, type RenderOptions, screen } from '@testing-library/react';
import ModelsBottomBar from './ModelsBottomBar';
import { IntlTestWrapper } from '../../../../i18n/test-utils';
@@ -13,6 +13,7 @@ let mockCurrentModel: string | null = 'config-model';
let mockCurrentProvider: string | null = 'config-provider';
const mockGetProviders = vi.fn();
const mockOnModelChanged = vi.fn();
+const mockPreventCloseAutoFocus = vi.fn();
vi.mock('../../../ModelAndProviderContext', () => ({
useModelAndProvider: () => ({
@@ -41,10 +42,47 @@ vi.mock('../../../bottom_menu/BottomMenuAlertPopover', () => ({
}));
vi.mock('../../../ui/dropdown-menu', () => ({
- DropdownMenu: ({ children }: { children: React.ReactNode }) =>
{children}
,
+ DropdownMenu: ({
+ children,
+ open,
+ onOpenChange,
+ }: {
+ children: React.ReactNode;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ }) => (
+
+
+ {children}
+
+ ),
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => {children}
,
- DropdownMenuContent: ({ children }: { children: React.ReactNode }) => {children}
,
- DropdownMenuItem: ({ children }: { children: React.ReactNode }) => {children}
,
+ DropdownMenuContent: ({
+ children,
+ onCloseAutoFocus,
+ }: {
+ children: React.ReactNode;
+ onCloseAutoFocus?: (event: Pick) => void;
+ }) => (
+
+
+ {children}
+
+ ),
+ DropdownMenuItem: ({
+ children,
+ onSelect,
+ }: {
+ children: React.ReactNode;
+ onSelect?: () => void;
+ }) => ,
+ DropdownMenuSeparator: () => null,
+}));
+
+vi.mock('../subcomponents/SwitchModelModal', () => ({
+ SwitchModelModal: () => ,
}));
vi.mock('../../localInference/ModelSettingsPanel', () => ({
@@ -107,4 +145,43 @@ describe('ModelsBottomBar', () => {
expect(screen.getByText('config-model')).toBeInTheDocument();
expect(screen.queryByTestId('model-loading-state')).not.toBeInTheDocument();
});
+
+ it('opens model overlays after the menu closes with the appropriate focus behavior', () => {
+ renderWithIntl(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: 'Open model menu' }));
+ expect(screen.getByTestId('model-menu')).toHaveAttribute('data-open', 'true');
+
+ fireEvent.click(screen.getByRole('button', { name: 'Local Model Settings' }));
+ expect(screen.getByTestId('model-menu')).toHaveAttribute('data-open', 'false');
+ expect(
+ screen.queryByRole('heading', { name: 'Local Model Settings — Display local-model' })
+ ).not.toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Complete model menu close' }));
+ expect(
+ screen.getByRole('heading', { name: 'Local Model Settings — Display local-model' })
+ ).toBeInTheDocument();
+ expect(mockPreventCloseAutoFocus).not.toHaveBeenCalled();
+
+ fireEvent.click(screen.getByRole('button', { name: '×' }));
+ fireEvent.click(screen.getByRole('button', { name: 'Open model menu' }));
+ fireEvent.click(screen.getByRole('button', { name: 'Change Model' }));
+ expect(screen.getByTestId('model-menu')).toHaveAttribute('data-open', 'false');
+ expect(screen.queryByTestId('switch-model-modal')).not.toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Complete model menu close' }));
+ expect(screen.getByTestId('switch-model-modal')).toBeInTheDocument();
+ expect(mockPreventCloseAutoFocus).toHaveBeenCalledOnce();
+ });
});
diff --git a/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx
index 11e88bc24..edc26bd82 100644
--- a/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx
+++ b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx
@@ -1,5 +1,5 @@
import { Sliders, Bot, LoaderCircle, Settings, History } from 'lucide-react';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useModelAndProvider } from '../../../ModelAndProviderContext';
import { SwitchModelModal } from '../subcomponents/SwitchModelModal';
import { View } from '../../../../utils/navigationUtils';
@@ -68,6 +68,8 @@ interface ModelsBottomBarProps {
sessionLoaded?: boolean;
}
+type ModelMenuModal = 'switch-model' | 'local-model-settings';
+
export default function ModelsBottomBar({
sessionId,
dropdownRef,
@@ -80,7 +82,11 @@ export default function ModelsBottomBar({
}: ModelsBottomBarProps) {
// ChatInput owns the override state and passes effective model/provider as sessionModel/sessionProvider.
// Fall back to config defaults when no session-specific model is available.
- const { currentModel: configModel, currentProvider: configProvider, changeModel } = useModelAndProvider();
+ const {
+ currentModel: configModel,
+ currentProvider: configProvider,
+ changeModel,
+ } = useModelAndProvider();
const currentModel = sessionModel ?? configModel;
const currentProvider = sessionProvider ?? configProvider;
@@ -89,6 +95,8 @@ export default function ModelsBottomBar({
const [displayModelName, setDisplayModelName] = useState(
intl.formatMessage(i18n.selectModel)
);
+ const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
+ const pendingModalRef = useRef(null);
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
const [isLocalModelSettingsOpen, setIsLocalModelSettingsOpen] = useState(false);
const [providerDefaultModel, setProviderDefaultModel] = useState(null);
@@ -162,6 +170,24 @@ export default function ModelsBottomBar({
onModelChanged({ model, provider });
};
+ const openModalAfterMenuCloses = (modal: ModelMenuModal) => {
+ pendingModalRef.current = modal;
+ setIsModelMenuOpen(false);
+ };
+
+ const handleModelMenuCloseAutoFocus = (event: Event) => {
+ const pendingModal = pendingModalRef.current;
+ if (!pendingModal) return;
+
+ pendingModalRef.current = null;
+ if (pendingModal === 'switch-model') {
+ event.preventDefault();
+ setIsAddModelModalOpen(true);
+ } else {
+ setIsLocalModelSettingsOpen(true);
+ }
+ };
+
const handleRecentModelClick = async (recent: RecentModel) => {
const previousModel = currentModel;
const previousProvider = currentProvider;
@@ -195,7 +221,7 @@ export default function ModelsBottomBar({
return (
-
+
@@ -212,7 +238,12 @@ export default function ModelsBottomBar({
)}
-
+
{intl.formatMessage(i18n.currentModel)}
@@ -241,18 +272,20 @@ export default function ModelsBottomBar({
onClick={() => void handleRecentModelClick(recent)}
>
- {getModelDisplayName(recent.model)} — {recent.provider}
+
+ {getModelDisplayName(recent.model)} — {recent.provider}
+
))}
>
)}
- setIsAddModelModalOpen(true)}>
+ openModalAfterMenuCloses('switch-model')}>
{intl.formatMessage(i18n.changeModel)}
{currentProvider === 'local' && currentModel && (
- setIsLocalModelSettingsOpen(true)}>
+ openModalAfterMenuCloses('local-model-settings')}>
{intl.formatMessage(i18n.localModelSettings)}