Revert "Mark workspaces done without deleting them (#3217)" (#3996)

This reverts commit 426f869fa3.
This commit is contained in:
Neil 2026-05-30 23:30:17 -07:00 committed by GitHub
parent b8944852de
commit c86539dcd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 14 additions and 317 deletions

View File

@ -1845,19 +1845,6 @@ describe('Store', () => {
expect(updated.comment).toBe('updated')
})
it('setWorktreeMeta persists workspace status without deleting worktree metadata', async () => {
const store = await createStore()
store.setWorktreeMeta('wt1', { displayName: 'first', workspaceStatus: 'in-progress' })
const updated = store.setWorktreeMeta('wt1', { workspaceStatus: 'completed' })
store.flush()
const persisted = readDataFile() as PersistedState
expect(updated.displayName).toBe('first')
expect(store.getWorktreeMeta('wt1')?.workspaceStatus).toBe('completed')
expect(persisted.worktreeMeta['wt1']?.workspaceStatus).toBe('completed')
expect(persisted.worktreeMeta['wt1']?.displayName).toBe('first')
})
// ── 9. Settings: get/update ────────────────────────────────────────
it('updateSettings merges partial updates', async () => {

View File

@ -6,8 +6,7 @@ import type {
GlobalSettings,
Repo,
Worktree,
WorktreeCardProperty,
WorkspaceStatusDefinition
WorktreeCardProperty
} from '../../../../shared/types'
import type WorktreeCardComponent from './WorktreeCard'
import type * as WorkspaceDeleteQuickAction from './workspace-delete-quick-action'
@ -22,17 +21,6 @@ let tabsByWorktree: Record<string, { id: string }[]> = {}
let ptyIdsByTabId: Record<string, string[]> = {}
let browserTabsByWorktree: Record<string, { id: string }[]> = {}
let settings: Partial<GlobalSettings> | null = null
let workspaceStatuses: WorkspaceStatusDefinition[] = [
{ id: 'completed', label: 'Done', color: 'conductor-done', icon: 'conductor-done' },
{ id: 'in-review', label: 'In review', color: 'conductor-review', icon: 'conductor-review' },
{
id: 'in-progress',
label: 'In progress',
color: 'conductor-progress',
icon: 'conductor-progress'
},
{ id: 'todo', label: 'Todo', color: 'neutral', icon: 'circle' }
]
let workspaceDeleteModifierPressed = false
let gitConflictOperationByWorktree: Record<string, GitConflictOperation> = {}
let WorktreeCard: typeof WorktreeCardComponent
@ -55,7 +43,6 @@ vi.mock('@/store', () => ({
ptyIdsByTabId,
tabsByWorktree,
updateWorktreeMeta,
workspaceStatuses,
worktreeCardProperties
})
}))
@ -147,17 +134,6 @@ describe('WorktreeCard quick actions', () => {
ptyIdsByTabId = {}
browserTabsByWorktree = {}
settings = null
workspaceStatuses = [
{ id: 'completed', label: 'Done', color: 'conductor-done', icon: 'conductor-done' },
{ id: 'in-review', label: 'In review', color: 'conductor-review', icon: 'conductor-review' },
{
id: 'in-progress',
label: 'In progress',
color: 'conductor-progress',
icon: 'conductor-progress'
},
{ id: 'todo', label: 'Todo', color: 'neutral', icon: 'circle' }
]
workspaceDeleteModifierPressed = false
gitConflictOperationByWorktree = {}
})
@ -292,16 +268,15 @@ describe('WorktreeCard quick actions', () => {
expect(markup).not.toContain('data-worktree-card-meta-row=""')
})
it('shows mark done as the top-right quick action for an inactive workspace', () => {
it('hides delete by default for an inactive workspace', () => {
const markup = renderToStaticMarkup(
<WorktreeCard worktree={makeWorktree()} repo={makeRepo()} isActive={false} />
)
expect(markup).toContain('aria-label="Mark workspace done"')
expect(markup).not.toContain('aria-label="Delete workspace"')
})
it('replaces mark done with delete while Option/Alt is held', () => {
it('shows delete as the top-right quick action while Option/Alt is held', () => {
workspaceDeleteModifierPressed = true
const markup = renderToStaticMarkup(
@ -309,27 +284,9 @@ describe('WorktreeCard quick actions', () => {
)
expect(markup).toContain('aria-label="Delete workspace"')
expect(markup).not.toContain('aria-label="Mark workspace done"')
})
it('shows mark done as the quick action for inactive folder workspace instances', () => {
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree({
id: 'repo-1::/repo::workspace:123e4567-e89b-12d3-a456-426614174000',
path: '/repo',
isMainWorktree: false
})}
repo={{ ...makeRepo(), kind: 'folder' }}
isActive={false}
/>
)
expect(markup).toContain('aria-label="Mark workspace done"')
expect(markup).not.toContain('aria-label="Delete workspace"')
})
it('replaces mark done with delete for folder workspace instances while Option/Alt is held', () => {
it('shows delete as the quick action for folder workspace instances while Option/Alt is held', () => {
workspaceDeleteModifierPressed = true
const markup = renderToStaticMarkup(
@ -345,7 +302,6 @@ describe('WorktreeCard quick actions', () => {
)
expect(markup).toContain('aria-label="Delete workspace"')
expect(markup).not.toContain('aria-label="Mark workspace done"')
})
it('shows delete for a current workspace while Option/Alt is held', () => {
@ -371,10 +327,9 @@ describe('WorktreeCard quick actions', () => {
)
expect(markup).not.toContain('aria-label="Delete workspace"')
expect(markup).toContain('aria-label="Mark workspace done"')
})
it('keeps mark done available for a workspace with live activity', () => {
it('does not replace sleep with delete for a workspace with live activity', () => {
const worktree = makeWorktree()
tabsByWorktree = { [worktree.id]: [{ id: 'tab-1' }] }
ptyIdsByTabId = { 'tab-1': ['pty-1'] }
@ -385,10 +340,9 @@ describe('WorktreeCard quick actions', () => {
expect(markup).not.toContain('aria-label="Sleep workspace"')
expect(markup).not.toContain('aria-label="Delete workspace"')
expect(markup).toContain('aria-label="Mark workspace done"')
})
it('keeps mark done available for an active workspace', () => {
it('does not show sleep as the top-right quick action for an active workspace', () => {
const worktree = makeWorktree()
tabsByWorktree = { [worktree.id]: [{ id: 'tab-1' }] }
ptyIdsByTabId = { 'tab-1': ['pty-1'] }
@ -399,21 +353,9 @@ describe('WorktreeCard quick actions', () => {
expect(markup).not.toContain('aria-label="Sleep workspace"')
expect(markup).not.toContain('aria-label="Delete workspace"')
expect(markup).toContain('aria-label="Mark workspace done"')
})
it('reverses the quick action for a done workspace', () => {
const worktree = makeWorktree({ workspaceStatus: 'completed' })
const markup = renderToStaticMarkup(
<WorktreeCard worktree={worktree} repo={makeRepo()} isActive={false} />
)
expect(markup).toContain('aria-label="Mark workspace in progress"')
expect(markup).not.toContain('aria-label="Delete workspace"')
})
it('keeps delete hidden when the workspace is current but not selected in the sidebar', () => {
it('does not show delete when the workspace is current but not selected in the sidebar', () => {
const worktree = makeWorktree()
const markup = renderToStaticMarkup(
@ -421,18 +363,6 @@ describe('WorktreeCard quick actions', () => {
)
expect(markup).not.toContain('aria-label="Delete workspace"')
expect(markup).toContain('aria-label="Mark workspace done"')
})
it('hides the completion quick action when the Done status is unavailable', () => {
workspaceStatuses = [{ id: 'todo', label: 'Todo', color: 'neutral', icon: 'circle' }]
const markup = renderToStaticMarkup(
<WorktreeCard worktree={makeWorktree()} repo={makeRepo()} isActive={false} />
)
expect(markup).not.toContain('aria-label="Mark workspace done"')
expect(markup).not.toContain('aria-label="Delete workspace"')
})
it('does not show the rebase operation chip on the card', () => {

View File

@ -8,8 +8,6 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip
import {
AlertTriangle,
ChevronDown,
CircleCheck,
CircleDot,
GitMerge,
LoaderCircle,
Server,
@ -49,7 +47,6 @@ import { installWindowVisibilityInterval, isWindowVisible } from '@/lib/window-v
import { isMacAppDataPath } from '@/lib/passive-macos-app-data-access'
import { runWorktreeDelete } from './delete-worktree-flow'
import { WorktreeTitleInlineRename } from './WorktreeTitleInlineRename'
import { getWorktreeCompletionAction } from './worktree-completion-action'
import {
canShowWorkspaceDeleteQuickAction,
useWorkspaceDeleteModifierPressed
@ -121,7 +118,6 @@ const WorktreeCard = React.memo(function WorktreeCard({
const openModal = useAppStore((s) => s.openModal)
const openTaskPage = useAppStore((s) => s.openTaskPage)
const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta)
const workspaceStatuses = useAppStore((s) => s.workspaceStatuses)
const fetchHostedReviewForBranch = useAppStore((s) => s.fetchHostedReviewForBranch)
const settings = useAppStore((s) => s.settings)
const fetchIssue = useAppStore((s) => s.fetchIssue)
@ -436,10 +432,6 @@ const WorktreeCard = React.memo(function WorktreeCard({
},
[worktree.id, worktree.isUnread, updateWorktreeMeta]
)
// Why: completion is metadata-only, so it is safe on active, main, and SSH
// workspaces where a delete hover action would be too risky.
const completionAction = getWorktreeCompletionAction([worktree], workspaceStatuses)
const completionTargetStatus = completionAction?.targetStatus
// Why: delete is destructive, so it only appears while the user is holding
// Option/Alt instead of being part of the ordinary hover chrome.
const showDeleteQuickAction = canShowWorkspaceDeleteQuickAction({
@ -447,22 +439,15 @@ const WorktreeCard = React.memo(function WorktreeCard({
isDeleting,
isMainWorktree: worktree.isMainWorktree
})
const showCompletionQuickAction =
completionAction != null && !isDeleting && !showDeleteQuickAction
const showWorkspaceQuickAction = showDeleteQuickAction || showCompletionQuickAction
const handleWorkspaceQuickAction = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
event.stopPropagation()
if (showDeleteQuickAction) {
runWorktreeDelete(worktree.id)
return
}
if (completionTargetStatus) {
updateWorktreeMeta(worktree.id, { workspaceStatus: completionTargetStatus })
}
},
[completionTargetStatus, showDeleteQuickAction, updateWorktreeMeta, worktree.id]
[showDeleteQuickAction, worktree.id]
)
const unreadTooltip = worktree.isUnread ? 'Mark read' : 'Mark unread'
@ -586,7 +571,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
const showTitleRowUnread = compactCards && showUnreadQuickAction && !showStatus
const showTitleRowPrimary = compactCards && worktree.isMainWorktree && !isFolder
const hasMetaRow = hasMetadataBadge || cacheStartedAt != null
const showHeaderActions = showTitleRowUnread || showTitleRowPrimary || showWorkspaceQuickAction
const showHeaderActions = showTitleRowUnread || showTitleRowPrimary || showDeleteQuickAction
const showBranchIdentityHover = !isFolder && branch !== worktree.displayName
// Why: sidebar rows need a small surface inset, while their content remains
// aligned with the pre-inset layout and the repo header hierarchy.
@ -801,7 +786,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
</Tooltip>
)}
{showWorkspaceQuickAction && (
{showDeleteQuickAction && (
<Tooltip>
<TooltipTrigger asChild>
<button
@ -812,27 +797,15 @@ const WorktreeCard = React.memo(function WorktreeCard({
className={cn(
'inline-flex size-4 items-center justify-center rounded bg-transparent opacity-0 transition-colors transition-opacity',
'group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100',
showDeleteQuickAction
? 'text-muted-foreground hover:bg-destructive/10 hover:text-destructive focus-visible:bg-destructive/10 focus-visible:text-destructive'
: 'text-muted-foreground hover:bg-transparent hover:text-foreground focus-visible:bg-transparent focus-visible:text-foreground'
'text-muted-foreground hover:bg-destructive/10 hover:text-destructive focus-visible:bg-destructive/10 focus-visible:text-destructive'
)}
aria-label={
showDeleteQuickAction
? 'Delete workspace'
: (completionAction?.ariaLabel ?? '')
}
aria-label="Delete workspace"
>
{showDeleteQuickAction ? (
<Trash2 className="size-3.5" />
) : completionAction?.kind === 'mark-done' ? (
<CircleCheck className="size-3.5" />
) : (
<CircleDot className="size-3.5" />
)}
<Trash2 className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{showDeleteQuickAction ? 'Delete workspace' : completionAction?.tooltip}
Delete workspace
</TooltipContent>
</Tooltip>
)}

View File

@ -17,8 +17,6 @@ import {
Copy,
Bell,
BellOff,
CircleCheck,
CircleDot,
CircleX,
Moon,
Pencil,
@ -44,7 +42,6 @@ import { getLineageRenderInfo } from './worktree-list-groups'
import { getWorkspaceStatus, getWorkspaceStatusVisualMeta } from './workspace-status'
import { WorktreeOpenInSubMenu } from './WorktreeOpenInMenu'
import { ProjectGroupNameDialog } from './ProjectGroupNameDialog'
import { getWorktreeCompletionAction } from './worktree-completion-action'
type Props = {
worktree: Worktree
@ -256,10 +253,6 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({
? status
: ''
}, [activeContextWorktrees, workspaceStatuses])
const completionAction = useMemo(
() => getWorktreeCompletionAction(activeContextWorktrees, workspaceStatuses),
[activeContextWorktrees, workspaceStatuses]
)
const batchDeleteWorktrees = useMemo(
() =>
activeContextWorktrees.filter((item) => {
@ -364,28 +357,6 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({
[activeContextWorktrees, setMenuOpenState, updateWorktreeMeta, workspaceStatuses]
)
const handleAssignCompletionStatus = useCallback(() => {
if (!completionAction) {
return
}
setMenuOpenState(false)
// Why: marking done is only board metadata; SSH and local workspaces must
// avoid archive hooks, filesystem deletion, or git worktree removal.
void Promise.all(
activeContextWorktrees.map((item) =>
getWorkspaceStatus(item, workspaceStatuses) === completionAction.targetStatus
? Promise.resolve()
: updateWorktreeMeta(item.id, { workspaceStatus: completionAction.targetStatus })
)
)
}, [
activeContextWorktrees,
completionAction,
setMenuOpenState,
updateWorktreeMeta,
workspaceStatuses
])
const handleRename = useCallback(() => {
openModal('edit-meta', {
worktreeId: worktree.id,
@ -615,16 +586,6 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({
<DropdownMenuSeparator />
</>
)}
{completionAction && (
<DropdownMenuItem onSelect={handleAssignCompletionStatus} disabled={deletingContext}>
{completionAction.kind === 'mark-done' ? (
<CircleCheck className="size-3.5" />
) : (
<CircleDot className="size-3.5" />
)}
{completionAction.label}
</DropdownMenuItem>
)}
<DropdownMenuSub>
<DropdownMenuSubTrigger disabled={deletingContext}>
<Kanban className="size-3.5" />

View File

@ -1,79 +0,0 @@
import { describe, expect, it } from 'vitest'
import { cloneDefaultWorkspaceStatuses } from '../../../../shared/workspace-statuses'
import type { Worktree } from '../../../../shared/types'
import { getWorktreeCompletionAction } from './worktree-completion-action'
function makeWorktree(workspaceStatus?: string): Pick<Worktree, 'workspaceStatus'> {
return { workspaceStatus }
}
describe('getWorktreeCompletionAction', () => {
it('targets Done for an active workspace', () => {
const action = getWorktreeCompletionAction(
[makeWorktree('in-progress')],
cloneDefaultWorkspaceStatuses()
)
expect(action).toMatchObject({
kind: 'mark-done',
targetStatus: 'completed',
label: 'Mark Done',
ariaLabel: 'Mark workspace done'
})
})
it('reverses a done workspace back to In Progress', () => {
const action = getWorktreeCompletionAction(
[makeWorktree('completed')],
cloneDefaultWorkspaceStatuses()
)
expect(action).toMatchObject({
kind: 'mark-in-progress',
targetStatus: 'in-progress',
label: 'Mark In Progress',
ariaLabel: 'Mark workspace in progress'
})
})
it('marks a mixed multi-selection done and includes the selected count', () => {
const action = getWorktreeCompletionAction(
[makeWorktree('completed'), makeWorktree('todo')],
cloneDefaultWorkspaceStatuses()
)
expect(action).toMatchObject({
kind: 'mark-done',
targetStatus: 'completed',
label: 'Mark 2 Done',
ariaLabel: 'Mark selected workspaces done'
})
})
it('reverses an all-done multi-selection together', () => {
const action = getWorktreeCompletionAction(
[makeWorktree('completed'), makeWorktree('completed')],
cloneDefaultWorkspaceStatuses()
)
expect(action).toMatchObject({
kind: 'mark-in-progress',
targetStatus: 'in-progress',
label: 'Mark 2 In Progress',
ariaLabel: 'Mark selected workspaces in progress'
})
})
it('hides when the target status is not part of the current status board', () => {
expect(
getWorktreeCompletionAction(
[makeWorktree('todo')],
[{ id: 'todo', label: 'Todo', color: 'neutral', icon: 'circle' }]
)
).toBeNull()
})
it('hides while workspace statuses are unavailable', () => {
expect(getWorktreeCompletionAction([makeWorktree('todo')], undefined)).toBeNull()
})
})

View File

@ -1,54 +0,0 @@
import type { Worktree, WorkspaceStatus, WorkspaceStatusDefinition } from '../../../../shared/types'
import {
DEFAULT_WORKSPACE_STATUS_ID,
DONE_WORKSPACE_STATUS_ID,
getWorkspaceStatus,
isWorkspaceStatusId
} from '../../../../shared/workspace-statuses'
export type WorktreeCompletionActionKind = 'mark-done' | 'mark-in-progress'
export type WorktreeCompletionAction = {
kind: WorktreeCompletionActionKind
targetStatus: WorkspaceStatus
label: string
tooltip: string
ariaLabel: string
}
export function getWorktreeCompletionAction(
worktrees: readonly Pick<Worktree, 'workspaceStatus'>[],
workspaceStatuses: readonly WorkspaceStatusDefinition[] | undefined
): WorktreeCompletionAction | null {
if (worktrees.length === 0 || !Array.isArray(workspaceStatuses)) {
return null
}
const allDone = worktrees.every(
(worktree) => getWorkspaceStatus(worktree, workspaceStatuses) === DONE_WORKSPACE_STATUS_ID
)
const targetStatus = allDone ? DEFAULT_WORKSPACE_STATUS_ID : DONE_WORKSPACE_STATUS_ID
if (!isWorkspaceStatusId(targetStatus, workspaceStatuses)) {
return null
}
const isMulti = worktrees.length > 1
if (allDone) {
return {
kind: 'mark-in-progress',
targetStatus,
label: isMulti ? `Mark ${worktrees.length} In Progress` : 'Mark In Progress',
tooltip: 'Mark in progress',
ariaLabel: isMulti ? 'Mark selected workspaces in progress' : 'Mark workspace in progress'
}
}
return {
kind: 'mark-done',
targetStatus,
label: isMulti ? `Mark ${worktrees.length} Done` : 'Mark Done',
tooltip: 'Mark done',
ariaLabel: isMulti ? 'Mark selected workspaces done' : 'Mark workspace done'
}
}

View File

@ -1921,26 +1921,6 @@ describe('worktree remote runtime mutations', () => {
expect(store.getState().worktreesByRepo.repo1[0]?.comment).toBe('remote note')
})
it('marks a workspace done by persisting workspace status metadata only', async () => {
const store = createTestStore()
const wt = makeWorktree({
id: 'repo1::/path/wt1',
repoId: 'repo1',
path: '/path/wt1',
workspaceStatus: 'in-progress'
})
store.setState({ worktreesByRepo: { repo1: [wt] } } as Partial<AppState>)
await store.getState().updateWorktreeMeta(wt.id, { workspaceStatus: 'completed' })
expect(mockApi.worktrees.updateMeta).toHaveBeenCalledWith({
worktreeId: wt.id,
updates: { workspaceStatus: 'completed' }
})
expect(mockApi.worktrees.remove).not.toHaveBeenCalled()
expect(store.getState().worktreesByRepo.repo1[0]?.workspaceStatus).toBe('completed')
})
it('resolves and persists a push target when manually linking a GitHub PR', async () => {
const store = createTestStore()
const pushTarget = { remoteName: 'origin', branchName: 'bot/pr-bug-scan-2504' }

View File

@ -16,7 +16,6 @@ type WorkspaceStatusNormalizationOptions = {
}
export const DEFAULT_WORKSPACE_STATUS_ID: WorkspaceStatus = 'in-progress'
export const DONE_WORKSPACE_STATUS_ID: WorkspaceStatus = 'completed'
export const DEFAULT_WORKSPACE_STATUS_COLOR_ID = 'neutral'
export const DEFAULT_WORKSPACE_STATUS_ICON_ID = 'circle-dot'
export const WORKSPACE_BOARD_COLUMN_WIDTH_DEFAULT = 308