fix(agent-picker): list agents named by the search before other agents' model setups (#39806)

* fix(agent-picker): list agents named by the search before other agents' model setups

Typing "codex" in the agent picker showed OpenCode/Cursor model setups
(e.g. "OpenCode · GPT-5 Codex") above the Codex agent itself, and Enter
picked the first setup. Direct agent matches now render first and are the
Enter target.

* test(agent-picker): assert search highlight and Enter target for named agent

* test(agent-picker): import fireEvent after rebase

(cherry picked from commit f04e06b023)
(cherry picked from commit f2960d1b73)
This commit is contained in:
Neil
2026-11-09 01:57:18 -08:00
committed by Neil
parent 2e6c9e70db
commit d40925eb08
2 changed files with 103 additions and 26 deletions
@@ -0,0 +1,76 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import type { ComponentProps } from 'react'
import type * as LaunchSetup from './AgentLaunchSetupButton'
import type { AgentLaunchSetupButton } from './AgentLaunchSetupButton'
import { AgentLaunchPickerMenu } from './AgentLaunchPickerMenu'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuSub,
DropdownMenuSubTrigger
} from '@/components/ui/dropdown-menu'
import type { AgentPickerEntry } from '../../../../shared/agent-launch/picker/agent-launch-picker'
vi.mock('@/hooks/useShortcutLabel', () => ({ useOptionalShortcutLabel: () => null }))
vi.mock('./AgentLaunchSetupButton', async (importOriginal) => ({
...(await importOriginal<typeof LaunchSetup>()),
AgentLaunchSetupButton: vi.fn((props: ComponentProps<typeof AgentLaunchSetupButton>) => (
<DropdownMenuSub>
<DropdownMenuSubTrigger asChild hideChevron>
{props.renderAnchor?.(<button>Setup</button>)}
</DropdownMenuSubTrigger>
</DropdownMenuSub>
))
}))
afterEach(() => {
cleanup()
vi.clearAllMocks()
})
it('lists an agent named by the query before other agents’ model setups', () => {
const codex: AgentPickerEntry = {
agent: 'codex',
label: 'Codex',
rows: [{ agent: 'codex', kind: 'cli' }],
surfaces: ['terminal'],
defaultSurface: 'terminal',
terminal: {},
structured: {},
catalog: { origin: 'probe', fetchedAt: 1, models: [] }
}
const opencode: AgentPickerEntry = {
...codex,
agent: 'opencode',
label: 'OpenCode',
rows: [{ agent: 'opencode', kind: 'cli' }],
catalog: {
origin: 'probe',
fetchedAt: 1,
models: [{ id: 'gpt-5-codex', label: 'GPT-5 Codex', isDefault: false, efforts: [] }]
}
}
const launch = vi.fn()
render(
<DropdownMenu defaultOpen modal={false}>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent>
<AgentLaunchPickerMenu entries={[opencode, codex]} runLaunch={launch} />
</DropdownMenuContent>
</DropdownMenu>
)
fireEvent.focus(screen.getByRole('textbox'))
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'codex' } })
const items = screen.getAllByRole('menuitem').map((item) => item.textContent)
expect(items[0]).toContain('Codex')
expect(items[0]).not.toContain('OpenCode')
expect(items.some((text) => text?.includes('OpenCode · GPT-5 Codex'))).toBe(true)
const highlighted = document.querySelectorAll('[data-search-default]')
expect(highlighted).toHaveLength(1)
expect(highlighted[0].textContent).toContain('Codex')
expect(highlighted[0].textContent).not.toContain('OpenCode')
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' })
expect(launch).toHaveBeenCalledWith({ agent: 'codex', kind: 'cli' }, undefined)
})
@@ -136,8 +136,9 @@ export function AgentLaunchPickerMenu({
}
// Why: Enter in the search picks the first enabled row, so show it highlighted like cmdk comboboxes.
const enabled = (entry: AgentPickerEntry) => !disabled && entry.surfaces.length > 0
const firstSetup = setups.find((setup) => enabled(setup.entry))
const firstEntry = firstSetup ? undefined : visible.find(enabled)
// Why: a query naming an agent should surface that agent before other agents’ model setups.
const firstEntry = visible.find(enabled)
const firstSetup = firstEntry ? undefined : setups.find((setup) => enabled(setup.entry))
const showSearchDefault = searchFocused && query.trim().length > 0
return (
<div ref={root} data-agent-launch-picker="true">
@@ -178,6 +179,30 @@ export function AgentLaunchPickerMenu({
</div>
) : null}
<div className="max-h-64 overflow-y-auto scrollbar-sleek">
{visible.map((entry) => {
const source = draftForEntry?.(entry)
return (
<AgentPickerMenuRow
key={entry.agent}
draftScope={draftScope}
entry={entry}
mode={mode}
searchDefault={showSearchDefault && entry === firstEntry}
source={source}
selected={value?.agent === entry.agent ? value : undefined}
setupDisabled={setupDisabled}
openSetup={openSetup}
onSetupOpenChange={setActiveSetup}
disabled={disabled}
terminalBlocked={terminalBlocked}
runLaunch={runLaunch}
onSelectAgent={onSelectAgent}
isDefault={defaultAgent === entry.agent}
onSetDefault={onSetDefault}
shortcut={mode !== 'select' ? shortcutLabels?.get(entry.agent) : undefined}
/>
)
})}
{setups.map((setup) => {
const source = draftForEntry?.(setup.entry)
return (
@@ -203,30 +228,6 @@ export function AgentLaunchPickerMenu({
/>
)
})}
{visible.map((entry) => {
const source = draftForEntry?.(entry)
return (
<AgentPickerMenuRow
key={entry.agent}
draftScope={draftScope}
entry={entry}
mode={mode}
searchDefault={showSearchDefault && entry === firstEntry}
source={source}
selected={value?.agent === entry.agent ? value : undefined}
setupDisabled={setupDisabled}
openSetup={openSetup}
onSetupOpenChange={setActiveSetup}
disabled={disabled}
terminalBlocked={terminalBlocked}
runLaunch={runLaunch}
onSelectAgent={onSelectAgent}
isDefault={defaultAgent === entry.agent}
onSetDefault={onSetDefault}
shortcut={mode !== 'select' ? shortcutLabels?.get(entry.agent) : undefined}
/>
)
})}
{!visible.length && !setups.length ? (
<DropdownMenuItem disabled>
{t('auto.components.agent.AgentCombobox.579c768bde', 'No agents match your search.')}