mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
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:
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -211,6 +211,10 @@
|
||||
"readFailed": "Agent設定を読み込めませんでした。ホストへの接続を確認して再試行してください。",
|
||||
"saveFailed": "Agent設定を保存できませんでした。ホストへの接続を確認して再試行してください。",
|
||||
"hostDefaults": "選択したホストで起動する新しいAgentの既定値。",
|
||||
"defaultOpensAs": "{{value0}}の開き方",
|
||||
"defaultOpensAsChat": "チャット",
|
||||
"defaultOpensAsCli": "CLI",
|
||||
"defaultOpensAsScope": "このコンピューターから起動する新しい{{value0}}セッション(ペアリングしたホスト上も含む)に適用されます。プロバイダーのスイッチと連動します。",
|
||||
"save": "保存",
|
||||
"clearDefault": "デフォルトの Agent をクリア"
|
||||
},
|
||||
|
||||
@@ -211,6 +211,10 @@
|
||||
"readFailed": "에이전트 설정을 읽을 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.",
|
||||
"saveFailed": "에이전트 설정을 저장할 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.",
|
||||
"hostDefaults": "선택한 호스트의 새 에이전트 기본값입니다.",
|
||||
"defaultOpensAs": "{{value0}} 여는 방식",
|
||||
"defaultOpensAsChat": "채팅",
|
||||
"defaultOpensAsCli": "CLI",
|
||||
"defaultOpensAsScope": "이 컴퓨터에서 시작하는 새 {{value0}} 세션(페어링된 호스트 포함)에 적용됩니다. 공급자의 스위치와 같습니다.",
|
||||
"save": "저장",
|
||||
"clearDefault": "기본 에이전트 지우기"
|
||||
},
|
||||
|
||||
@@ -211,6 +211,10 @@
|
||||
"readFailed": "无法读取智能体设置。请检查主机连接并重试。",
|
||||
"saveFailed": "无法保存智能体设置。请检查主机连接并重试。",
|
||||
"hostDefaults": "所选主机上新智能体的默认设置。",
|
||||
"defaultOpensAs": "{{value0}} 的打开方式",
|
||||
"defaultOpensAsChat": "聊天",
|
||||
"defaultOpensAsCli": "CLI",
|
||||
"defaultOpensAsScope": "适用于从这台电脑启动的新 {{value0}} 会话,包括在已配对主机上启动的会话。与提供方中的开关一致。",
|
||||
"save": "保存",
|
||||
"clearDefault": "清除默认智能体"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user