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
+ }
+}