From 83f163e09f8d19319d6ed5f91fa373a74dedcd8f Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:03:50 -0700 Subject: [PATCH] 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 --- .../settings/AgentDefaultSetting.tsx | 34 ++++++++- .../settings/AgentLaunchSettingsCatalog.tsx | 17 +++++ .../settings/NativeChatProvidersSetting.tsx | 23 ++---- .../native-chat-provider-choice.test.tsx | 59 +++++++++++++++ .../settings/native-chat-provider-choice.ts | 74 +++++++++++++++++++ src/renderer/src/i18n/locales/en.json | 4 + src/renderer/src/i18n/locales/es.json | 4 + src/renderer/src/i18n/locales/fr.json | 4 + src/renderer/src/i18n/locales/ja.json | 4 + src/renderer/src/i18n/locales/ko.json | 4 + src/renderer/src/i18n/locales/zh.json | 4 + 11 files changed, 215 insertions(+), 16 deletions(-) create mode 100644 src/renderer/src/components/settings/native-chat-provider-choice.test.tsx create mode 100644 src/renderer/src/components/settings/native-chat-provider-choice.ts diff --git a/src/renderer/src/components/settings/AgentDefaultSetting.tsx b/src/renderer/src/components/settings/AgentDefaultSetting.tsx index c83887fba21..ed21c9d971d 100644 --- a/src/renderer/src/components/settings/AgentDefaultSetting.tsx +++ b/src/renderer/src/components/settings/AgentDefaultSetting.tsx @@ -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({ ) })} + {opensAs ? ( +
+ + 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') } + ]} + /> +

+ {translate( + 'settings.agents.defaultOpensAsScope', + 'Applies to new {{value0}} sessions launched from this computer, including on paired hosts. Matches its Providers switch.', + { value0: opensAs.agentLabel } + )} +

+
+ ) : null} ) } diff --git a/src/renderer/src/components/settings/AgentLaunchSettingsCatalog.tsx b/src/renderer/src/components/settings/AgentLaunchSettingsCatalog.tsx index 1f674228a7f..20982ffcd14 100644 --- a/src/renderer/src/components/settings/AgentLaunchSettingsCatalog.tsx +++ b/src/renderer/src/components/settings/AgentLaunchSettingsCatalog.tsx @@ -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({ diff --git a/src/renderer/src/components/settings/NativeChatProvidersSetting.tsx b/src/renderer/src/components/settings/NativeChatProvidersSetting.tsx index da426f076bc..bed0faaf52b 100644 --- a/src/renderer/src/components/settings/NativeChatProvidersSetting.tsx +++ b/src/renderer/src/components/settings/NativeChatProvidersSetting.tsx @@ -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 }) - }} + } /> {/* Chats sign in on the host they run on, under the program that serves them. */} diff --git a/src/renderer/src/components/settings/native-chat-provider-choice.test.tsx b/src/renderer/src/components/settings/native-chat-provider-choice.test.tsx new file mode 100644 index 00000000000..d429124df5d --- /dev/null +++ b/src/renderer/src/components/settings/native-chat-provider-choice.test.tsx @@ -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) { + const updateSettings = vi.fn() + store.settings = settings + render( + + ) + 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() + }) +}) diff --git a/src/renderer/src/components/settings/native-chat-provider-choice.ts b/src/renderer/src/components/settings/native-chat-provider-choice.ts new file mode 100644 index 00000000000..ee39e1c47fc --- /dev/null +++ b/src/renderer/src/components/settings/native-chat-provider-choice.ts @@ -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) => void | Promise +}): Promise { + 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) => void | Promise + 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 + }) + } +} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 889f88b7c67..ab8d0db9476 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -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", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index f4056104fe8..eb0ce0077b4 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -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" }, diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 6ca667a54c1..08d1b8e986a 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -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" }, diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 476dd5275b1..9f6d16c9a45 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -211,6 +211,10 @@ "readFailed": "Agent設定を読み込めませんでした。ホストへの接続を確認して再試行してください。", "saveFailed": "Agent設定を保存できませんでした。ホストへの接続を確認して再試行してください。", "hostDefaults": "選択したホストで起動する新しいAgentの既定値。", + "defaultOpensAs": "{{value0}}の開き方", + "defaultOpensAsChat": "チャット", + "defaultOpensAsCli": "CLI", + "defaultOpensAsScope": "このコンピューターから起動する新しい{{value0}}セッション(ペアリングしたホスト上も含む)に適用されます。プロバイダーのスイッチと連動します。", "save": "保存", "clearDefault": "デフォルトの Agent をクリア" }, diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 88438cb4542..1fbca31eb57 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -211,6 +211,10 @@ "readFailed": "에이전트 설정을 읽을 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.", "saveFailed": "에이전트 설정을 저장할 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.", "hostDefaults": "선택한 호스트의 새 에이전트 기본값입니다.", + "defaultOpensAs": "{{value0}} 여는 방식", + "defaultOpensAsChat": "채팅", + "defaultOpensAsCli": "CLI", + "defaultOpensAsScope": "이 컴퓨터에서 시작하는 새 {{value0}} 세션(페어링된 호스트 포함)에 적용됩니다. 공급자의 스위치와 같습니다.", "save": "저장", "clearDefault": "기본 에이전트 지우기" }, diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 900ed3aff66..2c7bbbb1486 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -211,6 +211,10 @@ "readFailed": "无法读取智能体设置。请检查主机连接并重试。", "saveFailed": "无法保存智能体设置。请检查主机连接并重试。", "hostDefaults": "所选主机上新智能体的默认设置。", + "defaultOpensAs": "{{value0}} 的打开方式", + "defaultOpensAsChat": "聊天", + "defaultOpensAsCli": "CLI", + "defaultOpensAsScope": "适用于从这台电脑启动的新 {{value0}} 会话,包括在已配对主机上启动的会话。与提供方中的开关一致。", "save": "保存", "clearDefault": "清除默认智能体" },