mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
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:
@@ -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', () => {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user