diff --git a/mobile/src/cloud/CloudAgentPicker.test.tsx b/mobile/src/cloud/CloudAgentPicker.test.tsx new file mode 100644 index 00000000000..b482871bd4f --- /dev/null +++ b/mobile/src/cloud/CloudAgentPicker.test.tsx @@ -0,0 +1,91 @@ +import { createElement } from 'react' +import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer' +import { describe, expect, it, vi } from 'vitest' + +vi.mock('react-native', () => ({ + Pressable: 'Pressable', + StyleSheet: { create: (styles: unknown) => styles }, + Text: 'Text', + View: 'View' +})) +vi.mock('../components/MobileAgentIcon', () => ({ + MobileAgentIcon: 'MobileAgentIcon' +})) +vi.mock('../i18n/use-mobile-translation', () => ({ + useMobileTranslation: () => (_key: string, fallback: string) => fallback +})) +vi.mock('../theme/use-mobile-color-scheme', () => ({ + useMobileColorScheme: () => 'dark' +})) + +import { + ORCA_CLOUD_DEFAULT_AGENT, + ORCA_CLOUD_IMAGE_AGENTS, + type OrcaCloudImageAgent +} from '../../../src/shared/hosted-sandbox/orca-cloud-image-agents' +import { CloudAgentPicker } from './CloudAgentPicker' + +function render(props: { + value: OrcaCloudImageAgent + onChange: (agent: OrcaCloudImageAgent) => void + disabled?: boolean +}): ReactTestRenderer { + const rendered: { tree: ReactTestRenderer | null } = { tree: null } + act(() => { + rendered.tree = create(createElement(CloudAgentPicker, props)) + }) + if (rendered.tree === null) { + throw new Error('the picker did not render') + } + return rendered.tree +} + +function radios(tree: ReactTestRenderer): ReactTestInstance[] { + return tree.root.findAll( + (node: ReactTestInstance) => + String(node.type) === 'Pressable' && node.props.accessibilityRole === 'radio' + ) +} + +function iconOf(node: ReactTestInstance): string { + return node.find((child: ReactTestInstance) => String(child.type) === 'MobileAgentIcon').props + .agentId +} + +describe('CloudAgentPicker', () => { + it('offers exactly the agents the Orca Cloud image ships, in image order', () => { + const tree = render({ + value: ORCA_CLOUD_DEFAULT_AGENT, + onChange: () => {} + }) + expect(radios(tree).map(iconOf)).toEqual([...ORCA_CLOUD_IMAGE_AGENTS]) + }) + + it('is one radio group with the current agent checked', () => { + const tree = render({ value: 'gemini', onChange: () => {} }) + const group = tree.root.find( + (node: ReactTestInstance) => node.props.accessibilityRole === 'radiogroup' + ) + expect(group.props.accessibilityLabel).toBe('Agent') + const checked = radios(tree).filter((node) => node.props.accessibilityState.checked) + expect(checked.map(iconOf)).toEqual(['gemini']) + }) + + it('reports the pressed agent and holds every chip while disabled', () => { + const picks: string[] = [] + const tree = render({ + value: 'claude', + onChange: (agent) => picks.push(agent) + }) + const cursor = radios(tree).find((node) => iconOf(node) === 'cursor')! + act(() => cursor.props.onPress()) + expect(picks).toEqual(['cursor']) + + const disabled = render({ + value: 'claude', + onChange: () => {}, + disabled: true + }) + expect(radios(disabled).every((node) => node.props.disabled === true)).toBe(true) + }) +}) diff --git a/mobile/src/cloud/CloudAgentPicker.tsx b/mobile/src/cloud/CloudAgentPicker.tsx index e4171470df6..23193a4ff17 100644 --- a/mobile/src/cloud/CloudAgentPicker.tsx +++ b/mobile/src/cloud/CloudAgentPicker.tsx @@ -1,27 +1,23 @@ import { Pressable, Text, View } from 'react-native' import { formatAgentTypeLabel } from '../../../src/shared/agent-status/presentation/agent-type-label' -import { ClaudeIcon, OpenAIIcon } from '../components/AgentIcons' +import { + ORCA_CLOUD_IMAGE_AGENTS, + type OrcaCloudImageAgent +} from '../../../src/shared/hosted-sandbox/orca-cloud-image-agents' +import { MobileAgentIcon } from '../components/MobileAgentIcon' import { useMobileTranslation } from '../i18n/use-mobile-translation' import { colors, radii, spacing } from '../theme/mobile-theme' import { themedStyleSheet } from '../theme/themed-styles' import { useMobileColorScheme } from '../theme/use-mobile-color-scheme' -/** The agent CLIs every Orca Cloud image ships. */ -const CLOUD_AGENTS = [ - { id: 'claude', Icon: ClaudeIcon }, - { id: 'codex', Icon: OpenAIIcon } -] as const - -export const CLOUD_DEFAULT_AGENT = 'claude' - /** Product names, from the app's one agent-name table. */ export function cloudAgentLabel(agent: string): string { return formatAgentTypeLabel(agent) } export function CloudAgentPicker(props: { - value: string - onChange: (agent: string) => void + value: OrcaCloudImageAgent + onChange: (agent: OrcaCloudImageAgent) => void disabled?: boolean }) { useMobileColorScheme() @@ -32,7 +28,8 @@ export function CloudAgentPicker(props: { accessibilityRole="radiogroup" accessibilityLabel={t('cloud.agent.label', 'Agent')} > - {CLOUD_AGENTS.map(({ id, Icon }) => { + {/* Only what the image ships: the VM does not exist yet, so nothing else could start. */} + {ORCA_CLOUD_IMAGE_AGENTS.map((id) => { const label = cloudAgentLabel(id) const checked = props.value === id return ( @@ -50,7 +47,7 @@ export function CloudAgentPicker(props: { pressed && styles.pressed ]} > - + {label} ) diff --git a/mobile/src/cloud/CloudPromptComposer.tsx b/mobile/src/cloud/CloudPromptComposer.tsx index 25887cb0375..a1b5f7d001f 100644 --- a/mobile/src/cloud/CloudPromptComposer.tsx +++ b/mobile/src/cloud/CloudPromptComposer.tsx @@ -4,7 +4,8 @@ import { Pressable, Text, TextInput, View } from 'react-native' import { useMobileTranslation } from '../i18n/use-mobile-translation' import { colors } from '../theme/mobile-theme' import { useMobileColorScheme } from '../theme/use-mobile-color-scheme' -import { CloudAgentPicker, CLOUD_DEFAULT_AGENT } from './CloudAgentPicker' +import { ORCA_CLOUD_DEFAULT_AGENT } from '../../../src/shared/hosted-sandbox/orca-cloud-image-agents' +import { CloudAgentPicker } from './CloudAgentPicker' import { cloudWorkspaceScreenStyles as styles } from './cloud-workspace-screen-styles' /** One card: the agent, the prompt, and Send. The send button keeps its shape in every state; when @@ -20,7 +21,7 @@ export function CloudPromptComposer(props: { useMobileColorScheme() const t = useMobileTranslation() const [draft, setDraft] = useState('') - const [agent, setAgent] = useState(CLOUD_DEFAULT_AGENT) + const [agent, setAgent] = useState(ORCA_CLOUD_DEFAULT_AGENT) const canSend = draft.trim() !== '' && !props.disabled const send = async () => { if (canSend && (await props.onSend(draft.trim(), agent))) { diff --git a/mobile/src/cloud/NewCloudWorkspaceScreen.tsx b/mobile/src/cloud/NewCloudWorkspaceScreen.tsx index 47abc85f8ca..cc69e9c6218 100644 --- a/mobile/src/cloud/NewCloudWorkspaceScreen.tsx +++ b/mobile/src/cloud/NewCloudWorkspaceScreen.tsx @@ -17,7 +17,8 @@ import { SettingsNavBar, SettingsNote, SettingsSection } from '../settings/setti import { colors, radii, spacing, typography } from '../theme/mobile-theme' import { themedStyleSheet } from '../theme/themed-styles' import { useMobileColorScheme } from '../theme/use-mobile-color-scheme' -import { CloudAgentPicker, CLOUD_DEFAULT_AGENT } from './CloudAgentPicker' +import { ORCA_CLOUD_DEFAULT_AGENT } from '../../../src/shared/hosted-sandbox/orca-cloud-image-agents' +import { CloudAgentPicker } from './CloudAgentPicker' import { CloudPhaseSteps } from './CloudPhaseSteps' import { createCloudWorkspace, renameCloudWorkspace } from './cloud-control-plane-client' import { enqueueCloudPrompt } from './cloud-prompt-queue' @@ -52,7 +53,7 @@ export function NewCloudWorkspaceScreen() { const existing = useCloudWorkspaces(useCloudEnrollment()).data const [repo, setRepo] = useState('') const [branch, setBranch] = useState('') - const [agent, setAgent] = useState(CLOUD_DEFAULT_AGENT) + const [agent, setAgent] = useState(ORCA_CLOUD_DEFAULT_AGENT) const [prompt, setPrompt] = useState('') const [size, setSize] = useState('standard') const [startedAt, setStartedAt] = useState(null) diff --git a/src/renderer/src/components/NewWorkspaceComposerModal.tsx b/src/renderer/src/components/NewWorkspaceComposerModal.tsx index 22daa3182dd..763570fd6d8 100644 --- a/src/renderer/src/components/NewWorkspaceComposerModal.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerModal.tsx @@ -9,8 +9,6 @@ import type { AgentPickerEntry } from '../../../shared/agent-launch/picker/agent-launch-picker' import { DirectWorkItemSetupDialog } from './new-workspace/DirectWorkItemSetupDialog' -import { isOrcaCloudSelection } from './new-workspace/run-target-options' -import { agentChoicePending } from '@/lib/host-chat-program' import type { AgentLaunchSurface } from '@/lib/agent-session/launch/routing/agent-session-launch-plan' import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useAppStore } from '@/store/index' @@ -28,13 +26,8 @@ import AgentSettingsDialog, { } from '@/components/agent/AgentSettingsDialog' import type { AddRepoDialogHostedController } from '@/components/sidebar/project-admission/host-selection/use-add-repo-hosted-controller' import { useComposerState } from '@/hooks/useComposerState' -import { - composerLaunchWorkspace, - quickWorkspacePickerInputs, - pickQuickWorkspaceAgent, - resolveQuickWorkspaceAgentSelection -} from '@/lib/quick-workspace-agent-selection' -import { useAgentsOfferedForLaunch } from '@/lib/host-chat-program-acquisition' +import { quickWorkspacePickerInputs } from '@/lib/quick-workspace-agent-selection' +import { useComposerQuickAgent } from './new-workspace/use-composer-quick-agent' import { shouldAllowComposerEnterSubmitTarget } from '@/lib/new-workspace-enter-guard' import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' import type { TuiAgent } from '../../../shared/agent-launch/catalog/tui-agent' @@ -165,38 +158,12 @@ function QuickTabBody({ const [quickAgentOverride, setQuickAgentOverride] = useState( modalData.recovery?.agent ) - // The picker rows' workspace when known, so the offered list and the rows read one host. - const launchWorkspace = - cardProps.agentRows?.target.workspace ?? composerLaunchWorkspace(cardProps) - const offeredAgentIds = useAgentsOfferedForLaunch(launchWorkspace, cardProps.detectedAgentIds) - const detected = cardProps.detectedAgentIds ? [...cardProps.detectedAgentIds] : null - const choicePending = (agent: TuiAgent): boolean => - agentChoicePending(useAppStore.getState(), agent, launchWorkspace, detected) - // Pending discovery preserves the existing catalog fallback; current facts settle the default. - const preferredQuickAgent = pickQuickWorkspaceAgent( - settings?.defaultTuiAgent, - cardProps.detectedAgentIds, - settings?.disabledTuiAgents, - offeredAgentIds, - choicePending - ) - // An explicit pick stays valid where it opens as a chat without its CLI. - const resolvedQuickAgentSelection = resolveQuickWorkspaceAgentSelection({ + const { orcaCloud, orcaCloudModel, quickAgent, chipAgentIds } = useComposerQuickAgent( + cardProps, + settings, quickAgentOverride, - preferredQuickAgent, - detectedAgentIds: cardProps.detectedAgentIds, - offeredAgentIds, - selectedChoiceUnresolved: Boolean(quickAgentOverride && choicePending(quickAgentOverride)), - disabledTuiAgents: settings?.disabledTuiAgents - }) - if (resolvedQuickAgentSelection.quickAgentOverride !== quickAgentOverride) { - // Repair invalid picks before children render an unavailable option. - setQuickAgentOverride(resolvedQuickAgentSelection.quickAgentOverride) - } - const quickAgent = resolvedQuickAgentSelection.quickAgent - const chipAgentIds = - offeredAgentIds && - new Set([...offeredAgentIds, ...(quickAgent && choicePending(quickAgent) ? [quickAgent] : [])]) + setQuickAgentOverride + ) // Why keyed by agent: a pick repaired to another agent must not carry the old one's surface. const [quickAgentPick, setQuickAgentPick] = useState< Omit, 'note'> @@ -213,6 +180,7 @@ function QuickTabBody({ : undefined const quickAgentSurface = quickAgentPick.agent === quickAgent ? quickAgentPick.surface : undefined const launchSelection = observation.selectionFor(quickAgent, quickAgentSurface) + const agentMenuModel = orcaCloudModel ?? (cardProps.agentRows ? observation.model : null) const createDisabled = formDisabled || !launchSelection.ready const handleQuickAgentChange = useCallback( @@ -322,7 +290,7 @@ function QuickTabBody({ // An Orca Cloud sandbox is a whole workspace, not a local worktree. const primaryActionLabel = isFolderWorkspaceTarget ? getFolderWorkspacePrimaryActionLabel(t) - : cardProps.selectedRepoIsGit && !isOrcaCloudSelection(cardProps) + : cardProps.selectedRepoIsGit && !orcaCloud ? t('auto.components.NewWorkspaceComposerModal.createWorktree', 'Create worktree') : t('auto.components.NewWorkspaceComposerModal.createWorkspace', 'Create workspace') @@ -380,6 +348,8 @@ function QuickTabBody({ quickAgent={quickAgent} agentPickerDraft={pickerDraft} agentSetupDisabled={ + // Orca Cloud's per-agent defaults live on a VM that does not exist yet. + orcaCloud || observation.model.state !== 'settled' || observation.model.pickerContext?.executionHostId !== 'local' } @@ -389,9 +359,9 @@ function QuickTabBody({ detectedAgentIds={chipAgentIds} createDisabled={createDisabled} agentSurfaceRows={ - cardProps.agentRows + agentMenuModel ? { - model: observation.model, + model: agentMenuModel, selection: launchSelection, surface: quickAgentSurface, contextKey: observation.contextKey, diff --git a/src/renderer/src/components/new-workspace/use-composer-quick-agent.ts b/src/renderer/src/components/new-workspace/use-composer-quick-agent.ts new file mode 100644 index 00000000000..1eff6c49da8 --- /dev/null +++ b/src/renderer/src/components/new-workspace/use-composer-quick-agent.ts @@ -0,0 +1,82 @@ +import { useMemo } from 'react' +import { useAppStore } from '@/store/index' +import { agentChoicePending } from '@/lib/host-chat-program' +import { useAgentsOfferedForLaunch } from '@/lib/host-chat-program-acquisition' +import { + composerLaunchWorkspace, + pickQuickWorkspaceAgent, + resolveQuickWorkspaceAgentSelection +} from '@/lib/quick-workspace-agent-selection' +import { + ORCA_CLOUD_AGENT_IDS, + orcaCloudAgentMenuModel, + pickOrcaCloudAgent +} from '@/lib/orca-cloud/orca-cloud-agent-picker' +import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import { isOrcaCloudSelection } from './run-target-options' +import type { NewWorkspaceComposerCardProps } from './new-workspace-composer-card-props' + +type ComposerQuickAgentProps = Pick< + NewWorkspaceComposerCardProps, + | 'agentRows' + | 'detectedAgentIds' + | 'repoId' + | 'selectedRepoIsGit' + | 'selectedRepoExecutionHostId' + | 'ephemeralVmRecipes' + | 'selectedEphemeralVmRecipeId' +> + +/** The composer's agent: the user's pick when the launch host can start it, else the default. */ +export function useComposerQuickAgent( + cardProps: ComposerQuickAgentProps, + settings: GlobalSettings | null | undefined, + quickAgentOverride: TuiAgent | null | undefined, + setQuickAgentOverride: (agent: TuiAgent | null | undefined) => void +) { + // The picker rows' workspace when known, so the offered list and the rows read one host. + const launchWorkspace = + cardProps.agentRows?.target.workspace ?? composerLaunchWorkspace(cardProps) + const hostOfferedAgentIds = useAgentsOfferedForLaunch(launchWorkspace, cardProps.detectedAgentIds) + // Orca Cloud has no VM yet: the image's agents are its inventory, never this desktop's CLIs. + const orcaCloud = isOrcaCloudSelection(cardProps) + const orcaCloudModel = useMemo( + () => (orcaCloud ? orcaCloudAgentMenuModel(settings) : null), + [orcaCloud, settings] + ) + const detectedAgentIds = orcaCloud ? ORCA_CLOUD_AGENT_IDS : cardProps.detectedAgentIds + const offeredAgentIds = orcaCloud ? ORCA_CLOUD_AGENT_IDS : hostOfferedAgentIds + const detected = detectedAgentIds ? [...detectedAgentIds] : null + const choicePending = (agent: TuiAgent): boolean => + !orcaCloud && agentChoicePending(useAppStore.getState(), agent, launchWorkspace, detected) + // Pending discovery preserves the existing catalog fallback; current facts settle the default. + const preferredQuickAgent = orcaCloud + ? pickOrcaCloudAgent(settings?.defaultTuiAgent, settings?.disabledTuiAgents) + : pickQuickWorkspaceAgent( + settings?.defaultTuiAgent, + detectedAgentIds, + settings?.disabledTuiAgents, + offeredAgentIds, + choicePending + ) + // An explicit pick stays valid where it opens as a chat without its CLI. + const resolved = resolveQuickWorkspaceAgentSelection({ + quickAgentOverride, + preferredQuickAgent, + detectedAgentIds, + offeredAgentIds, + selectedChoiceUnresolved: Boolean(quickAgentOverride && choicePending(quickAgentOverride)), + disabledTuiAgents: settings?.disabledTuiAgents + }) + if (resolved.quickAgentOverride !== quickAgentOverride) { + // Repair invalid picks (e.g. a local-only agent once Orca Cloud is chosen) before children render it. + setQuickAgentOverride(resolved.quickAgentOverride) + } + const quickAgent = resolved.quickAgent + // Chips list offered agents, keeping a pick whose chat is still being learned. + const chipAgentIds = + offeredAgentIds && + new Set([...offeredAgentIds, ...(quickAgent && choicePending(quickAgent) ? [quickAgent] : [])]) + return { orcaCloud, orcaCloudModel, quickAgent, chipAgentIds } +} diff --git a/src/renderer/src/lib/orca-cloud/orca-cloud-agent-picker.test.ts b/src/renderer/src/lib/orca-cloud/orca-cloud-agent-picker.test.ts new file mode 100644 index 00000000000..e271d1bdbd5 --- /dev/null +++ b/src/renderer/src/lib/orca-cloud/orca-cloud-agent-picker.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest' +import { ORCA_CLOUD_IMAGE_AGENTS } from '../../../../shared/hosted-sandbox/orca-cloud-image-agents' +import { resolveQuickWorkspaceAgentSelection } from '@/lib/quick-workspace-agent-selection' +import { + ORCA_CLOUD_AGENT_IDS, + orcaCloudAgentMenuModel, + pickOrcaCloudAgent +} from './orca-cloud-agent-picker' + +describe('pickOrcaCloudAgent', () => { + it('keeps a default the image ships', () => { + expect(pickOrcaCloudAgent('gemini')).toBe('gemini') + expect(pickOrcaCloudAgent('copilot')).toBe('copilot') + }) + + it('falls back to the cloud default for an agent the image does not ship', () => { + expect(pickOrcaCloudAgent('aider')).toBe('claude') + expect(pickOrcaCloudAgent('goose')).toBe('claude') + expect(pickOrcaCloudAgent(undefined)).toBe('claude') + expect(pickOrcaCloudAgent(null)).toBe('claude') + }) + + it('keeps Blank Terminal as the default when the user chose it', () => { + expect(pickOrcaCloudAgent('blank')).toBeNull() + }) + + it('skips disabled agents, staying inside the image', () => { + expect(pickOrcaCloudAgent('gemini', ['gemini'])).toBe('claude') + expect(pickOrcaCloudAgent('aider', ['claude'])).toBe('codex') + expect(pickOrcaCloudAgent('aider', [...ORCA_CLOUD_IMAGE_AGENTS])).toBeNull() + }) +}) + +describe('orcaCloudAgentMenuModel', () => { + it('lists exactly the image agents as terminal rows, default first', () => { + const model = orcaCloudAgentMenuModel({ defaultTuiAgent: 'opencode' }) + expect(model.state).toBe('settled') + expect(model.rows.every((row) => row.kind === 'terminal')).toBe(true) + expect(model.rows.map((row) => row.agent)).toEqual([ + 'opencode', + 'claude', + 'codex', + 'copilot', + 'gemini', + 'cursor' + ]) + expect(new Set(model.rows.map((row) => row.agent))).toEqual(new Set(ORCA_CLOUD_IMAGE_AGENTS)) + expect(model.detection.detectedIds).toEqual([...ORCA_CLOUD_IMAGE_AGENTS]) + expect(model.defaultTuiAgent).toBe('opencode') + }) + + it('omits agents the user disabled and ignores a local-only default', () => { + const model = orcaCloudAgentMenuModel({ + defaultTuiAgent: 'aider', + disabledTuiAgents: ['codex'] + }) + const agents = model.rows.map((row) => row.agent) + expect(agents).not.toContain('codex') + expect(agents).not.toContain('aider') + expect(agents).toHaveLength(ORCA_CLOUD_IMAGE_AGENTS.length - 1) + }) +}) + +describe('Orca Cloud composer selection repair', () => { + it('repairs a local-only pick to the cloud default instead of submitting it', () => { + const preferredQuickAgent = pickOrcaCloudAgent('aider') + expect( + resolveQuickWorkspaceAgentSelection({ + quickAgentOverride: 'aider', + preferredQuickAgent, + detectedAgentIds: ORCA_CLOUD_AGENT_IDS, + offeredAgentIds: ORCA_CLOUD_AGENT_IDS + }) + ).toEqual({ quickAgent: 'claude', quickAgentOverride: 'claude' }) + }) + + it('keeps a pick the image ships', () => { + expect( + resolveQuickWorkspaceAgentSelection({ + quickAgentOverride: 'cursor', + preferredQuickAgent: 'claude', + detectedAgentIds: ORCA_CLOUD_AGENT_IDS, + offeredAgentIds: ORCA_CLOUD_AGENT_IDS + }) + ).toEqual({ quickAgent: 'cursor', quickAgentOverride: 'cursor' }) + }) +}) diff --git a/src/renderer/src/lib/orca-cloud/orca-cloud-agent-picker.ts b/src/renderer/src/lib/orca-cloud/orca-cloud-agent-picker.ts new file mode 100644 index 00000000000..955bb49d95f --- /dev/null +++ b/src/renderer/src/lib/orca-cloud/orca-cloud-agent-picker.ts @@ -0,0 +1,59 @@ +import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent' +import { pickTuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent-selection' +import { + buildNewTabAgentMenuRows, + type NewTabAgentMenuSettings +} from '../../../../shared/agent-launch/picker/agent-launch-picker-rows' +import { + ORCA_CLOUD_DEFAULT_AGENT, + ORCA_CLOUD_IMAGE_AGENTS, + isOrcaCloudImageAgent +} from '../../../../shared/hosted-sandbox/orca-cloud-image-agents' +import type { NewTabAgentMenuModel } from '@/components/tab-bar/new-tab-agent-menu-model' + +/** + * The agents an Orca Cloud workspace can start before its VM exists: the image ships them, so the + * composer treats them as installed instead of asking this desktop which CLIs it has. + */ +export const ORCA_CLOUD_AGENT_IDS: ReadonlySet = new Set(ORCA_CLOUD_IMAGE_AGENTS) + +/** The user's default when the image ships it (or Blank Terminal), else the cloud default. */ +export function pickOrcaCloudAgent( + preferred: TuiAgent | 'blank' | null | undefined, + disabledTuiAgents?: Iterable | null +): TuiAgent | null { + const start = + preferred === 'blank' || (preferred && isOrcaCloudImageAgent(preferred)) + ? preferred + : ORCA_CLOUD_DEFAULT_AGENT + return pickTuiAgent(start, ORCA_CLOUD_IMAGE_AGENTS, disabledTuiAgents) +} + +/** + * Picker rows for the Orca Cloud image. Terminal only: whether an agent's chat runs on the VM is + * not knowable before the VM exists, and the terminal is what create launches there today. + */ +export function orcaCloudAgentMenuModel( + settings: NewTabAgentMenuSettings | null | undefined +): Extract { + const detectedIds: TuiAgent[] = [...ORCA_CLOUD_IMAGE_AGENTS] + return { + state: 'settled', + rows: buildNewTabAgentMenuRows({ + settings, + detectedOnHost: detectedIds, + chatAgents: [], + hostChatAnswer: () => 'no-chats', + chatProgram: () => 'unknown' + }), + detection: { + detectedIds, + isLoading: false, + detectionFailed: false, + isRefreshing: false, + needsServerUpdate: false, + refresh: async () => detectedIds + }, + defaultTuiAgent: settings?.defaultTuiAgent + } +}