@@ -83,7 +84,7 @@ export function AgentChoiceControls({
disabled={disabled}
label={
selected?.model
- ? (findCatalogModel(catalog, selected.model)?.label ?? selected.model)
+ ? (model?.label ?? selected.model)
: translate('settings.sourceControl.actionModel.agentDefault', 'Agent default')
}
/>
diff --git a/src/renderer/src/components/settings/source-control-action-permission-draft.test.tsx b/src/renderer/src/components/settings/source-control-action-permission-draft.test.tsx
index 7436ff0a614..bbfb4bbaf67 100644
--- a/src/renderer/src/components/settings/source-control-action-permission-draft.test.tsx
+++ b/src/renderer/src/components/settings/source-control-action-permission-draft.test.tsx
@@ -434,6 +434,76 @@ it('keeps the old Codex workspace-write Ask literal and explains that terminal t
).toBeDefined()
})
+it.each([
+ { model: undefined, controls: 2, fast: false },
+ { model: 'unlisted-host-model', controls: 2, fast: false },
+ { model: 'opus', controls: 3, fast: true }
+])('offers only model-advertised action mode controls for $model', ({ model, controls, fast }) => {
+ render(
+
+
+
+ )
+ expect(screen.getAllByRole('combobox')).toHaveLength(controls)
+ expect(screen.queryByText('Fast mode') !== null).toBe(fast)
+ expect(screen.getByRole('button', { name: 'Claude permissions' })).toBeDefined()
+ expect(screen.getByRole('textbox', { name: 'Arguments' })).toHaveProperty('value', '--verbose')
+})
+
+it.each(['host-private', 'sonnet'])(
+ 'keeps a saved %s Fast badge removable without offering a Fast select',
+ async (model) => {
+ write.mockClear()
+ const saved = {
+ value: normalizeSourceControlAiSettings({
+ ...initial,
+ actions: {
+ fixChecks: {
+ agentId: 'claude',
+ agentArgs: `--settings '{"theme":"light"}' --add-dir docs`,
+ launchPreferences: {
+ agentId: 'claude',
+ model,
+ options: { fastMode: 'true' }
+ }
+ }
+ }
+ })
+ }
+ render(
)
+ expect(
+ screen.getByRole('combobox', { name: model === 'sonnet' ? 'Sonnet' : model })
+ ).toBeDefined()
+ expect(screen.queryByText('Fast mode')).toBeNull()
+ expect(screen.getAllByRole('combobox')).toHaveLength(2)
+ fireEvent.click(screen.getByRole('button', { name: /Remove --settings/ }))
+ expect(screen.queryByRole('button', { name: /Remove --settings/ })).toBeNull()
+ fireEvent.click(screen.getByText('Save'))
+ await waitFor(() => expect(write).toHaveBeenCalledTimes(1))
+ const recipe = saved.value.actions?.fixChecks
+ expect(sourceControlActionSelectedPreferences(recipe ?? {}, 'claude')).toMatchObject({
+ model
+ })
+ expect(
+ sourceControlActionSelectedPreferences(recipe ?? {}, 'claude')?.options?.fastMode
+ ).toBeUndefined()
+ const args = sourceControlActionAgentArgs(recipe ?? {}, 'claude', null)
+ expect(shell.tokenizeStartupCommand(args ?? '', 'posix')).toMatchObject({
+ ok: true,
+ tokens: ['--settings', '{"theme":"light"}', '--add-dir', 'docs']
+ })
+ expect(args).toContain('"theme":"light"')
+ expect(args).toContain('--add-dir docs')
+ expect(args).not.toContain('"fastMode":true')
+ }
+)
+
it('edits and saves a sparse repository model without authoring the inherited permission', async () => {
write.mockClear()
const source = {
diff --git a/src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx b/src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx
index 67a21fcfdad..d3f5f498b79 100644
--- a/src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx
+++ b/src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx
@@ -75,7 +75,11 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { ReviewNotesSendMenuContent } from '@/components/editor/ReviewNotesSendMenuContent'
-import { QuickLaunchAgentMenuItems, ProjectedAgentMenuItems } from './QuickLaunchButton'
+import {
+ QuickLaunchAgentMenuItems,
+ ProjectedAgentMenuItems,
+ useTabAgentMenuLaunch
+} from './QuickLaunchButton'
import { useTabBarCreateMenuController } from './use-tab-bar-create-menu-controller'
import * as menuRows from './use-new-tab-agent-menu-rows'
import {
@@ -655,3 +659,45 @@ describe('QuickLaunchAgentMenuItems launches', () => {
expect(launchMock).not.toHaveBeenCalled()
})
})
+
+it('forwards configured choices through the existing dispatcher and launch-only Arguments only to terminals', () => {
+ launchMock.mockClear()
+ launchMock.mockReturnValue({ surface: { kind: 'local-terminal', tabId: 'new-tab' } })
+ const hook = renderHook(() =>
+ useTabAgentMenuLaunch({
+ worktreeId: WORKTREE_ID,
+ groupId: 'group-1',
+ onFocusTerminal: vi.fn()
+ })
+ )
+ const choices = { model: 'astra', options: { effort: 'low' }, permissionMode: 'ask' as const }
+ hook.result.current(
+ { agent: 'codex', kind: 'cli', executionHostId: 'runtime:server' },
+ {
+ agent: 'codex',
+ surface: 'terminal',
+ choices,
+ arguments: '--add-dir docs'
+ }
+ )
+ expect(launchMock.mock.calls[0][0]).toMatchObject({
+ requestedSurface: 'terminal',
+ selectedExecutionHostId: 'runtime:server',
+ agentArgs: expect.stringContaining('--add-dir docs'),
+ sessionOptions: { model: 'astra', effort: 'low', permissionMode: 'ask' }
+ })
+ hook.result.current(
+ { agent: 'codex', kind: 'chat', executionHostId: 'runtime:server' },
+ {
+ agent: 'codex',
+ surface: 'chat',
+ choices,
+ arguments: '--add-dir docs'
+ }
+ )
+ expect(launchMock.mock.calls[1][0]).toMatchObject({
+ requestedSurface: 'chat',
+ sessionOptions: { model: 'astra', effort: 'low', permissionMode: 'ask' }
+ })
+ expect(launchMock.mock.calls[1][0]).not.toHaveProperty('agentArgs')
+})
diff --git a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx
index 9b36523c376..7fa6458efdd 100644
--- a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx
+++ b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx
@@ -1,3 +1,9 @@
+import { AgentLaunchPickerMenu } from '@/components/agent/AgentLaunchPickerMenu'
+import {
+ agentPickerLaunchInputs,
+ type AgentPickerEntry,
+ type AgentPickerDraft
+} from '../../../../shared/agent-launch-picker'
import React, { useCallback, useRef } from 'react'
import { toast } from 'sonner'
import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab'
@@ -70,7 +76,7 @@ export function useTabAgentMenuLaunch({
onFocusChat?: (tabId: string) => void
}) {
const runLaunch = useCallback(
- (row: NewTabAgentMenuRow) => {
+ (row: NewTabAgentMenuRow, draft?: AgentPickerDraft, pickerEntry?: AgentPickerEntry) => {
if (disabled || row.kind === 'chat-unavailable') {
return
}
@@ -87,6 +93,7 @@ export function useTabAgentMenuLaunch({
}
: {}),
agent,
+ ...(draft ? agentPickerLaunchInputs(draft, pickerEntry) : {}),
worktreeId,
groupId,
...(prompt !== undefined ? { prompt } : {}),
@@ -165,11 +172,17 @@ function QuickLaunchAgentMenuItemsInner(props: QuickLaunchAgentMenuItemsProps):
export function ProjectedAgentMenuItems({
disabled = false,
model,
- runLaunch
+ runLaunch,
+ pickerEntries,
+ onSelectPickerAgent,
+ hidePickerSearch
}: {
disabled?: boolean
model: NewTabAgentMenuModel
- runLaunch: (row: NewTabAgentMenuRow) => void
+ runLaunch: (row: NewTabAgentMenuRow, draft?: AgentPickerDraft) => void
+ pickerEntries?: AgentPickerEntry[]
+ onSelectPickerAgent?: (agent: AgentPickerEntry['agent']) => void
+ hidePickerSearch?: boolean
}): React.JSX.Element {
const desktopDefaultAgent = useAppStore((s) => s.settings?.defaultTuiAgent)
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
@@ -235,60 +248,70 @@ export function ProjectedAgentMenuItems({
)}
) : null}
- {rows.map((row, index) => {
- const { agent } = row
- const label = getCatalogEntry(agent)?.label ?? agent
- // Chat rows come first; the rows that open a terminal start their own group.
- const startsCliGroup = row.kind === 'cli' && index > 0 && rows[index - 1]?.kind !== 'cli'
- if (row.kind === 'chat-unavailable') {
- return (
-
-
- {label}
-
- {chatUnavailableLabel(row.reason)}
-
-
- )
- }
- const showsDefaultAgentShortcut =
- newAgentShortcut !== null && defaultAgent !== 'blank' && agent === defaultAgent
- const rowContent = (
- <>
-
-
{rowLabel(row)}
- {showsDefaultAgentShortcut ? (
-
{newAgentShortcut}
- ) : null}
- >
- )
- const other = otherSurfaceRow(row, detectedIds)
- return (
-
- {startsCliGroup ? : null}
- {other ? (
-
- {rowContent}
-
- ) : (
- runLaunch(row)}
- title={rowTitle(row, label)}
- >
- {rowContent}
+ {pickerEntries ? (
+
+ ) : (
+ rows.map((row, index) => {
+ const { agent } = row
+ const label = getCatalogEntry(agent)?.label ?? agent
+ // Chat rows come first; the rows that open a terminal start their own group.
+ const startsCliGroup = row.kind === 'cli' && index > 0 && rows[index - 1]?.kind !== 'cli'
+ if (row.kind === 'chat-unavailable') {
+ return (
+
+
+ {label}
+
+ {chatUnavailableLabel(row.reason)}
+
- )}
-
- )
- })}
+ )
+ }
+ const showsDefaultAgentShortcut =
+ newAgentShortcut !== null && defaultAgent !== 'blank' && agent === defaultAgent
+ const rowContent = (
+ <>
+
+
{rowLabel(row)}
+ {showsDefaultAgentShortcut ? (
+
{newAgentShortcut}
+ ) : null}
+ >
+ )
+ const other = otherSurfaceRow(row, detectedIds)
+ return (
+
+ {startsCliGroup ? : null}
+ {other ? (
+
+ {rowContent}
+
+ ) : (
+ runLaunch(row)}
+ title={rowTitle(row, label)}
+ >
+ {rowContent}
+
+ )}
+
+ )
+ })
+ )}
>
)
diff --git a/src/renderer/src/components/tab-bar/TabBarCreateEntry.search.test.tsx b/src/renderer/src/components/tab-bar/TabBarCreateEntry.search.test.tsx
index 9ac3b9cb837..5db51f5edd1 100644
--- a/src/renderer/src/components/tab-bar/TabBarCreateEntry.search.test.tsx
+++ b/src/renderer/src/components/tab-bar/TabBarCreateEntry.search.test.tsx
@@ -546,3 +546,105 @@ describe('TabBarCreateEntry search behavior', () => {
expect(tabSearchMock.calls.at(-1)).toEqual({ enabled: false, query: '' })
})
})
+
+it('launches searched defaults on Enter and reaches the alternate only through Configure', () => {
+ const launched = vi.fn()
+ const entry = {
+ agent: 'codex' as const,
+ label: 'Codex',
+ rows: [{ agent: 'codex' as const, kind: 'chat' as const }],
+ surfaces: ['chat' as const, 'terminal' as const],
+ defaultSurface: 'chat' as const,
+ terminal: {},
+ structured: {},
+ catalog: {
+ origin: 'probe' as const,
+ fetchedAt: 1,
+ models: [
+ { id: 'astra', label: 'Astra', isDefault: true, efforts: [{ value: 'low', label: 'Low' }] }
+ ]
+ }
+ }
+ renderEntry({ agentPickerEntries: [entry], onLaunchAgentOption: launched })
+ setQuery('astra low')
+ submit()
+ expect(launched).toHaveBeenCalledOnce()
+ expect(launched.mock.calls[0][0].setup).toMatchObject({
+ surface: 'chat',
+ choices: { model: 'astra', options: { effort: 'low' } }
+ })
+ launched.mockClear()
+ renderEntry({ agentPickerEntries: [entry], onLaunchAgentOption: launched })
+ setQuery('astra low')
+ expect(container.querySelector('button[aria-label="Launch terminal"]')).toBeNull()
+ const configure = container.querySelector
(
+ 'button[aria-label="Configure Codex"]'
+ )!
+ expect(configure.dataset.variant).toBe('ghost')
+ expect(configure.dataset.size).toBe('icon-xs')
+ expect(
+ configure.querySelector('svg.lucide-chevron-right')?.classList.contains('text-muted-foreground')
+ ).toBe(true)
+ expect(configure.classList.contains('border')).toBe(false)
+ expect(configure.previousElementSibling?.getAttribute('data-slot')).toBe('separator')
+ expect(configure.previousElementSibling?.getAttribute('data-orientation')).toBe('vertical')
+ expect(
+ configure.previousElementSibling?.classList.contains('data-[orientation=vertical]:h-4')
+ ).toBe(true)
+ act(() => configure.click())
+ expect(launched).not.toHaveBeenCalled()
+ act(() =>
+ [...document.querySelectorAll('button')]
+ .find((button) => button.textContent === 'Launch terminal')
+ ?.click()
+ )
+ expect(launched).toHaveBeenCalledOnce()
+ expect(launched.mock.calls[0][0].setup.surface).toBe('terminal')
+})
+
+it.each(['g', 'low', 'high'])(
+ 'keeps the file as the selected Enter action for %s with setup search mounted',
+ (query) => {
+ fileListMock.current = { files: [`${query}.ts`], loading: false, loadError: null }
+ const launched = vi.fn(),
+ opened = vi.fn().mockResolvedValue(undefined)
+ renderEntry({
+ onOpenEntry: opened,
+ onLaunchAgentOption: launched,
+ agentPickerEntries: [
+ {
+ agent: 'codex',
+ label: 'Codex',
+ rows: [{ agent: 'codex', kind: 'chat' }],
+ surfaces: ['chat', 'terminal'],
+ defaultSurface: 'chat',
+ terminal: {},
+ structured: {},
+ catalog: {
+ origin: 'probe',
+ fetchedAt: 1,
+ models: [
+ {
+ id: 'gpt-6-astra',
+ label: 'GPT 6 Astra',
+ isDefault: true,
+ efforts: [
+ { value: 'low', label: 'Low' },
+ { value: 'high', label: 'High' }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ })
+ setQuery(query)
+ expect(container.querySelector('[aria-selected="true"]')?.textContent).toContain(
+ query === 'g' ? 'Search Google' : `${query}.ts`
+ )
+ expect(rowTexts().some((text) => text.includes(`${query}.ts`))).toBe(true)
+ submit()
+ expect(launched).not.toHaveBeenCalled()
+ expect(opened).toHaveBeenCalledOnce()
+ }
+)
diff --git a/src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx b/src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx
index e2d4b42899a..39ccef2112e 100644
--- a/src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx
+++ b/src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx
@@ -6,7 +6,11 @@ import {
getTabEntryOptions,
isTabEntryAbsolutePathLike
} from './tab-create-entry-action'
-import { findMatchingTabAgentLaunchOptions } from './tab-agent-launch-options'
+import {
+ configuredAgentPickerLaunchOption,
+ findMatchingTabAgentLaunchOptions,
+ matchingAgentPickerLaunchOptions
+} from './tab-agent-launch-options'
import { findMatchingTabCreateMenuOptions } from './tab-create-menu-options'
import { getActiveOptionId, type ActiveOption } from './tab-create-entry-active-option'
import {
@@ -42,6 +46,7 @@ export default function TabBarCreateEntry(props: TabBarCreateEntryProps): React.
function TabBarCreateEntrySession({
agentOptions = EMPTY_AGENT_OPTIONS,
+ agentPickerEntries,
groupId,
menuOpen,
menuOptions = EMPTY_MENU_OPTIONS,
@@ -148,8 +153,11 @@ function TabBarCreateEntrySession({
() =>
terminalQueryMode
? EMPTY_AGENT_OPTIONS
- : findMatchingTabAgentLaunchOptions(query, agentOptions),
- [agentOptions, query, terminalQueryMode]
+ : [
+ ...matchingAgentPickerLaunchOptions(agentPickerEntries ?? [], query),
+ ...findMatchingTabAgentLaunchOptions(query, agentOptions)
+ ],
+ [agentOptions, agentPickerEntries, query, terminalQueryMode]
)
const disabled = !onOpenEntry
@@ -377,6 +385,15 @@ function TabBarCreateEntrySession({
id={resultOptionDomId(index)}
option={option}
selected={index === activeSelectedIndex}
+ onUseAgent={
+ option.kind === 'agent' && option.option.pickerEntry
+ ? (draft) =>
+ submitOption({
+ kind: 'agent',
+ option: configuredAgentPickerLaunchOption(option.option, draft)
+ })
+ : undefined
+ }
disabled={
disabled ||
pending ||
diff --git a/src/renderer/src/components/tab-bar/TabBarCreateEntryRow.tsx b/src/renderer/src/components/tab-bar/TabBarCreateEntryRow.tsx
index 9ff5ad5fa8b..25c18b01ad1 100644
--- a/src/renderer/src/components/tab-bar/TabBarCreateEntryRow.tsx
+++ b/src/renderer/src/components/tab-bar/TabBarCreateEntryRow.tsx
@@ -18,6 +18,8 @@ import { SEARCH_ENGINE_LABELS } from '../../../../shared/browser-url'
import { formatBrowserHistoryUrl } from '@/lib/browser-history-match'
import type { ActiveOption } from './tab-create-entry-active-option'
import { BrowserFavicon } from '@/components/browser-favicon'
+import { AgentLaunchSetupButton } from '../agent/AgentLaunchSetupButton'
+import type { AgentPickerDraft } from '../../../../shared/agent-launch-picker'
export const RESULT_LISTBOX_ID = 'tab-create-entry-results'
@@ -47,6 +49,7 @@ export function EntryActionRow({
labelOverride,
loading = false,
onClick,
+ onUseAgent,
option,
selected
}: {
@@ -54,6 +57,7 @@ export function EntryActionRow({
id: string
labelOverride?: string
loading?: boolean
+ onUseAgent?: (draft: AgentPickerDraft) => void
onClick: () => void
option: ActiveOption
selected: boolean
@@ -61,43 +65,54 @@ export function EntryActionRow({
const presentation = getActionPresentation(option, labelOverride)
const row = (
-