fix(sidebar): Option shows the trash on cloud workspace rows, owned and paired (OPTION-TRASH-CLOUD)

Holding Option showed the delete quick action on local worktrees but never on a cloud
workspace's row. The gate (canShowWorkspaceDeleteQuickAction) refused every main
worktree, and a cloud workspace's own row is its sandbox's main checkout. The gate now
takes cloudPrimary (useIsCloudPrimaryRow: owned, or paired via the runtime catalog
or bindings). The live card's DeleteQuickAction (place.deleteQuickAction) reads it, and
the click goes through runWorktreeDelete, whose cloud-primary branch opens the cloud
delete confirmation (requestCloudPrimaryDelete), never Remove Project. A local repo's
primary checkout still gets no trash.
This commit is contained in:
Neil
2026-11-12 16:54:54 -08:00
parent 9c18966cba
commit f622900065
4 changed files with 211 additions and 2 deletions
@@ -94,6 +94,15 @@ export function canShowWorkspaceDeleteQuickAction(args: {
deleteModifierPressed: boolean
isDeleting: boolean
isMainWorktree: boolean
/**
* An Orca Cloud workspace's own checkout, owned or paired (OPTION-TRASH-CLOUD). It is the cloud
* workspace, not a repo's primary: the delete funnel destroys the sandbox after the cloud confirm.
*/
cloudPrimary?: boolean
}): boolean {
return args.deleteModifierPressed && !args.isDeleting && !args.isMainWorktree
return (
args.deleteModifierPressed &&
!args.isDeleting &&
(!args.isMainWorktree || args.cloudPrimary === true)
)
}
@@ -1,4 +1,5 @@
import { useWorkspaceReferenceDetails } from '../review/use-workspace-reference-details'
import { useIsCloudPrimaryRow } from '@/lib/orca-cloud/cloud-host-rows'
import { getWorkspaceAttachments } from '../../../../../../shared/workspace-attachments/workspace-attachments'
import { summarizeWorkspaceReviewChecks } from '../../../../../../shared/workspace-review-checks'
import { canShowWorkspaceDeleteQuickAction } from '../../workspace-actions/deletion/workspace-delete-quick-action'
@@ -100,13 +101,16 @@ export function useWorktreeCardController(props: ResolvedWorktreeCardProps) {
openModal: foundation.openModal
})
// A cloud workspace's own row (owned or paired) is deletable: the sandbox goes, after the cloud confirm.
const cloudPrimary = useIsCloudPrimaryRow(worktree)
// Why: delete is destructive, so it only appears while holding Option/Alt, not in the ordinary hover chrome.
const showDeleteQuickAction =
!props.affiliateListMode &&
canShowWorkspaceDeleteQuickAction({
deleteModifierPressed: linked.deleteModifierPressed,
isDeleting: linked.isDeleting,
isMainWorktree: worktree.isMainWorktree
isMainWorktree: worktree.isMainWorktree,
cloudPrimary
})
const workspaceActions = useWorktreeCardWorkspaceActions({
worktree,
@@ -0,0 +1,73 @@
import { useEffect } from 'react'
import { getDefaultSettings } from '../../../../../../shared/constants'
import { defineFixture } from '../../../../component-fixtures/component-fixture'
import { seedCloudWorkspaceSnapshot } from '@/lib/orca-cloud/orca-cloud-workspace-store'
import { makeRepo, makeWorktree } from '../../../worktree-jump/worktree-jump-palette-test-fixtures'
import WorktreeCard from './WorktreeCard'
// OPTION-TRASH-CLOUD: the sidebar while Option is held, each card as if hovered. A cloud workspace's
// own row (created here, or paired from Home) shows the trash like a local worktree; a local repo's
// primary checkout still doesn't (Git can't delete it).
const repo = { ...makeRepo(), displayName: 'orca' }
const worktrees = [
makeWorktree('local-main', 'orca', { isMainWorktree: true }),
makeWorktree('local-wt', 'fix-login', { branch: 'refs/heads/fix-login' }),
makeWorktree('cloud-owned', 'Add a push test', {
isMainWorktree: true,
hostId: 'runtime:env-owned',
branch: 'refs/heads/add-push-test'
}),
makeWorktree('cloud-paired', 'Ada’s cloud workspace', {
isMainWorktree: true,
hostId: 'runtime:env-paired',
branch: 'refs/heads/ada/onboarding'
})
]
// The created workspace's sandbox server, and a workspace paired from Home.
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the cloud row check reads only id, name, source and orcaCloudPaired.
const CLOUD_SERVERS = [
{ id: 'env-owned', name: 'orca sandbox a1b2c3d4', source: 'ephemeral-vm' },
{ id: 'env-paired', name: 'orca (cloud)', orcaCloudPaired: true }
] as never
function HoldOption({ children }: { children: React.ReactNode }): React.JSX.Element {
useEffect(() => {
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Alt', altKey: true }))
}, [])
return <>{children}</>
}
export const optionHeld = defineFixture({
width: 320,
store: {
settings: getDefaultSettings('/home/fixture'),
repos: [repo],
worktreesByRepo: { [repo.id]: worktrees },
worktreeCardProperties: ['status', 'branch'],
runtimeEnvironments: CLOUD_SERVERS
},
setup: () => seedCloudWorkspaceSnapshot({ ids: new Set(['cloud-owned']), idsLoaded: true }),
render: () => (
<HoldOption>
{/* Every card as if hovered: the trash only shows on the card under the pointer. */}
<style>
{'[data-option-delete-fixture] button[aria-label="Delete workspace"]{opacity:1}'}
</style>
<div
data-option-delete-fixture=""
className="flex flex-col gap-1 bg-worktree-sidebar p-2 text-worktree-sidebar-foreground"
>
{worktrees.map((worktree) => (
<WorktreeCard
key={worktree.id}
worktree={worktree}
repo={repo}
isActive={false}
nativeDragEnabled={false}
/>
))}
</div>
</HoldOption>
)
})
@@ -0,0 +1,123 @@
// @vitest-environment happy-dom
// OPTION-TRASH-CLOUD: holding Option shows the trash on a cloud workspace's own row (owned or
// paired), the live card's DeleteQuickAction, and it opens the cloud delete (sandbox), never the
// local "Remove Project". A local repo's primary checkout still gets no trash.
import { act, cleanup, fireEvent, render } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import { TooltipProvider } from '@/components/ui/tooltip'
import { seedCloudWorkspaceSnapshot } from '@/lib/orca-cloud/orca-cloud-workspace-store'
import { makeRepo, makeWorktree } from '../../../worktree-jump/worktree-jump-palette-test-fixtures'
import { canShowWorkspaceDeleteQuickAction } from '../../workspace-actions/deletion/workspace-delete-quick-action'
import WorktreeCard from './WorktreeCard'
const mocks = vi.hoisted(() => ({ cloudDelete: vi.fn() }))
vi.mock('@/lib/orca-cloud/cloud-primary-delete-request', () => ({
requestCloudPrimaryDelete: mocks.cloudDelete
}))
vi.mock('@/components/sidebar/workspace-actions/sleep/use-worktree-sleep-state', () => ({
useIsSleepingWorktree: () => false,
selectIsSleepingWorktree: () => false
}))
vi.mock('@/components/sidebar/workspace-card/activity/use-worktree-activity-status', () => ({
useWorktreeActivityStatus: () => 'inactive'
}))
const repo = makeRepo()
const ownedCloud = makeWorktree('cloud-owned', 'Owned cloud', { isMainWorktree: true })
const pairedCloud = makeWorktree('cloud-paired', 'Paired cloud', {
isMainWorktree: true,
hostId: 'runtime:env-paired'
})
const localPrimary = makeWorktree('local-main', 'Local primary', { isMainWorktree: true })
const localWorktree = makeWorktree('local-wt', 'Local worktree')
function seed(): void {
act(() => {
useAppStore.setState({
repos: [repo],
worktreesByRepo: { [repo.id]: [ownedCloud, pairedCloud, localPrimary, localWorktree] },
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the cloud row check reads only id and orcaCloudPaired.
runtimeEnvironments: [
{ id: 'env-paired', name: 'web (cloud)', orcaCloudPaired: true }
] as never
})
seedCloudWorkspaceSnapshot({ ids: new Set(['cloud-owned']), idsLoaded: true })
})
}
function card(worktree: ReturnType<typeof makeWorktree>) {
return render(
<TooltipProvider>
<WorktreeCard worktree={worktree} repo={repo} isActive={false} nativeDragEnabled={false} />
</TooltipProvider>
)
}
const trash = (container: HTMLElement) =>
container.querySelector<HTMLButtonElement>('button[aria-label="Delete workspace"]')
afterEach(() => {
act(() => {
window.dispatchEvent(new KeyboardEvent('keyup', { key: 'Alt' }))
})
cleanup()
seedCloudWorkspaceSnapshot({})
mocks.cloudDelete.mockReset()
})
describe('Option shows the trash on cloud rows (OPTION-TRASH-CLOUD)', () => {
it('the gate: a cloud primary is deletable, a local primary is not', () => {
const held = { deleteModifierPressed: true, isDeleting: false }
expect(canShowWorkspaceDeleteQuickAction({ ...held, isMainWorktree: true })).toBe(false)
expect(
canShowWorkspaceDeleteQuickAction({ ...held, isMainWorktree: true, cloudPrimary: true })
).toBe(true)
expect(canShowWorkspaceDeleteQuickAction({ ...held, isMainWorktree: false })).toBe(true)
// Never without Option, never while deleting.
expect(
canShowWorkspaceDeleteQuickAction({
deleteModifierPressed: false,
isDeleting: false,
isMainWorktree: true,
cloudPrimary: true
})
).toBe(false)
expect(
canShowWorkspaceDeleteQuickAction({
deleteModifierPressed: true,
isDeleting: true,
isMainWorktree: true,
cloudPrimary: true
})
).toBe(false)
})
it.each([
['owned', ownedCloud],
['paired', pairedCloud]
])('%s cloud row: Option shows the trash and it opens the cloud delete', (_kind, worktree) => {
seed()
const view = card(worktree)
expect(trash(view.container)).toBeNull()
act(() => {
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Alt', altKey: true }))
})
const button = trash(view.container)
expect(button).not.toBeNull()
fireEvent.click(button!)
expect(mocks.cloudDelete).toHaveBeenCalledWith(expect.objectContaining({ id: worktree.id }))
expect(useAppStore.getState().activeModal).not.toBe('confirm-remove-folder')
})
it('a local primary still gets no trash; a local worktree does', () => {
seed()
const primary = card(localPrimary)
const worktree = card(localWorktree)
act(() => {
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Alt', altKey: true }))
})
expect(trash(primary.container)).toBeNull()
expect(trash(worktree.container)).not.toBeNull()
})
})