From 9e30fe31bd4cbd278753eecf5b8e79a0de86c35e Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 17 May 2026 15:03:15 -0700 Subject: [PATCH] Fix Kanban unread bell dismissal (#2163) --- .../WorktreeCard.quick-actions.test.tsx | 113 ++++++++++++++++++ .../src/components/sidebar/WorktreeCard.tsx | 11 ++ 2 files changed, 124 insertions(+) create mode 100644 src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx diff --git a/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx new file mode 100644 index 000000000..fa32f0d2d --- /dev/null +++ b/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx @@ -0,0 +1,113 @@ +import { renderToStaticMarkup } from 'react-dom/server' +import type { ReactNode } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Repo, Worktree, WorktreeCardProperty } from '../../../../shared/types' + +const fetchHostedReviewForBranch = vi.fn() +const fetchIssue = vi.fn() +const openModal = vi.fn() +const updateWorktreeMeta = vi.fn() + +let worktreeCardProperties: WorktreeCardProperty[] = ['status', 'unread'] + +vi.mock('@/store', () => ({ + useAppStore: (selector: (state: unknown) => unknown) => + selector({ + deleteStateByWorktreeId: {}, + fetchHostedReviewForBranch, + fetchIssue, + gitConflictOperationByWorktree: {}, + hostedReviewCache: {}, + issueCache: {}, + openModal, + remoteBranchConflictByWorktreeId: {}, + settings: null, + sshConnectionStates: new Map(), + sshTargetLabels: new Map(), + updateWorktreeMeta, + worktreeCardProperties + }) +})) + +vi.mock('@/lib/worktree-activation', () => ({ + activateAndRevealWorktree: vi.fn() +})) + +vi.mock('@/components/ui/tooltip', () => ({ + Tooltip: ({ children }: { children: ReactNode }) => <>{children}, + TooltipContent: ({ children }: { children: ReactNode }) => <>{children}, + TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children} +})) + +vi.mock('./use-worktree-activity-status', () => ({ + useWorktreeActivityStatus: () => 'idle' +})) + +vi.mock('./CacheTimer', () => ({ + default: () => null +})) + +vi.mock('./WorktreeCardAgents', () => ({ + default: () => null +})) + +vi.mock('./SshDisconnectedDialog', () => ({ + SshDisconnectedDialog: () => null +})) + +vi.mock('./WorktreeContextMenu', () => ({ + default: ({ children }: { children: ReactNode }) => <>{children}, + CLOSE_ALL_CONTEXT_MENUS_EVENT: 'orca:test-close-context-menus', + WORKTREE_CONTEXT_MENU_SCOPE_ATTR: 'data-orca-context-menu-scope' +})) + +function makeRepo(): Repo { + return { + id: 'repo-1', + path: '/repo', + displayName: 'orca', + badgeColor: '#999999', + addedAt: 1 + } +} + +function makeWorktree(overrides: Partial = {}): Worktree { + return { + id: 'repo-1::/repo/worktrees/quick-action', + repoId: 'repo-1', + path: '/repo/worktrees/quick-action', + displayName: 'Quick action', + branch: 'quick-action', + head: 'abc123', + isBare: false, + isMainWorktree: false, + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: true, + isPinned: false, + sortOrder: 0, + lastActivityAt: 1, + ...overrides + } +} + +describe('WorktreeCard quick actions', () => { + beforeEach(() => { + vi.clearAllMocks() + worktreeCardProperties = ['status', 'unread'] + }) + + it('marks the unread toggle as a workspace-board-preserving action', async () => { + const { default: WorktreeCard } = await import('./WorktreeCard') + + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('aria-label="Mark as read"') + expect(markup).toContain('data-workspace-board-preserve-open=""') + }) +}) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index 13bb326d4..13455fc6c 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -336,6 +336,15 @@ const WorktreeCard = React.memo(function WorktreeCard({ [isDeleting, isMultiSelected, selectedWorktrees, worktree.id] ) + const stopQuickActionPointerPropagation = useCallback( + (event: React.PointerEvent) => { + // Why: the Kanban board is dismissed by document-level pointer handling. + // Quick card actions mutate metadata, but must not count as card activation. + event.stopPropagation() + }, + [] + ) + // Why: the 'unread' card property is the user's opt-out. When off, we render // as if the workspace is read so bold emphasis never appears. The persisted // `worktree.isUnread` flag is unchanged; only the rendering changes. @@ -386,6 +395,8 @@ const WorktreeCard = React.memo(function WorktreeCard({