diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index 6c3b25b48..5b44c6dd5 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -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 () => { diff --git a/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx index 6afde5b7d..31578afa6 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx @@ -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 = {} let ptyIdsByTabId: Record = {} let browserTabsByWorktree: Record = {} let settings: Partial | 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 = {} 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( ) - 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( - - ) - - 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( - - ) - - 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( - - ) - - 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', () => { diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index 6fd5987ad..201d96da0 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -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) => { 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({ )} - {showWorkspaceQuickAction && ( + {showDeleteQuickAction && ( - {showDeleteQuickAction ? 'Delete workspace' : completionAction?.tooltip} + Delete workspace )} diff --git a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx index 203312209..f4dc1aa75 100644 --- a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx +++ b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx @@ -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({ )} - {completionAction && ( - - {completionAction.kind === 'mark-done' ? ( - - ) : ( - - )} - {completionAction.label} - - )} diff --git a/src/renderer/src/components/sidebar/worktree-completion-action.test.ts b/src/renderer/src/components/sidebar/worktree-completion-action.test.ts deleted file mode 100644 index 493f15be5..000000000 --- a/src/renderer/src/components/sidebar/worktree-completion-action.test.ts +++ /dev/null @@ -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 { - 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() - }) -}) diff --git a/src/renderer/src/components/sidebar/worktree-completion-action.ts b/src/renderer/src/components/sidebar/worktree-completion-action.ts deleted file mode 100644 index ca8f72678..000000000 --- a/src/renderer/src/components/sidebar/worktree-completion-action.ts +++ /dev/null @@ -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[], - 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' - } -} diff --git a/src/renderer/src/store/slices/worktrees.test.ts b/src/renderer/src/store/slices/worktrees.test.ts index caa71ecf2..9c3e215d3 100644 --- a/src/renderer/src/store/slices/worktrees.test.ts +++ b/src/renderer/src/store/slices/worktrees.test.ts @@ -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) - - 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' } diff --git a/src/shared/workspace-statuses.ts b/src/shared/workspace-statuses.ts index 0b44d9ac6..2d053dddb 100644 --- a/src/shared/workspace-statuses.ts +++ b/src/shared/workspace-statuses.ts @@ -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