diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 5278f980fd8..9787ac27c2d 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -1274,6 +1274,29 @@ html.native-shell .app-layout { box-shadow: 0 0 0 1px color-mix(in srgb, var(--sidebar-ring) 18%, transparent); } +/* Why: a sleeping card dims by mixing the theme's own text tokens toward the + sidebar surface in oklab, not by painting opacity over whatever is behind it. + An opacity step shrinks as the surface lightens, so the cue faded on lighter + themes and custom tints; an oklab mix is a fixed perceptual step, keeps themed + hues instead of greying them, and reaches every descendant through the tokens + they already use (#19624). The amber unread badge sits outside this family and + stays full-bright. */ +[data-worktree-sleeping-dim] { + color: color-mix(in oklab, var(--worktree-sidebar-foreground) 52%, var(--worktree-sidebar)); + --foreground: color-mix( + in oklab, + var(--worktree-sidebar-foreground) 52%, + var(--worktree-sidebar) + ); + --muted-foreground: color-mix( + in oklab, + var(--worktree-sidebar-foreground) 34%, + var(--worktree-sidebar) + ); + --muted: color-mix(in oklab, var(--worktree-sidebar-foreground) 5%, var(--worktree-sidebar)); + --border: color-mix(in oklab, var(--worktree-sidebar-foreground) 5%, var(--worktree-sidebar)); +} + .worktree-agent-row-hover:hover { background: color-mix(in srgb, var(--sidebar-foreground) 1.25%, transparent); } diff --git a/src/renderer/src/assets/worktree-card-active-style.test.ts b/src/renderer/src/assets/worktree-card-active-style.test.ts index 814c7ae55ee..5057da135b9 100644 --- a/src/renderer/src/assets/worktree-card-active-style.test.ts +++ b/src/renderer/src/assets/worktree-card-active-style.test.ts @@ -42,4 +42,22 @@ describe('worktree card active styling', () => { expect(secondary).toContain('var(--sidebar-ring) 15%') expect(darkSecondary).toContain('var(--sidebar-ring) 18%') }) + + it('dims sleeping cards through theme tokens so the cue survives any surface', () => { + const sleeping = getCssRuleBody('[data-worktree-sleeping-dim]') + + // Why oklab: a fixed perceptual step. An sRGB alpha over the painted backdrop + // shrank as the surface lightened, so slept and awake read alike (#19624). + expect(sleeping).toContain('in oklab') + // Why token-anchored: the mix is defined by the theme's own foreground and + // surface, so a custom background or tint scales it instead of cancelling it. + expect(sleeping).toContain('var(--worktree-sidebar-foreground)') + expect(sleeping).toContain('var(--worktree-sidebar)') + // Why these two: title text and the muted lane (Moon, host badge) carry the cue. + expect(sleeping).toContain('--foreground:') + expect(sleeping).toContain('--muted-foreground:') + // Why not opacity/filter: both dim toward the backdrop or strip themed hues. + expect(sleeping).not.toContain('opacity:') + expect(sleeping).not.toContain('filter:') + }) }) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx index 73927976237..e451ce5681f 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx @@ -14,6 +14,9 @@ const updateWorktreeMeta = vi.fn() const testDoubles = vi.hoisted(() => ({ activateWorktreeFromSidebar: vi.fn() })) +const sleepMocks = vi.hoisted(() => ({ + sleeping: false +})) let worktreeCardProperties: WorktreeCardProperty[] = ['status', 'comment'] let settings: Partial | null = null @@ -74,6 +77,10 @@ vi.mock('./use-worktree-activity-status', () => ({ useWorktreeActivityStatus: () => 'idle' })) +vi.mock('./use-worktree-sleep-state', () => ({ + useIsSleepingWorktree: () => sleepMocks.sleeping +})) + vi.mock('./CacheTimer', () => ({ default: () => null, usePromptCacheCountdownStartedAt: () => null @@ -154,6 +161,7 @@ describe('WorktreeCard affiliate list mode', () => { vi.clearAllMocks() worktreeCardProperties = ['status', 'comment'] settings = null + sleepMocks.sleeping = false }) afterEach(() => { @@ -228,4 +236,91 @@ describe('WorktreeCard affiliate list mode', () => { expect(container.querySelector('[data-testid="inline-agents"]')).not.toBeNull() }) + + it('dims the full card surface for sleeping workspaces in new card style', () => { + sleepMocks.sleeping = true + settings = { experimentalNewWorktreeCardStyle: true } + + act(() => { + root.render( + + ) + }) + + expect(container.querySelector('[data-worktree-sleeping-dim=""]')).not.toBeNull() + const dim = container.querySelector('[data-worktree-sleeping-dim=""]') + expect(dim).not.toBeNull() + // Why pinned: the dim is a theme-token mix in main.css, not an opacity or filter + // over the painted backdrop — those faded out on lighter surfaces and custom tints. + expect(dim?.getAttribute('class') ?? '').not.toContain('opacity-') + expect(dim?.getAttribute('class') ?? '').not.toContain('backdrop-') + }) + + it('keeps awake cards at full opacity in new card style', () => { + settings = { experimentalNewWorktreeCardStyle: true } + + act(() => { + root.render( + + ) + }) + + expect(container.querySelector('[data-worktree-sleeping-dim=""]')).toBeNull() + }) + + it('keeps legacy sleeping cards undimmed', () => { + sleepMocks.sleeping = true + + act(() => { + root.render( + + ) + }) + + expect(container.querySelector('[data-worktree-sleeping-dim=""]')).toBeNull() + }) + + it('keeps the unread badge rendered on a dimmed sleeping card', () => { + sleepMocks.sleeping = true + settings = { experimentalNewWorktreeCardStyle: true } + + act(() => { + root.render( + + ) + }) + + // Why both: the dim marks the row as sleeping while the unread badge still + // renders, so an unread sleeping row stays noticeable. + expect(container.querySelector('[data-worktree-sleeping-dim=""]')).not.toBeNull() + expect(container.querySelector('[data-worktree-unread-alert=""]')).not.toBeNull() + }) }) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx index 2d6678fb11c..069eb2c0701 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx @@ -99,6 +99,10 @@ vi.mock('./use-worktree-activity-status', () => ({ useWorktreeActivityStatus: () => 'active' })) +vi.mock('./use-worktree-sleep-state', () => ({ + useIsSleepingWorktree: () => false +})) + vi.mock('./CacheTimer', () => ({ default: () => null, usePromptCacheCountdownStartedAt: cacheTimerMocks.usePromptCacheCountdownStartedAt diff --git a/src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx index 2c1f10945d3..2b5411f7899 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx @@ -114,6 +114,10 @@ vi.mock('./use-worktree-activity-status', () => ({ useWorktreeActivityStatus: () => 'active' })) +vi.mock('./use-worktree-sleep-state', () => ({ + useIsSleepingWorktree: () => false +})) + vi.mock('./CacheTimer', () => ({ default: () => null, usePromptCacheCountdownStartedAt: cacheTimerMocks.usePromptCacheCountdownStartedAt diff --git a/src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx index 6d9fa6d1b5d..269f9ed9ac7 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx @@ -76,6 +76,10 @@ vi.mock('./use-worktree-activity-status', () => ({ useWorktreeActivityStatus: () => 'active' })) +vi.mock('./use-worktree-sleep-state', () => ({ + useIsSleepingWorktree: () => false +})) + function makeRepo(): Repo { return { id: 'repo-1', diff --git a/src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx index 1e26254e712..9147a881c47 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx @@ -36,9 +36,7 @@ vi.mock('@/store', () => ({ }) })) -vi.mock('@/lib/worktree-activation', () => ({ - activateAndRevealWorktree: vi.fn() -})) +vi.mock('@/lib/worktree-activation', () => ({ activateAndRevealWorktree: vi.fn() })) vi.mock('@/components/ui/tooltip', () => ({ Tooltip: ({ children }: { children: ReactNode }) => <>{children}, @@ -46,6 +44,10 @@ vi.mock('@/components/ui/tooltip', () => ({ TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children} })) +vi.mock('./use-worktree-sleep-state', () => ({ + useIsSleepingWorktree: () => false +})) + vi.mock('./CacheTimer', () => ({ default: () => null, usePromptCacheCountdownStartedAt: () => null diff --git a/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx index 7c9669a22f9..481b6ee25a4 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx @@ -57,6 +57,10 @@ vi.mock('./use-worktree-activity-status', () => ({ useWorktreeActivityStatus: () => 'active' })) +vi.mock('./use-worktree-sleep-state', () => ({ + useIsSleepingWorktree: () => false +})) + vi.mock('./CacheTimer', () => ({ default: () => null, usePromptCacheCountdownStartedAt: () => null diff --git a/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx index 47017f87fab..ba317dbfed2 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx @@ -53,6 +53,10 @@ vi.mock('./use-worktree-activity-status', () => ({ useWorktreeActivityStatus: () => 'idle' })) +vi.mock('./use-worktree-sleep-state', () => ({ + useIsSleepingWorktree: () => false +})) + vi.mock('./CacheTimer', () => ({ default: () => null, usePromptCacheCountdownStartedAt: () => null diff --git a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx index 1ae9026cc74..b605c5258ee 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx @@ -61,6 +61,10 @@ vi.mock('./use-worktree-activity-status', () => ({ useWorktreeActivityStatus: () => 'active' })) +vi.mock('./use-worktree-sleep-state', () => ({ + useIsSleepingWorktree: () => false +})) + vi.mock('./CacheTimer', () => ({ default: () => null, usePromptCacheCountdownStartedAt: () => null 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 ffef859d9ca..c411b58bd9a 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx @@ -61,6 +61,10 @@ vi.mock('./use-worktree-activity-status', () => ({ useWorktreeActivityStatus: () => 'idle' })) +vi.mock('./use-worktree-sleep-state', () => ({ + useIsSleepingWorktree: () => false +})) + vi.mock('./CacheTimer', () => ({ default: () => null, usePromptCacheCountdownStartedAt: () => null diff --git a/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx index 55ef3263ad4..3ee353bf61a 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx @@ -73,6 +73,10 @@ vi.mock('./use-worktree-activity-status', () => ({ useWorktreeActivityStatus: () => 'idle' })) +vi.mock('./use-worktree-sleep-state', () => ({ + useIsSleepingWorktree: () => false +})) + vi.mock('./WorktreeContextMenu', () => ({ default: ({ children }: { children: ReactNode }) => <>{children}, CLOSE_ALL_CONTEXT_MENUS_EVENT: 'orca:test-close-context-menus', diff --git a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx index 9baed34e995..f8fc458a5dd 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx @@ -5,7 +5,8 @@ import { WorktreeCardStatusSlot } from './WorktreeCardStatusSlot' import type { WorktreeCardPrDisplay } from './worktree-card-pr-display' const mocks = vi.hoisted(() => ({ - status: 'active' + status: 'active', + sleeping: false })) vi.mock('@/components/ui/tooltip', () => ({ @@ -20,9 +21,14 @@ vi.mock('./use-worktree-activity-status', () => ({ useWorktreeActivityStatus: () => mocks.status })) +vi.mock('./use-worktree-sleep-state', () => ({ + useIsSleepingWorktree: () => mocks.sleeping +})) + describe('WorktreeCardStatusSlot', () => { beforeEach(() => { mocks.status = 'active' + mocks.sleeping = false }) const review: WorktreeCardPrDisplay = { @@ -71,7 +77,6 @@ describe('WorktreeCardStatusSlot', () => { onPointerDown={vi.fn()} onToggleUnread={vi.fn()} newCardStyle - hasBranchIdentity={false} /> ) @@ -98,7 +103,6 @@ describe('WorktreeCardStatusSlot', () => { onPointerDown={vi.fn()} onToggleUnread={vi.fn()} newCardStyle - hasBranchIdentity={false} /> ) @@ -123,7 +127,6 @@ describe('WorktreeCardStatusSlot', () => { onPointerDown={vi.fn()} onToggleUnread={vi.fn()} newCardStyle - hasBranchIdentity={false} /> ) @@ -260,8 +263,11 @@ describe('WorktreeCardStatusSlot', () => { expect(markup).not.toContain('bg-emerald-500') }) - it('uses PR status instead of the inactive dot when new card style is on', () => { - mocks.status = 'inactive' + it('keeps sleeping distinct from PR status when new card style is on', () => { + // Why done, not inactive: a slept workspace keeps its retained done rows, + // so its status still reads 'done' — the exact case from #19624. + mocks.status = 'done' + mocks.sleeping = true const markup = renderToStaticMarkup( { /> ) - expect(markup).toContain('PR checks: Failed') - expect(markup).toContain('text-rose-500/85') + // Why: sleep must stay distinct from awake completion; sleeping never collapses into PR. + expect(markup).toContain('Sleeping') + expect(markup).toContain('lucide-moon') + expect(markup).not.toContain('PR checks: Failed') + expect(markup).not.toContain('text-rose-500/85') expect(markup).not.toContain('bg-neutral-500/40') }) - it('uses a branch icon with branch-only accessible copy by default', () => { + it('keeps sleeping moon distinct from the awake green dot when new card style is on', () => { + mocks.status = 'done' + mocks.sleeping = true const markup = renderToStaticMarkup( { onPointerDown={vi.fn()} onToggleUnread={vi.fn()} newCardStyle - hasBranchIdentity /> ) - expect(markup).toContain('Branch') - expect(markup).not.toContain('Branch or folder path') - expect(markup).toContain('lucide-git-branch') - expect(markup).toContain('size-[13px] translate-x-px text-muted-foreground/70') - expect(markup).toContain('text-muted-foreground/70') + expect(markup).toContain('Sleeping') + expect(markup).toContain('lucide-moon') + expect(markup).not.toContain('lucide-git-branch') expect(markup).not.toContain('bg-emerald-500') - expect(markup).not.toContain('data-tooltip-root') + expect(markup).not.toContain('bg-neutral-500/40') }) - it('uses context-aware branch or folder path accessible copy', () => { - const markup = renderToStaticMarkup( + it('distinguishes awake branch from sleeping moon when new card style is on', () => { + mocks.status = 'done' + const awakeMarkup = renderToStaticMarkup( { onToggleUnread={vi.fn()} newCardStyle hasBranchIdentity - branchIdentityLabel="Branch or folder path" /> ) - - expect(markup).toContain('Branch or folder path') - expect(markup).toContain('lucide-git-branch') - expect(markup).not.toContain('data-tooltip-root') + expect(awakeMarkup).toContain('Branch') + expect(awakeMarkup).toContain('lucide-git-branch') + expect(awakeMarkup).not.toContain('lucide-moon') + mocks.sleeping = true + const sleepingMarkup = renderToStaticMarkup( + + ) + // Why: sleeping wins over the branch lane, even with an identity present. + expect(sleepingMarkup).toContain('Sleeping') + expect(sleepingMarkup).toContain('lucide-moon') + expect(sleepingMarkup).not.toContain('lucide-git-branch') }) - it('keeps the quiet dot when the row has no branch identity', () => { + it('shows the green awake dot for quiet done workspaces when new card style is on', () => { + mocks.status = 'done' const markup = renderToStaticMarkup( { onPointerDown={vi.fn()} onToggleUnread={vi.fn()} newCardStyle - hasBranchIdentity={false} /> ) - expect(markup).toContain('Active') + expect(markup).toContain('Done') expect(markup).toContain('bg-emerald-500') expect(markup).not.toContain('lucide-git-branch') - expect(markup).not.toContain('data-tooltip-root') + expect(markup).not.toContain('lucide-moon') + expect(markup).toContain('data-tooltip-root') }) it('keeps working activity ahead of PR status in new card style', () => { @@ -447,7 +473,9 @@ describe('WorktreeCardStatusSlot', () => { expect(markup).not.toContain('data-tooltip-root') }) - it('overlays an unread badge on the branch icon in new card style', () => { + it('overlays an unread badge on the sleeping moon in new card style', () => { + mocks.status = 'done' + mocks.sleeping = true const markup = renderToStaticMarkup( { onPointerDown={vi.fn()} onToggleUnread={vi.fn()} newCardStyle - hasBranchIdentity /> ) - expect(markup).toContain('Branch · Unread') + expect(markup).toContain('Sleeping · Unread') expect(markup).toContain('data-worktree-status-lane-unread=""') expect(markup).toContain('data-worktree-unread-alert=""') expect(markup).not.toContain('Mark as read') expect(markup).not.toContain('group/unread') expect(markup).not.toContain('cursor-pointer') - expect(markup).toContain('lucide-git-branch') + expect(markup).toContain('lucide-moon') expect(markup).toContain('bg-amber-500') expect(markup).not.toContain('lucide-bell') expect(markup).not.toContain('text-amber-500') diff --git a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx index 72b04b22b1f..aaceddad464 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Bell, GitBranch } from 'lucide-react' +import { Bell, GitBranch, Moon } from 'lucide-react' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { translate } from '@/i18n/i18n' import { cn } from '@/lib/utils' @@ -7,6 +7,7 @@ import { getWorktreeStatusLabel, type WorktreeStatus } from '@/lib/worktree-stat import { FilledBellIcon } from './WorktreeCardHelpers' import StatusIndicator from './StatusIndicator' import { useWorktreeActivityStatus } from './use-worktree-activity-status' +import { useIsSleepingWorktree } from './use-worktree-sleep-state' import type { WorktreeCardPrDisplay } from './worktree-card-pr-display' import { getReviewLabel, ReviewIcon } from './worktree-review-helpers' @@ -31,10 +32,16 @@ const QUIET_REVIEW_REPLACEABLE_STATUSES = new Set(['active', 'do function getDefaultBranchIdentityLabel(): string { return translate('auto.components.sidebar.WorktreeCardStatusSlot.branchIdentity', 'Branch') } +function getSleepingStatusLabel(): string { + return translate('auto.components.sidebar.WorktreeCardStatusSlot.sleeping', 'Sleeping') +} // Why: branch-style SVGs are optically left-heavy; this keeps them aligned with // the centered activity dots in the shared status column. const compactReviewAndBranchStatusIconClassName = 'size-[13px] translate-x-px' const branchStatusIconClassName = `${compactReviewAndBranchStatusIconClassName} text-muted-foreground/70` +// Why no faint tint here: the sleeping row is dimmed as a whole, so the glyph +// keeps full muted-foreground and dims with everything around it. +const sleepingStatusIconClassName = 'size-[13px] text-muted-foreground' // Why: a left-edge badge overlays unread on the status glyph without widening // the lane or indenting the title; ring-sidebar cuts the dot out from busy icons. const newCardUnreadAlertClassName = @@ -101,20 +108,29 @@ export function WorktreeCardStatusSlot({ className }: WorktreeCardStatusSlotProps): React.JSX.Element | null { const status = useWorktreeActivityStatus(worktreeId) + const isSleeping = useIsSleepingWorktree(worktreeId) const statusLabel = getWorktreeStatusLabel(status) || status + // Why: sleep must stay distinct from awake completion; a sleeping workspace + // never collapses into branch/PR, even when retained done rows keep its + // status at 'done'. Attention states keep their own glyphs by construction. + const canShowSleepingStatus = + newCardStyle && showStatus && isSleeping && QUIET_REVIEW_REPLACEABLE_STATUSES.has(status) const canShowReviewStatus = newCardStyle && showStatus && prDisplay !== null && + !canShowSleepingStatus && QUIET_REVIEW_REPLACEABLE_STATUSES.has(status) const canShowBranchStatus = newCardStyle && showStatus && hasBranchIdentity && prDisplay === null && + !canShowSleepingStatus && QUIET_REVIEW_REPLACEABLE_STATUSES.has(status) - const passiveStatusLabel = - canShowReviewStatus && prDisplay + const passiveStatusLabel = canShowSleepingStatus + ? getSleepingStatusLabel() + : canShowReviewStatus && prDisplay ? getReviewStatusLabel(prDisplay) : canShowBranchStatus ? (branchIdentityLabel ?? getDefaultBranchIdentityLabel()) @@ -127,35 +143,40 @@ export function WorktreeCardStatusSlot({ newCardStyle && isUnread && showStatus && status !== 'working' && status !== 'permission' const reviewStatusIconClassName = compactReviewAndBranchStatusIconClassName const branchStatusIcon =