diff --git a/src/renderer/src/components/NewWorkspaceComposerModal.tsx b/src/renderer/src/components/NewWorkspaceComposerModal.tsx index 763570fd6d8..b13712a7f9e 100644 --- a/src/renderer/src/components/NewWorkspaceComposerModal.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerModal.tsx @@ -191,9 +191,12 @@ function QuickTabBody({ entry?: AgentPickerEntry ) => { setQuickAgentOverride(agent) - setQuickAgentPick({ agent, surface, draft, entry, contextKey: observation.contextKey }) + // Orca Cloud is terminal-only; keeping its surface would pin the agent to a terminal locally. + setQuickAgentPick( + orcaCloud ? { agent } : { agent, surface, draft, entry, contextKey: observation.contextKey } + ) }, - [observation.contextKey] + [observation.contextKey, orcaCloud] ) const handleCreate = useCallback(async (): Promise => { diff --git a/src/renderer/src/components/new-workspace/use-composer-quick-agent.test.tsx b/src/renderer/src/components/new-workspace/use-composer-quick-agent.test.tsx new file mode 100644 index 00000000000..59fe2af4b93 --- /dev/null +++ b/src/renderer/src/components/new-workspace/use-composer-quick-agent.test.tsx @@ -0,0 +1,101 @@ +// @vitest-environment happy-dom +import { cleanup, renderHook } from '@testing-library/react' +import { useState } from 'react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import { ORCA_CLOUD_IMAGE_AGENTS } from '../../../../shared/hosted-sandbox/orca-cloud-image-agents' +import type { EphemeralVmRecipeOption } from './new-workspace-composer-card-props' + +vi.mock('@/store/index', () => ({ useAppStore: { getState: () => ({}) } })) +vi.mock('@/lib/host-chat-program', () => ({ agentChoicePending: () => false })) +// This desktop has aider and claude; the host offers exactly what it detected. +vi.mock('@/lib/host-chat-program-acquisition', () => ({ + useAgentsOfferedForLaunch: (_workspace: unknown, detected: ReadonlySet | null) => + detected +})) + +import { useComposerQuickAgent } from './use-composer-quick-agent' + +afterEach(cleanup) + +const orcaCloudRecipe: EphemeralVmRecipeOption = { + id: 'orca-cloud', + name: 'Orca Cloud', + create: 'orca-cloud', + provider: 'orca-cloud' +} +const otherRecipe: EphemeralVmRecipeOption = { id: 'custom', name: 'Custom', create: './vm.sh' } + +function renderComposerAgent(props: { + recipe: EphemeralVmRecipeOption | null + override?: TuiAgent | null + settings?: Pick +}) { + return renderHook( + ({ recipe }: { recipe: EphemeralVmRecipeOption | null }) => { + const [override, setOverride] = useState(props.override) + return { + override, + ...useComposerQuickAgent( + { + agentRows: null, + detectedAgentIds: new Set(['aider', 'claude']), + repoId: 'repo-1', + selectedRepoIsGit: true, + selectedRepoExecutionHostId: null, + ephemeralVmRecipes: [orcaCloudRecipe, otherRecipe], + selectedEphemeralVmRecipeId: recipe?.id ?? null + }, + props.settings ?? null, + override, + setOverride + ) + } + }, + { initialProps: { recipe: props.recipe } } + ) +} + +describe('useComposerQuickAgent', () => { + it('offers only the image agents and an enabled terminal-only picker for Orca Cloud', () => { + const { result } = renderComposerAgent({ + recipe: orcaCloudRecipe, + settings: { defaultTuiAgent: 'aider' } + }) + expect(result.current.orcaCloud).toBe(true) + expect(result.current.quickAgent).toBe('claude') + expect([...(result.current.chipAgentIds ?? [])]).toEqual([...ORCA_CLOUD_IMAGE_AGENTS]) + expect(result.current.orcaCloudModel?.state).toBe('settled') + expect(result.current.orcaCloudModel?.rows.every((row) => row.kind === 'terminal')).toBe(true) + }) + + it('never submits a local-only pick to Orca Cloud, and restores it on the local host', () => { + const { result, rerender } = renderComposerAgent({ recipe: orcaCloudRecipe, override: 'aider' }) + expect(result.current.quickAgent).toBe('claude') + // The pick itself is kept, so going back to this desktop restores it. + expect(result.current.override).toBe('aider') + rerender({ recipe: null }) + expect(result.current.orcaCloud).toBe(false) + expect(result.current.quickAgent).toBe('aider') + }) + + it('keeps a default the image ships', () => { + const { result } = renderComposerAgent({ + recipe: orcaCloudRecipe, + settings: { defaultTuiAgent: 'gemini' } + }) + expect(result.current.quickAgent).toBe('gemini') + }) + + it('leaves other VM recipes on this host inventory', () => { + const { result } = renderComposerAgent({ + recipe: otherRecipe, + settings: { defaultTuiAgent: 'aider' } + }) + expect(result.current.orcaCloud).toBe(false) + expect(result.current.orcaCloudModel).toBeNull() + expect(result.current.quickAgent).toBe('aider') + expect([...(result.current.chipAgentIds ?? [])].sort()).toEqual(['aider', 'claude']) + }) +}) 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 index 1eff6c49da8..e4570289ffb 100644 --- a/src/renderer/src/components/new-workspace/use-composer-quick-agent.ts +++ b/src/renderer/src/components/new-workspace/use-composer-quick-agent.ts @@ -13,7 +13,7 @@ import { 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 type { NewTabAgentMenuSettings } from '../../../../shared/agent-launch/picker/agent-launch-picker-rows' import { isOrcaCloudSelection } from './run-target-options' import type { NewWorkspaceComposerCardProps } from './new-workspace-composer-card-props' @@ -31,7 +31,7 @@ type ComposerQuickAgentProps = Pick< /** 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, + settings: NewTabAgentMenuSettings | null | undefined, quickAgentOverride: TuiAgent | null | undefined, setQuickAgentOverride: (agent: TuiAgent | null | undefined) => void ) { @@ -69,8 +69,9 @@ export function useComposerQuickAgent( 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. + // Repair invalid picks before children render them. Orca Cloud only derives its agent, so a + // local-only pick returns when the user switches back to a host that has it. + if (!orcaCloud && resolved.quickAgentOverride !== quickAgentOverride) { setQuickAgentOverride(resolved.quickAgentOverride) } const quickAgent = resolved.quickAgent