fix(cloud): PAIRED-SLEEP sleep a paired cloud workspace's sandbox through the control plane; paired-from-birth rows are cloud rows from the catalog mark (#565)

Sleep on a paired cloud workspace's own row (no local VM record) now stops its sandbox via
cloudWorkspaces.sleep(sandboxId), so it stops billing; failures reuse the cloud sleep toast.
useIsCloudHostRow / isCloudPrimaryRowNow also read the runtime catalog's orcaCloudPaired mark,
so a GitHub-only create loses its "primary" badge and gets "Delete workspace…" and Sleep without
waiting for the pairing bindings to load.
This commit is contained in:
Neil
2026-11-12 14:26:49 -08:00
committed by GitHub
parent 0bab6a9c4c
commit 0fd98c5c2a
7 changed files with 238 additions and 17 deletions
@@ -1,4 +1,5 @@
import { selectAwakeBrowserTabsByWorktree } from '@/components/sidebar/workspace-actions/sleep/browser-tab-sleep'
import { useIsPairedCloudPrimaryRow } from '@/lib/orca-cloud/cloud-host-rows'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useAppStore } from '@/store/index'
import { useRepoById } from '@/store/repos/use-repository-catalog'
@@ -153,12 +154,16 @@ export function useWorktreeContextMenuModel({
unifiedTabsByWorktree
]
)
// A paired cloud workspace can be running with nothing open here: its Sleep reaches the sandbox.
const pairedCloudSleepable = useIsPairedCloudPrimaryRow(worktree) && !isMultiContext
const sleepableWorktrees = useMemo(
() =>
activeContextWorktrees.filter((item) =>
hasSleepableWorkspaceActivity(item.id, sleepActivity)
activeContextWorktrees.filter(
(item) =>
hasSleepableWorkspaceActivity(item.id, sleepActivity) ||
(pairedCloudSleepable && item.id === worktree.id)
),
[activeContextWorktrees, sleepActivity]
[activeContextWorktrees, sleepActivity, pairedCloudSleepable, worktree.id]
)
const lineageMenuActions = useWorkspaceLineageMenuActions({
enabled: menuOpen && !isMultiContext,
@@ -1,5 +1,6 @@
import { toast } from 'sonner'
import { toastCloudSleepFailure } from '@/lib/orca-cloud/cloud-sleep-failure-toast'
import { sleepPairedCloudSandboxes } from '@/lib/orca-cloud/paired-cloud-sleep'
import { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
@@ -95,16 +96,15 @@ export async function runSleepWorktrees(
for (const id of unsleptWorktreeIds) {
releaseWorktreeSleepView(id)
}
// A paired cloud workspace's sandbox sleeps through the control plane (PAIRED-SLEEP).
const unslept = new Set(unsleptWorktreeIds)
const pairedStillRunning = await sleepPairedCloudSandboxes(
worktreeIds.filter((id) => !unslept.has(id))
)
const stillRunningIds = [...vmStillRunningIds, ...pairedStillRunning]
// An Orca Cloud sandbox left running gets the cloud copy (sandbox hours, Try again) instead.
const cloudToasted = toastCloudSleepFailure(
vmStillRunningIds,
(ids) => void runSleepWorktrees(ids)
)
toastSleepFailures(
new Set(worktreeIds).size,
errors,
vmStillRunningIds.length > 0 && !cloudToasted
)
const cloudToasted = toastCloudSleepFailure(stillRunningIds, (ids) => void runSleepWorktrees(ids))
toastSleepFailures(new Set(worktreeIds).size, errors, stillRunningIds.length > 0 && !cloudToasted)
// A slept Orca Cloud sandbox won't restart, so its restart warning no longer applies.
for (const result of results) {
if (result.completed) {
@@ -0,0 +1,64 @@
// @vitest-environment happy-dom
// A cloud workspace created from a GitHub-only repo is paired from birth: its row sits on a
// "<repo> (cloud)" server on the sandbox's own checkout ("master · primary"). The runtime catalog's
// paired mark alone must make it a cloud row (no "primary" badge, "Delete workspace…", Sleep reaches
// the sandbox), before anything has loaded the pairing bindings.
import { renderHook } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
type CatalogEntry = {
id: string
name: string
source?: 'manual' | 'ephemeral-vm'
orcaCloudPaired?: true
}
const state = vi.hoisted(() => {
const value: { runtimeEnvironments: CatalogEntry[] } = { runtimeEnvironments: [] }
return value
})
vi.mock('@/store', () => ({
useAppStore: Object.assign((selector: (value: typeof state) => unknown) => selector(state), {
getState: () => state
})
}))
import {
isCloudPrimaryRowNow,
useIsCloudHostRow,
useIsCloudPrimaryRow,
useIsPairedCloudPrimaryRow
} from './cloud-host-rows'
const fromGitHub = {
id: 'push-test::/workspace/orca-sandbox-push-test',
hostId: 'runtime:env-gh',
isMainWorktree: true
}
afterEach(() => {
state.runtimeEnvironments = []
})
describe('a paired-from-birth cloud row (GitHub-only create)', () => {
it('is a cloud primary row from the catalog mark alone: no primary badge, Delete workspace…', () => {
state.runtimeEnvironments = [
{ id: 'env-gh', name: 'orca-sandbox-push-test (cloud)', orcaCloudPaired: true }
]
expect(renderHook(() => useIsCloudHostRow(fromGitHub)).result.current).toBe(true)
expect(renderHook(() => useIsCloudPrimaryRow(fromGitHub)).result.current).toBe(true)
expect(renderHook(() => useIsPairedCloudPrimaryRow(fromGitHub)).result.current).toBe(true)
expect(isCloudPrimaryRowNow(fromGitHub)).toBe(true)
})
it('the same row on an ordinary paired server is not a cloud row (upstream rules apply)', () => {
state.runtimeEnvironments = [{ id: 'env-gh', name: 'build box (cloud)', source: 'manual' }]
expect(renderHook(() => useIsCloudHostRow(fromGitHub)).result.current).toBe(false)
expect(isCloudPrimaryRowNow(fromGitHub)).toBe(false)
})
it('a local primary checkout is never a cloud row', () => {
const local = { id: 'repo::/path', hostId: 'local', isMainWorktree: true }
expect(renderHook(() => useIsCloudHostRow(local)).result.current).toBe(false)
expect(isCloudPrimaryRowNow(local)).toBe(false)
})
})
@@ -1,8 +1,11 @@
// Which sidebar rows are Orca Cloud workspaces, including PAIRED ones (CLOUD-PRIMARY-DELETE).
// `useIsCloudWorkspace` knows only the workspaces this desktop created. A workspace opened from Home
// on another desktop (or after a re-sign-in) is paired: a "<name> (cloud)" server whose rows live on
// `runtime:<environmentId>`. The cloud identity store already maps both kinds to their sandbox
// (this desktop's runtime records, and the pairing bindings), so this reads it.
// `runtime:<environmentId>`. Two sources, either is enough:
// - the runtime catalog: main marks a paired server `orcaCloudPaired` (PAIRED-AWARE), so
// `isOrcaCloudRuntimeEnvironment` knows it synchronously, with no priming (a row created from a
// GitHub-only repo is paired from birth and must lose its "primary" badge at once);
// - the cloud identity store, which also maps the row to its sandbox id (for Delete and Sleep).
//
// A cloud row on the sandbox's own checkout (isMainWorktree) IS the cloud workspace, not a local
// repo's primary checkout: Delete must destroy the sandbox, never "Remove the project".
@@ -13,6 +16,10 @@ import {
useCloudWorkspaceIdentities
} from '@/components/cloud-workspaces/cloud-workspace-identity'
import { getCloudWorkspaceSnapshot, useIsCloudWorkspace } from './orca-cloud-workspace-store'
import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import { parseExecutionHostId } from '../../../../shared/execution-host'
import { isOrcaCloudRuntimeEnvironment } from '../../../../shared/remote-runtime/host-catalog/orca-cloud-environment-name'
type Row = { id: string; hostId?: string | null; isMainWorktree?: boolean }
@@ -28,11 +35,25 @@ export function usePrimeCloudRowBindings(): void {
}, [])
}
/** Is the row's host an Orca Cloud server in the runtime catalog (owned, or marked paired)? */
function isCloudHostInCatalog(
state: Pick<AppState, 'runtimeEnvironments'>,
hostId: string | null | undefined
): boolean {
const parsed = parseExecutionHostId(hostId ?? undefined)
if (parsed?.kind !== 'runtime') {
return false
}
const environment = state.runtimeEnvironments?.find((entry) => entry.id === parsed.environmentId)
return environment ? isOrcaCloudRuntimeEnvironment(environment) : false
}
/** A row on an Orca Cloud workspace: this desktop's own, or one it paired with. */
export function useIsCloudHostRow(row: Row): boolean {
const owned = useIsCloudWorkspace(row.id)
const identities = useCloudWorkspaceIdentities()
return owned || sandboxIdForWorktree(row, identities) !== null
const inCatalog = useAppStore((state) => isCloudHostInCatalog(state, row.hostId))
return owned || inCatalog || sandboxIdForWorktree(row, identities) !== null
}
/** The cloud workspace itself (the sandbox's own checkout): its Delete destroys the sandbox. */
@@ -40,11 +61,19 @@ export function useIsCloudPrimaryRow(row: Row): boolean {
return useIsCloudHostRow(row) && row.isMainWorktree === true
}
/** A cloud workspace's own row that this desktop did NOT create (paired): no local VM record. */
export function useIsPairedCloudPrimaryRow(row: Row): boolean {
const owned = useIsCloudWorkspace(row.id)
return useIsCloudPrimaryRow(row) && !owned
}
/** Non-React: the same, for the delete funnel. */
export function isCloudPrimaryRowNow(row: Row): boolean {
return (
row.isMainWorktree === true &&
(getCloudWorkspaceSnapshot().ids.has(row.id) || sandboxIdForWorktree(row) !== null)
(getCloudWorkspaceSnapshot().ids.has(row.id) ||
isCloudHostInCatalog(useAppStore.getState(), row.hostId) ||
sandboxIdForWorktree(row) !== null)
)
}
@@ -5,9 +5,11 @@ import { translate } from '@/i18n/i18n'
import { useAppStore } from '@/store'
import { findWorktreeById } from '@/store/github/refresh/worktree-refresh'
import { getCloudWorkspaceSnapshot } from './orca-cloud-workspace-store'
import { isPairedCloudSleepTarget } from './paired-cloud-sleep'
export function isCloudWorkspaceId(worktreeId: string): boolean {
return getCloudWorkspaceSnapshot().ids.has(worktreeId)
// Paired workspaces' sandboxes too: their failed Sleep leaves a sandbox running just the same.
return getCloudWorkspaceSnapshot().ids.has(worktreeId) || isPairedCloudSleepTarget(worktreeId)
}
function workspaceName(worktreeId: string): string {
@@ -0,0 +1,73 @@
// PAIRED-SLEEP: Sleep on a paired cloud workspace puts its sandbox to sleep through the control plane;
// this desktop's own sandboxes keep the ordinary Sleep (their VM suspends), local rows are untouched.
import { afterEach, describe, expect, it, vi } from 'vitest'
const rows = vi.hoisted(
() => new Map<string, { id: string; hostId?: string; isMainWorktree?: boolean }>()
)
const owned = vi.hoisted(() => new Set<string>())
vi.mock('@/store', () => ({ useAppStore: { getState: () => ({}) } }))
vi.mock('@/store/workspaces/selection/worktree-on-host-selector', () => ({
getWorktreeOnHostFromState: (_state: unknown, id: string) => rows.get(id)
}))
vi.mock('./orca-cloud-workspace-store', () => ({
getCloudWorkspaceSnapshot: () => ({ ids: owned }),
useIsCloudWorkspace: () => false
}))
import { isPairedCloudSleepTarget, sleepPairedCloudSandboxes } from './paired-cloud-sleep'
import { resetCloudWorkspaceIdentityForTests } from '@/components/cloud-workspaces/cloud-workspace-identity'
function stub(sleep: (id: string) => Promise<unknown>) {
const fn = vi.fn(sleep)
vi.stubGlobal('window', {
api: {
cloudWorkspaces: {
sleep: fn,
localWorkspaces: async () => [],
environmentBindings: async () => ({ sbx_paired: { environmentId: 'env-paired' } })
}
}
})
return fn
}
afterEach(() => {
vi.unstubAllGlobals()
rows.clear()
owned.clear()
resetCloudWorkspaceIdentityForTests()
})
describe('PAIRED-SLEEP', () => {
it('a paired cloud workspace: its sandbox sleeps through the control plane', async () => {
rows.set('wt-paired', { id: 'wt-paired', hostId: 'runtime:env-paired', isMainWorktree: true })
const sleep = stub(async () => ({ ok: true, value: undefined }))
await expect(sleepPairedCloudSandboxes(['wt-paired'])).resolves.toEqual([])
expect(sleep).toHaveBeenCalledWith('sbx_paired')
expect(isPairedCloudSleepTarget('wt-paired')).toBe(true)
})
it('a refused sleep is reported: the sandbox is still running (cloud toast, Try again)', async () => {
rows.set('wt-paired', { id: 'wt-paired', hostId: 'runtime:env-paired', isMainWorktree: true })
stub(async () => ({ ok: false, error: 'conflict' }))
await expect(sleepPairedCloudSandboxes(['wt-paired'])).resolves.toEqual(['wt-paired'])
})
it('this desktop’s own sandbox and a local workspace are left to the ordinary Sleep', async () => {
rows.set('wt-owned', { id: 'wt-owned', hostId: 'runtime:env-paired', isMainWorktree: true })
owned.add('wt-owned')
rows.set('wt-local', { id: 'wt-local', hostId: 'local', isMainWorktree: true })
const sleep = stub(async () => ({ ok: true, value: undefined }))
await expect(sleepPairedCloudSandboxes(['wt-owned', 'wt-local'])).resolves.toEqual([])
expect(sleep).not.toHaveBeenCalled()
})
it('a non-primary worktree inside a paired sandbox does not sleep the whole sandbox', async () => {
rows.set('wt-other', { id: 'wt-other', hostId: 'runtime:env-paired', isMainWorktree: false })
const sleep = stub(async () => ({ ok: true, value: undefined }))
await sleepPairedCloudSandboxes(['wt-other'])
expect(sleep).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,48 @@
// PAIRED-SLEEP (PAIRED-AWARE): Sleep on a paired cloud workspace (opened here, created elsewhere: a
// GitHub-only repo, another device, a teammate's, a link) must put its SANDBOX to sleep. This desktop
// has no runtime record of it, so the ordinary Sleep only closed panels here while the sandbox kept
// running and using sandbox hours. The control plane sleeps it, like Home's Sleep does.
import { useAppStore } from '@/store'
import { getWorktreeOnHostFromState } from '@/store/workspaces/selection/worktree-on-host-selector'
import { getCloudWorkspaceSnapshot } from './orca-cloud-workspace-store'
import { isCloudPrimaryRowNow, resolveCloudRowSandboxId } from './cloud-host-rows'
import { refreshCloudWorktreeBindings } from '@/components/cloud-workspaces/cloud-workspace-identity'
type Row = { id: string; hostId?: string | null; isMainWorktree?: boolean }
function rowFor(worktreeId: string): Row | null {
return getWorktreeOnHostFromState(useAppStore.getState(), worktreeId, undefined) ?? null
}
/** A paired cloud workspace's own row: its Sleep must reach the sandbox through the control plane. */
export function isPairedCloudSleepTarget(worktreeId: string): boolean {
if (getCloudWorkspaceSnapshot().ids.has(worktreeId)) {
// This desktop's own sandbox: the ordinary Sleep suspends its VM.
return false
}
const row = rowFor(worktreeId)
return row !== null && isCloudPrimaryRowNow(row)
}
/** Sleeps each paired target's sandbox; returns the ids whose sandbox is still running. */
export async function sleepPairedCloudSandboxes(worktreeIds: readonly string[]): Promise<string[]> {
const stillRunning: string[] = []
const api = typeof window !== 'undefined' ? window.api?.cloudWorkspaces : undefined
if (!api) {
return stillRunning
}
// Sleep can run before any menu loaded the pairings (card action, shortcut): read them first.
await refreshCloudWorktreeBindings().catch(() => undefined)
for (const id of worktreeIds) {
if (!isPairedCloudSleepTarget(id)) {
continue
}
const row = rowFor(id)
const sandboxId = row ? await resolveCloudRowSandboxId(row) : null
const result = sandboxId ? await api.sleep(sandboxId).catch(() => null) : null
if (!result?.ok) {
stillRunning.push(id)
}
}
return stillRunning
}