The default agent can be set to open as a chat or a CLI (#31590)

* feat(settings): the default agent can be set to open as a chat or a CLI

With Chat UI on, Settings > Agents > Default Agent now shows a Chat | CLI
choice for the selected agent. It writes the same per-agent chat switch
that Chat > Providers and the + menu already use, so no new setting is
saved and the two places can't disagree.

* test: type the store stand-in without an assertion

* chore: drop accidental lockfile churn

* fix: clarify default agent launch preference scope
This commit is contained in:
Brennan Benson
2026-10-21 14:03:50 -07:00
committed by GitHub
parent c054e94eff
commit 83f163e09f
11 changed files with 215 additions and 16 deletions
@@ -4,7 +4,13 @@ import type { AgentCatalogEntry } from '@/lib/agent-catalog'
import { AgentIcon } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { SettingsSubsectionHeader } from './SettingsFormControls'
import { SettingsSegmentedControl, SettingsSubsectionHeader } from './SettingsFormControls'
export type DefaultAgentOpensAs = {
agentLabel: string
value: 'chat' | 'cli'
onChange: (value: 'chat' | 'cli') => void
}
function DefaultAgentPill({
active,
@@ -41,6 +47,7 @@ export function AgentDefaultSetting({
enabledDetectedAgents,
catalog,
description,
opensAs,
onSetDefault
}: {
defaultAgent: TuiAgent | 'blank' | null
@@ -48,6 +55,8 @@ export function AgentDefaultSetting({
enabledDetectedAgents: AgentCatalogEntry[]
catalog: AgentCatalogEntry[]
description: string
/** Present only when the default agent can open as a chat on this computer. */
opensAs?: DefaultAgentOpensAs
onSetDefault: (agent: TuiAgent | 'blank' | null) => void
}): React.JSX.Element {
const storedDefaultAgent =
@@ -97,6 +106,29 @@ export function AgentDefaultSetting({
)
})}
</div>
{opensAs ? (
<div className="flex flex-wrap items-center gap-3">
<SettingsSegmentedControl<'chat' | 'cli'>
value={opensAs.value}
onChange={opensAs.onChange}
ariaLabel={translate('settings.agents.defaultOpensAs', 'Open {{value0}} as', {
value0: opensAs.agentLabel
})}
size="sm"
options={[
{ value: 'chat', label: translate('settings.agents.defaultOpensAsChat', 'Chat') },
{ value: 'cli', label: translate('settings.agents.defaultOpensAsCli', 'CLI') }
]}
/>
<p className="text-xs text-muted-foreground">
{translate(
'settings.agents.defaultOpensAsScope',
'Applies to new {{value0}} sessions launched from this computer, including on paired hosts. Matches its Providers switch.',
{ value0: opensAs.agentLabel }
)}
</p>
</div>
) : null}
</section>
)
}
@@ -21,6 +21,10 @@ import { AgentDetectionCatalog } from './AgentDetectionCatalog'
import type { AgentCatalogRowProps } from './AgentCatalogRow'
import { HostAgentEnvironmentEditor } from './HostAgentEnvironmentEditor'
import { buildCodexSessionSourceHomeControl } from './codex-session-source-home-control'
import { buildDefaultAgentOpensAs } from './native-chat-provider-choice'
import { useHostStructuredAgentsVersion } from '@/lib/chat-provider-agents'
import { useAppStore } from '@/store'
import { readNativeDetectedAgents } from '@/store/slices/local-detected-agent-store-state'
import { translate } from '@/i18n/i18n'
export type AgentPermissionsRenderer = (props: AgentPermissionsSettingProps) => React.ReactNode
@@ -56,6 +60,8 @@ export function AgentLaunchSettingsCatalog({
refresh
} = useDetectedAgents(detectionTarget)
const detectedIds = useMemo(() => (detected ? new Set(detected) : null), [detected])
useHostStructuredAgentsVersion()
const installedHere = useAppStore(readNativeDetectedAgents)
const catalog = getAgentCatalog()
const detectedAgents =
detectedIds === null ? [] : catalog.filter((agent) => detectedIds.has(agent.id))
@@ -138,6 +144,17 @@ export function AgentLaunchSettingsCatalog({
'settings.agents.hostDefaults',
'Defaults for new agents on the selected host.'
)}
// The chat-or-CLI choice is this computer's, so a paired host's page doesn't offer it.
opensAs={
local
? buildDefaultAgentOpensAs({
defaultAgent: settings.defaultTuiAgent,
settings: local.settings,
updateSettings: local.updateSettings,
installedHere
})
: undefined
}
onSetDefault={(agent) => save({ type: 'default', agent })}
/>
{renderPermissions({
@@ -1,10 +1,7 @@
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
import {
buildNativeChatProviderUpdate,
isNativeChatProviderDisplayedOn
} from '../../../../shared/native-chat-providers'
import { isNativeChatProviderDisplayedOn } from '../../../../shared/native-chat-providers'
import type { TuiAgent } from '../../../../shared/tui-agent'
import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection'
import { translate } from '@/i18n/i18n'
@@ -13,7 +10,7 @@ import { chatProviderAgents, useHostStructuredAgentsVersion } from '@/lib/chat-p
import { useManagedRuntimeInstall } from '@/runtime/use-managed-runtime-install'
import { useAppStore } from '../../store'
import { readNativeDetectedAgents } from '@/store/slices/local-detected-agent-store-state'
import { createAgentSettingsUpdateQueue } from './agent-availability-settings'
import { saveNativeChatProviderChoice } from './native-chat-provider-choice'
import { Card, CardContent } from '../ui/card'
import { ManagedRuntimeInstallRow } from '../managed-runtime/ManagedRuntimeInstallRow'
import { SearchableSetting } from './SearchableSetting'
@@ -27,8 +24,6 @@ import {
} from './AntigravityProviderSignInArea'
import { getHostRuntimeLabel, getRuntimeSentenceLabel } from './accounts-pane-runtime'
const enqueueProviderUpdate = createAgentSettingsUpdateQueue()
// The agents listed here are this computer's, so their downloaded programs are too.
const CHAT_HOST: RuntimeClientTarget = { kind: 'local' }
@@ -193,16 +188,14 @@ export function NativeChatProvidersSetting({
'Open {{value0}} as a chat',
{ value0: label }
)}
onChange={() => {
const next = !on
return enqueueProviderUpdate({
getSettings: () => useAppStore.getState().settings,
onChange={() =>
saveNativeChatProviderChoice({
agent,
on: !on,
fallbackSettings: settings,
updateSettings,
buildUpdate: (latest) =>
buildNativeChatProviderUpdate(latest, agent, next)
updateSettings
})
}}
}
/>
</div>
{/* Chats sign in on the host they run on, under the program that serves them. */}
@@ -0,0 +1,59 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createGlobalSettingsFixture } from '../../../../shared/global-settings-test-fixture'
import { AgentDefaultSetting } from './AgentDefaultSetting'
import { buildDefaultAgentOpensAs } from './native-chat-provider-choice'
const store = vi.hoisted((): { settings: unknown } => ({ settings: null }))
vi.mock('../../store', () => ({ useAppStore: { getState: () => store } }))
afterEach(cleanup)
function renderDefault(settings: ReturnType<typeof createGlobalSettingsFixture>) {
const updateSettings = vi.fn()
store.settings = settings
render(
<AgentDefaultSetting
defaultAgent={settings.defaultTuiAgent}
detectedIds={new Set(['claude'])}
enabledDetectedAgents={[]}
catalog={[]}
description=""
opensAs={buildDefaultAgentOpensAs({
defaultAgent: settings.defaultTuiAgent,
settings,
updateSettings,
installedHere: ['claude']
})}
onSetDefault={vi.fn()}
/>
)
return updateSettings
}
describe('default agent chat or CLI', () => {
it("flips the default agent's own chat switch, the one Providers and the + menu read", async () => {
const updateSettings = renderDefault(
createGlobalSettingsFixture({
experimentalNativeChat: true,
defaultTuiAgent: 'claude',
nativeChatProviders: { codex: false }
})
)
const group = screen.getByRole('radiogroup', { name: 'Open Claude as' })
expect(group.querySelector('[aria-checked="true"]')?.textContent).toBe('Chat')
fireEvent.click(screen.getByRole('radio', { name: 'CLI' }))
await vi.waitFor(() =>
expect(updateSettings).toHaveBeenCalledWith({
nativeChatProviders: { codex: false, claude: false }
})
)
})
it('offers no choice while Chat UI is off', () => {
renderDefault(
createGlobalSettingsFixture({ experimentalNativeChat: false, defaultTuiAgent: 'claude' })
)
expect(screen.queryByRole('radiogroup')).toBeNull()
})
})
@@ -0,0 +1,74 @@
import { isAgentSessionHandleProvider } from '../../../../shared/agent-session-provider-handle'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { buildNativeChatProviderUpdate } from '../../../../shared/native-chat-providers'
import {
isNativeChatDefaultFor,
isNativeChatEnabled
} from '../../../../shared/structured-native-chat-launch-route'
import type { TuiAgent } from '../../../../shared/tui-agent'
import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection'
import { getAgentLabel } from '@/lib/agent-catalog'
import { chatProviderAgents } from '@/lib/chat-provider-agents'
import { useAppStore } from '../../store'
import { createAgentSettingsUpdateQueue } from './agent-availability-settings'
import type { DefaultAgentOpensAs } from './AgentDefaultSetting'
// One queue for every settings row that writes this map, so back-to-back flips never overwrite each other.
const enqueueProviderUpdate = createAgentSettingsUpdateQueue()
export function saveNativeChatProviderChoice({
agent,
on,
fallbackSettings,
updateSettings
}: {
agent: TuiAgent
on: boolean
fallbackSettings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void | Promise<void>
}): Promise<void> {
return enqueueProviderUpdate({
getSettings: () => useAppStore.getState().settings,
fallbackSettings,
updateSettings,
buildUpdate: (latest) => buildNativeChatProviderUpdate(latest, agent, on)
})
}
/** The default agent's chat-or-CLI choice, offered where the stored value and the Providers switch agree. */
export function buildDefaultAgentOpensAs({
defaultAgent,
settings,
updateSettings,
installedHere
}: {
defaultAgent: TuiAgent | 'blank' | null
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void | Promise<void>
installedHere: readonly TuiAgent[] | null
}): DefaultAgentOpensAs | undefined {
if (defaultAgent === null || defaultAgent === 'blank' || !isNativeChatEnabled(settings)) {
return undefined
}
const agent = defaultAgent
// Built-ins or agents in this computer's own inventory: elsewhere Providers shows a different unset value.
const offered =
chatProviderAgents(LOCAL_EXECUTION_HOST_ID, undefined).includes(agent) &&
(isAgentSessionHandleProvider(agent) || installedHere?.includes(agent) === true) &&
isTuiAgentEnabled(agent, settings.disabledTuiAgents)
if (!offered) {
return undefined
}
return {
agentLabel: getAgentLabel(agent),
value: isNativeChatDefaultFor(settings, agent) ? 'chat' : 'cli',
onChange: (value) =>
void saveNativeChatProviderChoice({
agent,
on: value === 'chat',
fallbackSettings: settings,
updateSettings
})
}
}
+4
View File
@@ -405,6 +405,10 @@
"readFailed": "Couldn’t read agent settings. Check the host connection and try again.",
"saveFailed": "Couldn’t save agent settings. Check the host connection and try again.",
"hostDefaults": "Defaults for new agents on the selected host.",
"defaultOpensAs": "Open {{value0}} as",
"defaultOpensAsChat": "Chat",
"defaultOpensAsCli": "CLI",
"defaultOpensAsScope": "Applies to new {{value0}} sessions launched from this computer, including on paired hosts. Matches its Providers switch.",
"save": "Save",
"clearDefault": "Clear default agent",
"autoPermissions": "Auto",
+4
View File
@@ -211,6 +211,10 @@
"readFailed": "No se pudo leer la configuración de agentes. Comprueba la conexión con el host e inténtalo de nuevo.",
"saveFailed": "No se pudo guardar la configuración de agentes. Comprueba la conexión con el host e inténtalo de nuevo.",
"hostDefaults": "Valores predeterminados para nuevos agentes en el host seleccionado.",
"defaultOpensAs": "Abrir {{value0}} como",
"defaultOpensAsChat": "Chat",
"defaultOpensAsCli": "CLI",
"defaultOpensAsScope": "Se aplica a las nuevas sesiones de {{value0}} iniciadas desde este equipo, incluso en hosts emparejados. Coincide con su interruptor en Proveedores.",
"save": "Guardar",
"clearDefault": "Borrar agente predeterminado"
},
+4
View File
@@ -315,6 +315,10 @@
"readFailed": "Impossible de lire les paramètres des agents. Vérifiez la connexion à l’hôte et réessayez.",
"saveFailed": "Impossible d’enregistrer les paramètres des agents. Vérifiez la connexion à l’hôte et réessayez.",
"hostDefaults": "Paramètres par défaut des nouveaux agents sur l’hôte sélectionné.",
"defaultOpensAs": "Ouvrir {{value0}} en tant que",
"defaultOpensAsChat": "Chat",
"defaultOpensAsCli": "CLI",
"defaultOpensAsScope": "S’applique aux nouvelles sessions {{value0}} lancées depuis cet ordinateur, y compris sur les hôtes associés. Correspond à son interrupteur dans Fournisseurs.",
"save": "Enregistrer",
"clearDefault": "Effacer l'agent par défaut"
},
+4
View File
@@ -211,6 +211,10 @@
"readFailed": "Agent設定を読み込めませんでした。ホストへの接続を確認して再試行してください。",
"saveFailed": "Agent設定を保存できませんでした。ホストへの接続を確認して再試行してください。",
"hostDefaults": "選択したホストで起動する新しいAgentの既定値。",
"defaultOpensAs": "{{value0}}の開き方",
"defaultOpensAsChat": "チャット",
"defaultOpensAsCli": "CLI",
"defaultOpensAsScope": "このコンピューターから起動する新しい{{value0}}セッション(ペアリングしたホスト上も含む)に適用されます。プロバイダーのスイッチと連動します。",
"save": "保存",
"clearDefault": "デフォルトの Agent をクリア"
},
+4
View File
@@ -211,6 +211,10 @@
"readFailed": "에이전트 설정을 읽을 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.",
"saveFailed": "에이전트 설정을 저장할 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.",
"hostDefaults": "선택한 호스트의 새 에이전트 기본값입니다.",
"defaultOpensAs": "{{value0}} 여는 방식",
"defaultOpensAsChat": "채팅",
"defaultOpensAsCli": "CLI",
"defaultOpensAsScope": "이 컴퓨터에서 시작하는 새 {{value0}} 세션(페어링된 호스트 포함)에 적용됩니다. 공급자의 스위치와 같습니다.",
"save": "저장",
"clearDefault": "기본 에이전트 지우기"
},
+4
View File
@@ -211,6 +211,10 @@
"readFailed": "无法读取智能体设置。请检查主机连接并重试。",
"saveFailed": "无法保存智能体设置。请检查主机连接并重试。",
"hostDefaults": "所选主机上新智能体的默认设置。",
"defaultOpensAs": "{{value0}} 的打开方式",
"defaultOpensAsChat": "聊天",
"defaultOpensAsCli": "CLI",
"defaultOpensAsScope": "适用于从这台电脑启动的新 {{value0}} 会话,包括在已配对主机上启动的会话。与提供方中的开关一致。",
"save": "保存",
"clearDefault": "清除默认智能体"
},