fix(cloud): W10-1: the Orca Cloud agent picker says when it substitutes an agent; fixtures; mobile chip labels

- When the user's default or a local pick isn't on the Orca Cloud image, one muted line under the
  picker says so: '{{agent}} isn't available on Orca Cloud yet, so {{fallback}} is selected.' It
  goes away once the user picks an image agent.
- Fixtures (dark/light): closed with Orca Cloud, open with the six agents, open with two disabled,
  and the fallback line.
- Mobile chips are labelled with the catalog name plus ', selected' on the chosen one.
This commit is contained in:
Neil
2026-11-10 05:01:58 -08:00
parent 4ca180d5ae
commit 32d6ada8bb
11 changed files with 177 additions and 11 deletions
+11 -1
View File
@@ -12,7 +12,8 @@ vi.mock('../components/MobileAgentIcon', () => ({
MobileAgentIcon: 'MobileAgentIcon'
}))
vi.mock('../i18n/use-mobile-translation', () => ({
useMobileTranslation: () => (_key: string, fallback: string) => fallback
useMobileTranslation: () => (_key: string, fallback: string, values?: Record<string, string>) =>
fallback.replace(/\{\{(\w+)\}\}/g, (_match, name: string) => values?.[name] ?? '')
}))
vi.mock('../theme/use-mobile-color-scheme', () => ({
useMobileColorScheme: () => 'dark'
@@ -69,6 +70,15 @@ describe('CloudAgentPicker', () => {
expect(group.props.accessibilityLabel).toBe('Agent')
const checked = radios(tree).filter((node) => node.props.accessibilityState.checked)
expect(checked.map(iconOf)).toEqual(['gemini'])
// Each chip says its name, plus its state when selected, with catalog names.
expect(radios(tree).map((node) => node.props.accessibilityLabel)).toEqual([
'Claude',
'Codex',
'OpenCode',
'Gemini, selected',
'Cursor',
'GitHub Copilot'
])
})
it('reports the pressed agent and holds every chip while disabled', () => {
+6
View File
@@ -36,6 +36,12 @@ export function CloudAgentPicker(props: {
<Pressable
key={id}
accessibilityRole="radio"
// Name plus state, so the chip reads the same in any order the wrap puts it in.
accessibilityLabel={
checked
? t('cloud.agent.chipSelected', '{{agent}}, selected', { agent: label })
: label
}
accessibilityState={{ checked, disabled: props.disabled }}
disabled={props.disabled}
onPress={() => props.onChange(id)}
+2 -1
View File
@@ -4968,7 +4968,8 @@
"deviceLabelConnected": "{{device}}, connected {{date}}"
},
"agent": {
"label": "Agent"
"label": "Agent",
"chipSelected": "{{agent}}, selected"
},
"started": {
"title": "{{agent}} started with your prompt",
@@ -158,12 +158,8 @@ function QuickTabBody({
const [quickAgentOverride, setQuickAgentOverride] = useState<TuiAgent | null | undefined>(
modalData.recovery?.agent
)
const { orcaCloud, orcaCloudModel, quickAgent, chipAgentIds } = useComposerQuickAgent(
cardProps,
settings,
quickAgentOverride,
setQuickAgentOverride
)
const { orcaCloud, orcaCloudModel, quickAgent, chipAgentIds, agentNotice } =
useComposerQuickAgent(cardProps, settings, quickAgentOverride, setQuickAgentOverride)
// Why keyed by agent: a pick repaired to another agent must not carry the old one's surface.
const [quickAgentPick, setQuickAgentPick] = useState<
Omit<NonNullable<ComposerModalData['recovery']>, 'note'>
@@ -361,6 +357,7 @@ function QuickTabBody({
// Chips list offered agents, keeping a pick whose chat is still being learned.
detectedAgentIds={chipAgentIds}
createDisabled={createDisabled}
agentNotice={agentNotice}
agentSurfaceRows={
agentMenuModel
? {
@@ -12,6 +12,7 @@ type NewWorkspaceComposerAgentSectionProps = Pick<
NewWorkspaceComposerCardProps,
| 'quickAgent'
| 'agentSurfaceRows'
| 'agentNotice'
| 'agentPickerDraft'
| 'agentSetupDisabled'
| 'selectedRepoIsGit'
@@ -36,6 +37,7 @@ type NewWorkspaceComposerAgentSectionProps = Pick<
export function NewWorkspaceComposerAgentSection({
quickAgent,
agentSurfaceRows,
agentNotice,
agentPickerDraft,
agentSetupDisabled,
selectedRepoIsGit,
@@ -100,6 +102,7 @@ export function NewWorkspaceComposerAgentSection({
onSelectBlankTerminal={() => onQuickAgentChange(null)}
onPick={(agent, draft, entry) => onQuickAgentChange(agent, draft?.surface, draft, entry)}
/>
{agentNotice ? <p className="text-xs text-muted-foreground">{agentNotice}</p> : null}
</div>
<div className="!mb-2">
@@ -0,0 +1,76 @@
// The composer's agent picker with Orca Cloud as the run target: the image's six agents, as installed.
import React from 'react'
import { defineFixture } from '../../component-fixtures/component-fixture'
import { AgentLaunchPicker } from '@/components/agent/AgentLaunchPicker'
import { orcaCloudAgentMenuModel } from '@/lib/orca-cloud/orca-cloud-agent-picker'
import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent'
const noop = (): void => {}
const settle = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 80))
function Picker(props: {
agent: TuiAgent
disabled?: TuiAgent[]
notice?: string
}): React.JSX.Element {
const model = orcaCloudAgentMenuModel({
defaultTuiAgent: 'claude',
disabledTuiAgents: props.disabled ?? [],
agentCmdOverrides: {},
agentDefaultArgs: {},
agentDefaultEnv: {}
} as unknown as Parameters<typeof orcaCloudAgentMenuModel>[0])
return (
<div className="h-[460px] bg-background p-4">
<div className="w-[420px] space-y-1">
<label className="text-xs font-medium text-muted-foreground">Agent [Optional]</label>
<AgentLaunchPicker
model={model}
agent={props.agent}
matchTriggerWidth
emptyLabel="Blank Terminal"
defaultAgent="claude"
onSetDefault={noop}
onSelectBlankTerminal={noop}
onPick={noop}
/>
{props.notice ? <p className="text-xs text-muted-foreground">{props.notice}</p> : null}
</div>
</div>
)
}
const open = async (frame: HTMLElement): Promise<void> => {
// Radix menus open on pointerdown, not click.
frame
.querySelector<HTMLButtonElement>('button[aria-haspopup]')
?.dispatchEvent(
new PointerEvent('pointerdown', { bubbles: true, button: 0, pointerType: 'mouse' })
)
await settle()
}
export const closed = defineFixture({ width: 480, render: () => <Picker agent="claude" /> })
export const openSix = defineFixture({
width: 480,
render: () => <Picker agent="claude" />,
interact: open
})
export const openTwoDisabled = defineFixture({
width: 480,
render: () => <Picker agent="claude" disabled={['cursor', 'copilot']} />,
interact: open
})
/** The user's default (Aider) isn't on the image: said once under the picker, not swapped silently. */
export const fallback = defineFixture({
width: 480,
render: () => (
<Picker
agent="claude"
notice="Aider isn’t available on Orca Cloud yet, so Claude is selected."
/>
)
})
@@ -48,6 +48,8 @@ export type NewWorkspaceComposerCardProps = {
nameInputRef?: React.RefObject<HTMLInputElement | null>
quickAgent: TuiAgent | null
agentSurfaceRows?: AgentComboboxSurfaceRows
/** One muted line under the agent picker (Orca Cloud: why another agent is selected). */
agentNotice?: string
agentPickerDraft?: AgentPickerDraft
agentSetupDisabled?: boolean
onQuickAgentChange: (
@@ -1,5 +1,7 @@
import { useMemo } from 'react'
import { useAppStore } from '@/store/index'
import { useTranslate } from '@/i18n/use-translate'
import { formatAgentTypeLabel } from '../../../../shared/agent-status/presentation/agent-type-label'
import { agentChoicePending } from '@/lib/host-chat-program'
import { useAgentsOfferedForLaunch } from '@/lib/host-chat-program-acquisition'
import {
@@ -9,6 +11,7 @@ import {
} from '@/lib/quick-workspace-agent-selection'
import {
ORCA_CLOUD_AGENT_IDS,
orcaCloudAgentFallback,
orcaCloudAgentMenuModel,
pickOrcaCloudAgent
} from '@/lib/orca-cloud/orca-cloud-agent-picker'
@@ -35,6 +38,7 @@ export function useComposerQuickAgent(
quickAgentOverride: TuiAgent | null | undefined,
setQuickAgentOverride: (agent: TuiAgent | null | undefined) => void
) {
const t = useTranslate()
// The picker rows' workspace when known, so the offered list and the rows read one host.
const launchWorkspace =
cardProps.agentRows?.target.workspace ?? composerLaunchWorkspace(cardProps)
@@ -79,5 +83,23 @@ export function useComposerQuickAgent(
const chipAgentIds =
offeredAgentIds &&
new Set([...offeredAgentIds, ...(quickAgent && choicePending(quickAgent) ? [quickAgent] : [])])
return { orcaCloud, orcaCloudModel, quickAgent, chipAgentIds }
// Said, not silent: the agent the user wanted isn't on the image, so another one is selected.
const orcaCloudFallback = orcaCloud
? orcaCloudAgentFallback({
override: quickAgentOverride,
defaultAgent: settings?.defaultTuiAgent,
selected: quickAgent
})
: null
const agentNotice = orcaCloudFallback
? t(
'components.newWorkspace.orcaCloudAgentFallback',
'{{agent}} isn’t available on Orca Cloud yet, so {{fallback}} is selected.',
{
agent: formatAgentTypeLabel(orcaCloudFallback.wanted),
fallback: formatAgentTypeLabel(orcaCloudFallback.selected)
}
)
: undefined
return { orcaCloud, orcaCloudModel, quickAgent, chipAgentIds, agentNotice }
}
+2 -1
View File
@@ -22944,7 +22944,8 @@
"yamlSource": "orca.yaml",
"bothSources": "orca.yaml + local",
"localSource": "local settings"
}
},
"orcaCloudAgentFallback": "{{agent}} isn’t available on Orca Cloud yet, so {{fallback}} is selected."
},
"JiraIssueWorkspace": {
"pendingChange": "Another issue change is in progress."
@@ -4,7 +4,8 @@ import { resolveQuickWorkspaceAgentSelection } from '@/lib/quick-workspace-agent
import {
ORCA_CLOUD_AGENT_IDS,
orcaCloudAgentMenuModel,
pickOrcaCloudAgent
pickOrcaCloudAgent,
orcaCloudAgentFallback
} from './orca-cloud-agent-picker'
describe('pickOrcaCloudAgent', () => {
@@ -85,3 +86,29 @@ describe('Orca Cloud composer selection repair', () => {
).toEqual({ quickAgent: 'cursor', quickAgentOverride: 'cursor' })
})
})
describe('orcaCloudAgentFallback', () => {
it('names the agent the user wanted when the image does not ship it', () => {
expect(
orcaCloudAgentFallback({ override: 'aider', defaultAgent: 'claude', selected: 'claude' })
).toEqual({ wanted: 'aider', selected: 'claude' })
expect(
orcaCloudAgentFallback({ override: undefined, defaultAgent: 'goose', selected: 'claude' })
).toEqual({ wanted: 'goose', selected: 'claude' })
})
it('says nothing once the pick is on the image, for Blank Terminal, or with nothing selected', () => {
expect(
orcaCloudAgentFallback({ override: 'gemini', defaultAgent: 'goose', selected: 'gemini' })
).toBeNull()
expect(
orcaCloudAgentFallback({ override: undefined, defaultAgent: 'codex', selected: 'codex' })
).toBeNull()
expect(
orcaCloudAgentFallback({ override: null, defaultAgent: 'goose', selected: null })
).toBeNull()
expect(
orcaCloudAgentFallback({ override: undefined, defaultAgent: 'blank', selected: null })
).toBeNull()
})
})
@@ -57,3 +57,24 @@ export function orcaCloudAgentMenuModel(
defaultTuiAgent: settings?.defaultTuiAgent
}
}
/**
* The agent the user wanted (a local pick made before choosing Orca Cloud, or their default) when the
* image doesn't ship it and another agent is selected instead; null when nothing was substituted.
*/
export function orcaCloudAgentFallback(args: {
override: TuiAgent | null | undefined
defaultAgent: TuiAgent | 'blank' | null | undefined
selected: TuiAgent | null
}): { wanted: TuiAgent; selected: TuiAgent } | null {
const wanted =
args.override !== undefined && args.override !== null
? args.override
: args.override === null || args.defaultAgent === 'blank'
? null
: (args.defaultAgent ?? null)
if (!wanted || !args.selected || isOrcaCloudImageAgent(wanted) || wanted === args.selected) {
return null
}
return { wanted, selected: args.selected }
}