diff --git a/apps/web/src/lib/core/component/AI/component/input/ModelCatalogPicker.test.tsx b/apps/web/src/lib/core/component/AI/component/input/ModelCatalogPicker.test.tsx
new file mode 100644
index 00000000000..bb50449af4b
--- /dev/null
+++ b/apps/web/src/lib/core/component/AI/component/input/ModelCatalogPicker.test.tsx
@@ -0,0 +1,146 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import {
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@solidjs/testing-library';
+import { createSignal, type JSX } from 'solid-js';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { ModelCatalogPicker } from './ModelCatalogPicker';
+import type { CatalogModelOption } from './modelCatalog';
+
+vi.mock('@ui', () => {
+ const cn = (...args: unknown[]) =>
+ args.flat(Infinity).filter(Boolean).join(' ');
+
+ let onOpenAutoFocus: ((event: Event) => void) | undefined;
+
+ const Dropdown: any = (props: { children?: JSX.Element }) => (
+
{props.children}
+ );
+ Dropdown.Trigger = (props: {
+ children?: JSX.Element;
+ 'aria-label'?: string;
+ }) => (
+ {
+ const event = new Event('focusScope.autoFocusOnMount', {
+ cancelable: true,
+ });
+ onOpenAutoFocus?.(event);
+ // Kobalte's selectable list still autofocuses the collection on a
+ // deferred timeout after onOpenAutoFocus is prevented.
+ setTimeout(() => {
+ document.getElementById('model-catalog-menu')?.focus();
+ }, 0);
+ }}
+ >
+ {props.children}
+
+ );
+ Dropdown.Content = (props: {
+ children?: JSX.Element;
+ onOpenAutoFocus?: (event: Event) => void;
+ }) => {
+ onOpenAutoFocus = props.onOpenAutoFocus;
+ return (
+
+ );
+ };
+ Dropdown.Group = (props: { children?: JSX.Element }) => (
+ {props.children}
+ );
+ Dropdown.GroupLabel = (props: { children?: JSX.Element }) => (
+ {props.children}
+ );
+ Dropdown.Item = (props: {
+ children?: JSX.Element;
+ onSelect?: () => void;
+ }) => (
+ props.onSelect?.()}>
+ {props.children}
+
+ );
+ Dropdown.Separator = () => ;
+ Dropdown.Sub = (props: { children?: JSX.Element }) => (
+ {props.children}
+ );
+ Dropdown.SubTrigger = (props: { children?: JSX.Element }) => (
+ {props.children}
+ );
+ Dropdown.SubContent = (props: { children?: JSX.Element }) => (
+ {props.children}
+ );
+
+ return { cn, Dropdown };
+});
+
+const OPTIONS: CatalogModelOption[] = [
+ { id: 'auto', label: 'Auto', group: 'Auto' },
+ { id: 'grok', label: 'Cursor Grok 4.6 High Fast' },
+ { id: 'opus', label: 'Claude Opus 5 High' },
+ { id: 'sonnet', label: 'Claude Sonnet 5 High' },
+ { id: 'sol', label: 'GPT-5.6 Sol High' },
+ { id: 'gemini', label: 'Gemini 3.8 Flash High' },
+];
+
+afterEach(() => {
+ cleanup();
+});
+
+function mountPicker() {
+ render(() => {
+ const [value, setValue] = createSignal('auto');
+ return (
+
+ );
+ });
+}
+
+describe('ModelCatalogPicker search focus', () => {
+ it('puts caret in the search field when the menu opens', async () => {
+ mountPicker();
+ fireEvent.click(screen.getByRole('button', { name: 'Agent model' }));
+ const search = screen.getByRole('textbox', { name: 'Search models' });
+ await waitFor(() => {
+ expect(document.activeElement).toBe(search);
+ });
+ });
+
+ it('puts caret in the search field when the menu is opened again', async () => {
+ mountPicker();
+ const trigger = screen.getByRole('button', { name: 'Agent model' });
+ fireEvent.click(trigger);
+ await waitFor(() => {
+ expect(document.activeElement).toBe(
+ screen.getByRole('textbox', { name: 'Search models' })
+ );
+ });
+
+ document.getElementById('model-catalog-menu')?.focus();
+ expect(document.activeElement).not.toBe(
+ screen.getByRole('textbox', { name: 'Search models' })
+ );
+
+ fireEvent.click(trigger);
+ await waitFor(() => {
+ expect(document.activeElement).toBe(
+ screen.getByRole('textbox', { name: 'Search models' })
+ );
+ });
+ });
+});
diff --git a/apps/web/src/lib/core/component/AI/component/input/ModelCatalogPicker.tsx b/apps/web/src/lib/core/component/AI/component/input/ModelCatalogPicker.tsx
index 5360040af0c..1bbaa3de47a 100644
--- a/apps/web/src/lib/core/component/AI/component/input/ModelCatalogPicker.tsx
+++ b/apps/web/src/lib/core/component/AI/component/input/ModelCatalogPicker.tsx
@@ -54,6 +54,21 @@ function ModelRow(props: {
);
}
+/**
+ * Kobalte menus still autofocus the collection on a deferred `setTimeout(0)`
+ * (`deferAutoFocus` in createSelectableList) after `onOpenAutoFocus` is
+ * prevented. A microtask loses that race; wait past the timeout so typing
+ * lands in search.
+ */
+function focusSearchAfterMenuOpen(input: () => HTMLInputElement | undefined) {
+ setTimeout(() => {
+ requestAnimationFrame(() => {
+ const search = input();
+ if (search?.isConnected) search.focus();
+ });
+ });
+}
+
export function ModelCatalogPicker(props: ModelCatalogPickerProps) {
const [query, setQuery] = createSignal('');
let searchRef: HTMLInputElement | undefined;
@@ -100,7 +115,7 @@ export function ModelCatalogPicker(props: ModelCatalogPickerProps) {
)}
onOpenAutoFocus={(event) => {
event.preventDefault();
- queueMicrotask(() => searchRef?.focus());
+ focusSearchAfterMenuOpen(() => searchRef);
}}
>