fix(cloud): CLOUD-PRIMARY-DELETE a cloud workspace on its default branch (owned or paired) is deletable: Delete workspace… opens the cloud delete dialog and destroys the sandbox; no primary rule, no Remove Project, no primary badge

This commit is contained in:
Neil
2026-11-12 13:18:57 -08:00
parent df079c08a9
commit b8ebb8afea
13 changed files with 482 additions and 13 deletions
+2
View File
@@ -7,6 +7,7 @@ import { BrowserWebAuthnAccountDialog } from './components/browser-webauthn-acco
import { BrowserExtensionInstallDialog } from './components/browser-extension-install-dialog'
import { PlansDialogHost } from './components/billing/plans/PlansDialogHost'
import { CloudMoveToComputerDialogMount } from './components/orca-cloud/CloudMoveToComputerDialogMount'
import { CloudPrimaryDeleteDialogMount } from './components/orca-cloud/CloudPrimaryDeleteDialog'
import { CloudSecretsAskDialogHost } from './components/cloud-secrets-ask/CloudSecretsMissingNotice'
import { OrcaCloudSignInSheet } from './components/settings/agent-accounts/sign-in-sheet/OrcaCloudSignInSheet'
import { AgentHostPanel } from './components/agent-host-panel/AgentHostPanel'
@@ -128,6 +129,7 @@ function App(): React.JSX.Element {
<BrowserExtensionInstallDialog />
<PlansDialogHost />
<CloudMoveToComputerDialogMount />
<CloudPrimaryDeleteDialogMount />
<CloudSecretsAskDialogHost />
<AgentHostPanel />
<OrcaCloudSignInSheet />
@@ -63,6 +63,18 @@ function asResult(
return { ok: false, code: 'failed', message, userMessage: message }
}
/** The control plane archives a sandbox this window cannot archive itself (paired, or no row). */
export function archiveSandboxThroughControlPlane(
sandboxId: string,
name: string,
deps: HomeArchiveDeps = defaultDeps()
): Promise<boolean> {
return deps.archiveWithUndo(`home:${sandboxId}`, {
name,
commit: async () => asResult(await deps.patchArchived(sandboxId, true))
})
}
export async function archiveFromHome(
w: CloudWorkspace,
name: string,
@@ -0,0 +1,69 @@
// CLOUD-PRIMARY-DELETE: the cloud workspace's own row deletes its sandbox through the control plane.
import { afterEach, describe, expect, it, vi } from 'vitest'
vi.mock('sonner', () => ({ toast: Object.assign(vi.fn(), { success: vi.fn(), error: vi.fn() }) }))
import { toast } from 'sonner'
import { destroyCloudPrimary } from './CloudPrimaryDeleteDialog'
import type { CloudPrimaryDeleteTarget } from '@/lib/orca-cloud/cloud-primary-delete-request'
import { resetCloudWorkspaceIdentityForTests } from '@/components/cloud-workspaces/cloud-workspace-identity'
function stub(
destroy: () => Promise<unknown>,
bindings: Record<string, { environmentId: string }>
) {
vi.stubGlobal('window', {
api: {
cloudWorkspaces: {
destroy: vi.fn(destroy),
localWorkspaces: async () => [],
environmentBindings: async () => bindings
}
}
})
return window.api.cloudWorkspaces.destroy
}
const row: CloudPrimaryDeleteTarget = {
id: 'wt-main',
hostId: 'runtime:env-cloud',
displayName: 'ik-main',
repoId: 'r',
branch: 'main'
}
afterEach(() => {
vi.unstubAllGlobals()
vi.mocked(toast.success).mockClear()
vi.mocked(toast.error).mockClear()
resetCloudWorkspaceIdentityForTests()
})
describe('destroyCloudPrimary', () => {
it('destroys the paired sandbox behind the row and says so', async () => {
const destroy = stub(async () => ({ ok: true, value: null }), {
sbx_paired: { environmentId: 'env-cloud' }
})
await destroyCloudPrimary(row)
expect(destroy).toHaveBeenCalledWith('sbx_paired')
expect(toast.success).toHaveBeenCalledWith('Deleted ik-main')
})
it('a failed destroy is an error, never "Deleted"', async () => {
stub(async () => ({ ok: false, error: 'unavailable' }), {
sbx_paired: { environmentId: 'env-cloud' }
})
await destroyCloudPrimary(row)
expect(toast.success).not.toHaveBeenCalled()
expect(toast.error).toHaveBeenCalledWith('Couldn’t delete ik-main', expect.anything())
})
it('no sandbox found: an error naming where to delete it, nothing destroyed', async () => {
const destroy = stub(async () => ({ ok: true, value: null }), {})
await destroyCloudPrimary(row)
expect(destroy).not.toHaveBeenCalled()
expect(toast.error).toHaveBeenCalledWith('Couldn’t delete ik-main', {
description: "Orca couldn't find this workspace's sandbox. Delete it from Cloud workspaces."
})
})
})
@@ -0,0 +1,117 @@
// CLOUD-PRIMARY-DELETE: Delete for a cloud row that IS the cloud workspace (the sandbox's own
// checkout, shown as "primary"), including paired ones this desktop did not create. A local removal
// cannot delete it (Git keeps a primary checkout; a paired sandbox isn't this desktop's to destroy),
// so this is the cloud delete dialog from Home and the sidebar, acting through the control plane:
// Delete destroys the sandbox, Archive instead archives it (with Undo).
import React, { useRef, useState, useSyncExternalStore } from 'react'
import { toast } from 'sonner'
import { Dialog, DialogContent, DialogFooter, DialogHeader } from '@/components/ui/dialog'
import { translate } from '@/i18n/i18n'
import {
DeleteCloudWorkspaceFooter,
DeleteCloudWorkspaceHeader
} from '@/components/sidebar/workspace-actions/deletion/DeleteCloudWorkspaceParts'
import { describeError } from '@/components/cloud-workspaces/cloud-workspace-errors'
import { archiveSandboxThroughControlPlane } from '@/components/cloud-workspaces/cloud-workspace-home-archive'
import { noteCloudArchiveChanged } from '@/lib/orca-cloud/cloud-archive-changes'
import { resolveCloudRowSandboxId } from '@/lib/orca-cloud/cloud-host-rows'
import { refreshCloudWorktreeBindings } from '@/components/cloud-workspaces/cloud-workspace-identity'
import {
cloudPrimaryDeleteRequest,
requestCloudPrimaryDelete,
subscribeCloudPrimaryDelete,
type CloudPrimaryDeleteTarget
} from '@/lib/orca-cloud/cloud-primary-delete-request'
type Target = CloudPrimaryDeleteTarget
const setRequested = requestCloudPrimaryDelete
const NO_SANDBOX = (): string =>
translate(
'orcaCloudWorkspace.primaryDelete.noSandbox',
"Orca couldn't find this workspace's sandbox. Delete it from Cloud workspaces."
)
async function withSandbox(target: Target, run: (sandboxId: string) => Promise<void>) {
const sandboxId = await resolveCloudRowSandboxId(target)
if (!sandboxId) {
toast.error(
translate('orcaCloudWorkspace.deleteFailedNamed', 'Couldn’t delete {{name}}', {
name: target.displayName ?? ''
}),
{ description: NO_SANDBOX() }
)
return
}
await run(sandboxId)
}
/** Destroys the sandbox (only "destroyed" counts) and refreshes what lists it. */
export async function destroyCloudPrimary(target: Target): Promise<void> {
const name = target.displayName ?? ''
await withSandbox(target, async (sandboxId) => {
const result = await window.api.cloudWorkspaces.destroy(sandboxId)
if (!result.ok) {
toast.error(
translate('orcaCloudWorkspace.deleteFailedNamed', 'Couldn’t delete {{name}}', { name }),
{ description: describeError(result.error, result.reason, translate) }
)
return
}
noteCloudArchiveChanged()
void refreshCloudWorktreeBindings().catch(() => undefined)
toast.success(translate('orcaCloudWorkspace.deletedNamed', 'Deleted {{name}}', { name }))
})
}
export function CloudPrimaryDeleteDialogMount(): React.JSX.Element | null {
const target = useSyncExternalStore(subscribeCloudPrimaryDelete, cloudPrimaryDeleteRequest)
const [deleting, setDeleting] = useState(false)
const cancelRef = useRef<HTMLButtonElement | null>(null)
if (!target) {
return null
}
const close = (): void => {
if (!deleting) {
setRequested(null)
}
}
return (
<Dialog open onOpenChange={(open) => !open && close()}>
<DialogContent
surface="security"
className="max-w-md"
data-cloud-primary-delete=""
onOpenAutoFocus={(event) => {
// Deleting the sandbox can't be undone: focus starts on Cancel, so Enter can't do it.
event.preventDefault()
cancelRef.current?.focus()
}}
>
<DialogHeader>
<DeleteCloudWorkspaceHeader worktree={target} dirtyCounts={new Map()} />
</DialogHeader>
<DialogFooter>
<DeleteCloudWorkspaceFooter
isDeleting={deleting}
cancelButtonRef={cancelRef}
onCancel={close}
onArchive={() => {
setRequested(null)
void withSandbox(target, async (sandboxId) => {
await archiveSandboxThroughControlPlane(sandboxId, target.displayName ?? '')
})
}}
onDelete={() => {
setDeleting(true)
void destroyCloudPrimary(target).finally(() => {
setDeleting(false)
setRequested(null)
})
}}
/>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,98 @@
// CLOUD-PRIMARY-DELETE: a cloud workspace on its repo's default branch (the sandbox's own checkout,
// here paired from another desktop) gets an enabled "Delete workspace…", with no primary-worktree
// note and no "Remove Project". A local primary checkout keeps upstream's rule, unchanged.
import { defineFixture } from '../../../../component-fixtures/component-fixture'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import {
refreshCloudWorktreeBindings,
resetCloudWorkspaceIdentityForTests
} from '@/components/cloud-workspaces/cloud-workspace-identity'
import WorktreeContextMenu from './WorktreeContextMenu'
const base: Omit<Worktree, 'id' | 'repoId' | 'hostId' | 'path' | 'displayName' | 'branch'> = {
instanceId: 'main-instance',
head: 'abc123',
isBare: false,
isMainWorktree: true,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1
}
function primaryMenu(repo: Repo, worktree: Worktree, pairedEnvironmentId?: string) {
return defineFixture({
width: 360,
store: { repos: [repo], worktreesByRepo: { [repo.id]: [worktree] } },
setup: () => {
resetCloudWorkspaceIdentityForTests()
Object.assign(window.api.cloudWorkspaces, {
localWorkspaces: () => Promise.resolve([]),
environmentBindings: () =>
Promise.resolve(
pairedEnvironmentId ? { sbx_paired: { environmentId: pairedEnvironmentId } } : {}
)
})
void refreshCloudWorktreeBindings()
},
render: () => (
<div className="h-[560px] bg-background p-4 text-foreground">
<WorktreeContextMenu worktree={worktree}>
<div className="text-sm">{worktree.displayName}</div>
</WorktreeContextMenu>
</div>
),
interact: async (frame) => {
await new Promise((resolve) => setTimeout(resolve, 150))
frame.querySelector('[data-worktree-context-menu-scope]')?.dispatchEvent(
new MouseEvent('contextmenu', {
bubbles: true,
cancelable: true,
clientX: 16,
clientY: 40
})
)
}
})
}
/** Paired cloud workspace on `main`: Delete workspace…, enabled; no "primary" rule, no Remove Project. */
export const cloudDefaultBranch = primaryMenu(
{
id: 'cloud',
path: '/workspace/orca-private',
displayName: 'orca-private (cloud)',
badgeColor: '',
addedAt: 1
},
{
...base,
id: 'cloud::/workspace/orca-private',
repoId: 'cloud',
hostId: 'runtime:env-cloud',
path: '/workspace/orca-private',
displayName: 'main',
branch: 'main'
},
'env-cloud'
)
/** A local repo's primary checkout: upstream's disabled Delete Worktree and Remove Project, unchanged. */
export const localPrimary = primaryMenu(
{ id: 'orca', path: '/preview/orca', displayName: 'orca', badgeColor: '', addedAt: 1 },
{
...base,
id: 'orca::/preview/orca',
repoId: 'orca',
hostId: 'local',
path: '/preview/orca',
displayName: 'main',
branch: 'main'
}
)
@@ -7,11 +7,13 @@ import { DropdownMenuItem, DropdownMenuShortcut } from '@/components/ui/dropdown
import { useOptionalShortcutLabel } from '@/hooks/useShortcutLabel'
import { useTranslate } from '@/i18n/use-translate'
import type { WorktreeContextMenuModel } from './use-worktree-context-menu-model'
import { useIsCloudPrimaryRow, usePrimeCloudRowBindings } from '@/lib/orca-cloud/cloud-host-rows'
export function WorktreeContextMenuDeleteItems({ model }: { model: WorktreeContextMenuModel }) {
const t = useTranslate()
const primaryDeleteDescriptionId = useId()
const deleteShortcut = useOptionalShortcutLabel('workspace.delete')
usePrimeCloudRowBindings()
const {
batchDeleteWorktrees,
contextDeletePending,
@@ -20,7 +22,7 @@ export function WorktreeContextMenuDeleteItems({ model }: { model: WorktreeConte
handleDelete,
isMultiContext,
lineageDescendantCount,
removesProject,
removesProject: modelRemovesProject,
worktree
} = model
const isActiveDeleteTarget = useAppStore((state) => {
@@ -32,21 +34,30 @@ export function WorktreeContextMenuDeleteItems({ model }: { model: WorktreeConte
worktreeCopyOwnerKey(target.worktree) === worktreeCopyOwnerKey(worktree) &&
target.worktree.instanceId === worktree.instanceId
})
const projectMissing = !isMultiContext && worktree.isMainWorktree && !removesProject
// An Orca Cloud workspace's own checkout is deletable: it destroys the sandbox (never "Remove Project").
const cloudPrimary = useIsCloudPrimaryRow(worktree) && !isMultiContext
const removesProject = !cloudPrimary && modelRemovesProject
const projectMissing =
!isMultiContext && !cloudPrimary && worktree.isMainWorktree && !removesProject
const label = contextDeletePending
? t('auto.components.sidebar.WorktreeContextMenu.b42391d8bf', 'Deleting…')
: isMultiContext
? deleteLabel
: folderWorkspaceId
? t('auto.components.sidebar.WorktreeContextMenu.250de158fd', 'Remove Workspace')
: removesProject
? t('auto.components.sidebar.WorktreeContextMenu.f5ac91531d', 'Remove Project from Orca')
: lineageDescendantCount > 0
: cloudPrimary
? t('auto.components.sidebar.WorktreeContextMenu.deleteCloudWorkspace', 'Delete workspace…')
: folderWorkspaceId
? t('auto.components.sidebar.WorktreeContextMenu.250de158fd', 'Remove Workspace')
: removesProject
? t(
'auto.components.sidebar.WorktreeContextMenu.deleteWithDescendants',
'Delete with Descendants…'
'auto.components.sidebar.WorktreeContextMenu.f5ac91531d',
'Remove Project from Orca'
)
: t('auto.components.sidebar.WorktreeContextMenu.f4475537d8', 'Delete')
: lineageDescendantCount > 0
? t(
'auto.components.sidebar.WorktreeContextMenu.deleteWithDescendants',
'Delete with Descendants…'
)
: t('auto.components.sidebar.WorktreeContextMenu.f4475537d8', 'Delete')
return (
<>
{/* Git cannot remove primary checkouts; keep disabled Delete beside the enabled Remove Project action. */}
@@ -346,3 +346,64 @@ describe('Delete from Home with a runtime record but no sidebar row (REG-8b)', (
vi.unstubAllGlobals()
})
})
vi.mock('@/lib/orca-cloud/cloud-primary-delete-request', () => ({
requestCloudPrimaryDelete: vi.fn()
}))
describe('Delete on a cloud workspace on its default branch (CLOUD-PRIMARY-DELETE)', () => {
beforeEach(async () => {
mocks.state.settings = { skipDeleteWorktreeConfirm: false }
mocks.state.openModal.mockClear()
mocks.state.repos = []
const { requestCloudPrimaryDelete } =
await import('@/lib/orca-cloud/cloud-primary-delete-request')
vi.mocked(requestCloudPrimaryDelete).mockClear()
})
async function pairWith(environmentId: string): Promise<void> {
vi.stubGlobal('window', {
api: {
cloudWorkspaces: {
localWorkspaces: async () => [],
environmentBindings: async () => ({ sbx_paired: { environmentId } })
}
}
})
const { refreshCloudWorktreeBindings } =
await import('@/components/cloud-workspaces/cloud-workspace-identity')
await refreshCloudWorktreeBindings()
}
it('a paired cloud workspace is deletable: the cloud delete dialog, never "Remove Project"', async () => {
await pairWith('env-cloud')
const { runWorktreeDelete } = await import('./delete-worktree-flow')
const { requestCloudPrimaryDelete } =
await import('@/lib/orca-cloud/cloud-primary-delete-request')
setWorktrees([{ id: 'wt-main', isMainWorktree: true, hostId: 'runtime:env-cloud' }])
runWorktreeDelete('wt-main', { expectedHostId: 'runtime:env-cloud' })
expect(requestCloudPrimaryDelete).toHaveBeenCalledWith(
expect.objectContaining({ id: 'wt-main', hostId: 'runtime:env-cloud' })
)
expect(mocks.state.openModal).not.toHaveBeenCalledWith(
'confirm-remove-folder',
expect.anything()
)
vi.unstubAllGlobals()
})
it('a local primary checkout still cannot be deleted: it offers removing the project', async () => {
await pairWith('env-cloud')
const { runWorktreeDelete } = await import('./delete-worktree-flow')
const { requestCloudPrimaryDelete } =
await import('@/lib/orca-cloud/cloud-primary-delete-request')
setWorktrees([{ id: 'wt-local-main', isMainWorktree: true }])
runWorktreeDelete('wt-local-main')
expect(requestCloudPrimaryDelete).not.toHaveBeenCalled()
expect(mocks.state.openModal).toHaveBeenCalledWith(
'confirm-remove-folder',
expect.objectContaining({ repoId: 'repo-1' })
)
vi.unstubAllGlobals()
})
})
@@ -24,6 +24,8 @@ import {
import { runWorktreeDeletesInParallel } from './worktree-delete-execution'
import { runWorktreeDeleteWithToast } from './run-worktree-delete-with-toast'
import { collectRemovalUnsavedFiles } from '@/components/editor/save/flush/removal-unsaved-editor-files'
import { isCloudPrimaryRowNow } from '@/lib/orca-cloud/cloud-host-rows'
import { requestCloudPrimaryDelete } from '@/lib/orca-cloud/cloud-primary-delete-request'
/** Standard Git-worktree deletion funnel; stale record/instance guards precede confirmation preferences. */
export function runWorktreeDelete(worktreeId: string, options: WorktreeDeleteOptions = {}): void {
@@ -40,6 +42,11 @@ export function runWorktreeDelete(worktreeId: string, options: WorktreeDeleteOpt
}
return
}
// An Orca Cloud workspace's own checkout is the workspace, not a repo's primary: delete the sandbox.
if (isCloudPrimaryRowNow(target)) {
requestCloudPrimaryDelete(target)
return
}
if (target.isMainWorktree) {
const repo = findRepoForHost(state.repos, target.repoId, {
hostId: target.hostId,
@@ -14,6 +14,7 @@ import {
import { WorktreeCardSshHostControl } from '../host/WorktreeCardSshHostControl'
import { WorktreeCardHostMark } from '../host/WorktreeCardHostMark'
import { CloudAgentHeldMark } from '@/components/orca-cloud/CloudAgentHeldMark'
import { useIsCloudHostRow } from '@/lib/orca-cloud/cloud-host-rows'
import { WorktreeTitleInlineRename } from '../identity/WorktreeTitleInlineRename'
import type { WorktreeCardController } from '../controller/use-worktree-card-controller'
@@ -51,6 +52,8 @@ export function WorktreeCardHeader({
isFolder,
handleWorkspaceQuickAction
} = card
// "primary" is a local checkout's role; on a cloud row it only means the default branch (design-squad).
const cloudRow = useIsCloudHostRow(worktree)
const {
showPinnedRepoIcon,
showInlineRepoBadge,
@@ -124,7 +127,7 @@ export function WorktreeCardHeader({
onOpen={handleOpenRenameErrorDialog}
/>
) : null}
{!compactCards && worktree.isMainWorktree && !isFolder && (
{!compactCards && worktree.isMainWorktree && !isFolder && !cloudRow && (
<WorktreeCardPrimaryBadge card={card} hasHoverDetails={hasHoverDetails} />
)}
@@ -141,7 +144,7 @@ export function WorktreeCardHeader({
{showHeaderActions && (
<div className="ml-auto flex shrink-0 items-center justify-center gap-1 pr-1.5">
{showTitleRowPrimary && <WorktreeCardPrimaryStar />}
{showTitleRowPrimary && !cloudRow && <WorktreeCardPrimaryStar />}
{showDeleteQuickAction && (
<WorktreeCardDeleteQuickAction
+5 -1
View File
@@ -7540,7 +7540,8 @@
"copyNameOption": "Worktree Name",
"lineageSubmenu": "Parent & Children",
"projectGroupSubmenu": "Project Group",
"moveToThisComputer": "Move to this computer"
"moveToThisComputer": "Move to this computer",
"deleteCloudWorkspace": "Delete workspace…"
},
"WorktreeList": {
"7a8b9c0d1e": "Update required",
@@ -27858,6 +27859,9 @@
"definition": "Archive keeps a snapshot of this workspace (its files, chats and terminal history) for 30 days, then deletes it. Unarchive it from its menu before then. Like a sleeping workspace, it doesn’t use sandbox hours.",
"archivedUntil": "A read-only snapshot of its files, chats and terminal history. Unarchive before {{date}} to open terminals and agents again; after that it’s deleted.",
"archived": "A read-only snapshot of its files, chats and terminal history. Unarchive within 30 days of archiving to open terminals and agents again; after that it’s deleted."
},
"primaryDelete": {
"noSandbox": "Orca couldn't find this workspace's sandbox. Delete it from Cloud workspaces."
}
},
"cloudAgentSetup": {
@@ -0,0 +1,55 @@
// 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.
//
// 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".
import { useEffect } from 'react'
import {
refreshCloudWorktreeBindings,
sandboxIdForWorktree,
useCloudWorkspaceIdentities
} from '@/components/cloud-workspaces/cloud-workspace-identity'
import { getCloudWorkspaceSnapshot, useIsCloudWorkspace } from './orca-cloud-workspace-store'
type Row = { id: string; hostId?: string | null; isMainWorktree?: boolean }
let primed = false
/** Loads the bindings once per app session (a menu open refreshes them for new pairings). */
export function usePrimeCloudRowBindings(): void {
useEffect(() => {
if (!primed) {
primed = true
void refreshCloudWorktreeBindings().catch(() => undefined)
}
}, [])
}
/** 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
}
/** The cloud workspace itself (the sandbox's own checkout): its Delete destroys the sandbox. */
export function useIsCloudPrimaryRow(row: Row): boolean {
return useIsCloudHostRow(row) && row.isMainWorktree === true
}
/** 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)
)
}
/** The sandbox behind a cloud row, re-reading the bindings first (a pairing may be new). */
export async function resolveCloudRowSandboxId(row: Row): Promise<string | null> {
await refreshCloudWorktreeBindings().catch(() => undefined)
return sandboxIdForWorktree(row)
}
@@ -0,0 +1,27 @@
// Which cloud row the cloud delete dialog is open for (CLOUD-PRIMARY-DELETE). Its own module so the
// delete funnel can request the dialog without importing the dialog (and the Home flows it uses).
import type { Worktree } from '../../../../shared/worktree/types'
export type CloudPrimaryDeleteTarget = Pick<
Worktree,
'id' | 'hostId' | 'displayName' | 'repoId' | 'branch'
> &
Partial<Pick<Worktree, 'runtimeOwnerEnvironmentId'>>
let requested: CloudPrimaryDeleteTarget | null = null
const listeners = new Set<() => void>()
export function subscribeCloudPrimaryDelete(listener: () => void): () => void {
listeners.add(listener)
return () => listeners.delete(listener)
}
export function cloudPrimaryDeleteRequest(): CloudPrimaryDeleteTarget | null {
return requested
}
/** Opens (target) or closes (null) the cloud delete dialog. */
export function requestCloudPrimaryDelete(target: CloudPrimaryDeleteTarget | null): void {
requested = target
listeners.forEach((listener) => listener())
}
@@ -950,6 +950,9 @@
"localCommands.settings.workspacesTitle",
"localCommands.text.tab",
"localCommands.text.trailingSpace",
"orcaCloudWorkspace.deleteFailedNamed",
"orcaCloudWorkspace.deletedNamed",
"orcaCloudWorkspace.primaryDelete.noSandbox",
"plugins.frameOpenExternal.alwaysAllow",
"plugins.frameOpenExternal.cancel",
"plugins.frameOpenExternal.message",