fix(cloud): setup page re-reads availability; agent step always offers Set up once

This commit is contained in:
Neil
2026-11-10 18:53:31 -08:00
parent f892a6d3f6
commit 81e8a44530
3 changed files with 46 additions and 26 deletions
@@ -12,6 +12,7 @@ import type { CloudSetupStep } from '@/components/new-workspace/run-on/cloud-sta
import { CloudSetupErrorNotice } from '@/components/new-workspace/cloud-setup/CloudSetupErrorNotice'
import { sessionDetailCopy } from '@/components/new-workspace/run-on/cloud-setup-session-copy'
import type { CloudSetupFlow } from './use-cloud-setup-flow'
import { openOrcaCloudAgentAccounts } from '@/components/new-workspace/OrcaCloudAgentSetupRow'
export type CloudSetupPageProps = {
flow: CloudSetupFlow
@@ -286,11 +287,9 @@ function StepActions({ flow }: { flow: CloudSetupFlow }): React.JSX.Element | nu
case 'agent':
return (
<div className="flex flex-wrap items-center gap-2">
{flow.current?.action ? (
<Button type="button" onClick={flow.current.action.onClick}>
{flow.current.action.label}
</Button>
) : null}
<Button type="button" onClick={flow.current?.action?.onClick ?? openOrcaCloudAgentAccounts}>
{flow.current?.action?.label ?? t('cloudSetupPage.agent.setUp', 'Set up once')}
</Button>
<Button type="button" variant="ghost" onClick={flow.skipAgent}>
{t('cloudSetupPage.agent.skip', 'Skip for now')}
</Button>
@@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { CloudSetupStep } from '@/components/new-workspace/run-on/cloud-start-model'
import { actionForStep, cloudSetupPageModel, sessionNeedsFrom } from './cloud-setup-page-model'
import { effectiveAvailability } from './use-cloud-setup-flow'
describe('cloudSetupPageModel', () => {
it('walks one step at a time: the first unfinished step is current', () => {
@@ -44,14 +43,3 @@ describe('cloudSetupPageModel', () => {
expect(actionForStep({ id: 'github', state: 'loading' })).toBe('checking')
})
})
describe('effectiveAvailability', () => {
it('a sign-in that lands upgrades the snapshot from Start', () => {
expect(effectiveAvailability('needs-sign-in', { state: 'signed-out' })).toBe('needs-sign-in')
expect(effectiveAvailability('needs-sign-in', { state: 'loading' })).toBe('needs-sign-in')
expect(
effectiveAvailability('needs-sign-in', { state: 'entitled', remainingSeconds: 60 })
).toBe('offered')
expect(effectiveAvailability('offered', { state: 'signed-out' })).toBe('offered')
})
})
@@ -33,15 +33,42 @@ import { cloudSetupPageModel, sessionNeedsFrom, type CloudSetupPageModel } from
/** Billing rows re-read this often while the page waits on a browser step (focus also re-reads). */
const BILLING_POLL_MS = 4_000
/** Main's availability is a snapshot from Start; once billing answers for a signed-in account, it wins. */
export function effectiveAvailability(
requested: OrcaCloudAvailability,
access: OrcaCloudRunTargetAccess
/**
* Main's Orca Cloud availability for the project (needs-sign-in, needs-capability, offered), read
* the way the composer reads it and re-read while the page waits, so a sign-in in the browser (or
* the dev sign-in) moves the page on. Starts from the composer's answer at Start.
*/
function useLiveAvailability(
repoId: string,
initial: OrcaCloudAvailability,
enabled: boolean
): OrcaCloudAvailability {
if (requested === 'needs-sign-in' && access.state !== 'signed-out' && access.state !== 'loading') {
return 'offered'
}
return requested
const [availability, setAvailability] = useState(initial)
useEffect(() => {
if (!enabled) {
return
}
let cancelled = false
const read = (): void => {
void Promise.resolve()
.then(() => window.api.ephemeralVm.listRecipes({ repoId }))
.then((result) => {
if (!cancelled && result.orcaCloud) {
setAvailability(result.orcaCloud)
}
})
.catch(() => undefined)
}
read()
const timer = window.setInterval(read, BILLING_POLL_MS)
window.addEventListener('focus', read)
return () => {
cancelled = true
window.clearInterval(timer)
window.removeEventListener('focus', read)
}
}, [enabled, repoId])
return availability
}
export type CloudSetupFlow = CloudSetupPageModel & {
@@ -69,7 +96,8 @@ export function useCloudSetupFlow(
): CloudSetupFlow {
const fetched = useOrcaCloudRunTargetAccess(!accessOverride)
const access = accessOverride ?? fetched
const availability = effectiveAvailability(request.availability, access)
const [settled, setSettled] = useState(false)
const availability = useLiveAvailability(request.repoId, request.availability, !settled && !accessOverride)
const [sessionId, setSessionId] = useState<string | null>(null)
const [inBrowser, setInBrowser] = useState(false)
const [busy, setBusy] = useState(false)
@@ -89,6 +117,11 @@ export function useCloudSetupFlow(
})
const { steps, held: switchTeam } = useSwitchTeamSteps(rowSteps, !session)
const model = cloudSetupPageModel(steps, { agentSkipped, teamSwitch: switchTeam !== null })
useEffect(() => {
if (model.action === 'done') {
setSettled(true)
}
}, [model.action])
const error = useMemo(() => {
const parsed = parseCloudSetupError(session?.error)
return parsed && session?.teamName ? { ...parsed, teamName: session.teamName } : parsed