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 fef27ea71..a219841b9 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx @@ -60,7 +60,7 @@ vi.mock('@/components/ui/tooltip', () => ({ })) vi.mock('./use-worktree-activity-status', () => ({ - useWorktreeActivityStatus: () => 'active' + useWorktreeActivityStatus: () => 'inactive' })) vi.mock('./CacheTimer', () => ({ @@ -171,8 +171,8 @@ describe('WorktreeCard linked PR display', () => { ) - expect(markup).toContain('Active') - expect(markup).toContain('bg-emerald-500') + expect(markup).toContain('Inactive') + expect(markup).toContain('bg-neutral-500/40') expect(markup).not.toContain('PR: Open') expect(markup).not.toContain('Linked PR #456') }, 20_000) 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 7456cce2f..54322d38f 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx @@ -21,6 +21,7 @@ let tabsByWorktree: Record = {} let ptyIdsByTabId: Record = {} let browserTabsByWorktree: Record = {} let settings: Partial | null = null +let activityStatus = 'idle' let projectGroups: unknown[] = [] let workspaceDeleteModifierPressed = false let gitConflictOperationByWorktree: Record = {} @@ -60,7 +61,7 @@ vi.mock('@/components/ui/tooltip', () => ({ })) vi.mock('./use-worktree-activity-status', () => ({ - useWorktreeActivityStatus: () => 'idle' + useWorktreeActivityStatus: () => activityStatus })) vi.mock('./CacheTimer', () => ({ @@ -144,6 +145,7 @@ describe('WorktreeCard quick actions', () => { projectGroups = [] workspaceDeleteModifierPressed = false gitConflictOperationByWorktree = {} + activityStatus = 'idle' }) it('marks the unread toggle as a workspace-board-preserving action', () => { @@ -252,6 +254,19 @@ describe('WorktreeCard quick actions', () => { expect(markup).not.toContain('lucide-git-branch') }) + it('keeps the quiet status dot when the branch card property is off', () => { + settings = { experimentalNewWorktreeCardStyle: true } + worktreeCardProperties = ['status'] + activityStatus = 'inactive' + + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('bg-neutral-500/40') + expect(markup).not.toContain('lucide-git-branch') + }) + it('does not render a pending first-agent rename title badge', () => { const markup = renderToStaticMarkup( ) : null} diff --git a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx index 8911c1783..8f204527c 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx @@ -192,7 +192,7 @@ describe('WorktreeCardStatusSlot', () => { expect(markup).not.toContain('PR checks: Failed') }) - it('uses PR status instead of the quiet active dot when new card style is on', () => { + it('keeps the active dot ahead of PR status when new card style is on', () => { const markup = renderToStaticMarkup( { /> ) - expect(markup).toContain('PR checks: Failed') + expect(markup).toContain('Active') expect(markup).toContain('inline-flex size-5 items-center justify-center') - expect(markup).toContain('size-[13px] translate-x-px') - expect(markup).toContain('text-rose-500/85') - expect(markup).not.toContain('bg-emerald-500') + expect(markup).toContain('bg-emerald-500') + expect(markup).not.toContain('PR checks: Failed') + }) + + it('keeps the emerald activity dot ahead of branch identity when new card style is on', () => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('Active') + expect(markup).toContain('bg-emerald-500') + expect(markup).not.toContain('lucide-git-branch') }) it('uses the unified compact review glyph for GitLab MR status', () => { + mocks.status = 'inactive' const markup = renderToStaticMarkup( { expect(markup).not.toContain('lucide-git-merge') }) - it('uses PR status instead of the quiet done dot when new card style is on', () => { + it('keeps the done dot ahead of PR status when new card style is on', () => { mocks.status = 'done' const markup = renderToStaticMarkup( { /> ) - expect(markup).toContain('PR checks: Failed') - expect(markup).not.toContain('bg-emerald-500') + expect(markup).toContain('Done') + expect(markup).toContain('bg-emerald-500') + expect(markup).not.toContain('PR checks: Failed') }) it('uses PR status instead of the inactive dot when new card style is on', () => { @@ -277,7 +298,8 @@ describe('WorktreeCardStatusSlot', () => { expect(markup).not.toContain('bg-neutral-500/40') }) - it('uses a branch icon with branch-only tooltip copy by default', () => { + it('uses a branch icon with branch-only tooltip copy on quiet rows', () => { + mocks.status = 'inactive' const markup = renderToStaticMarkup( { }) it('uses context-aware branch or folder path tooltip copy', () => { + mocks.status = 'inactive' const markup = renderToStaticMarkup( { }) it('overlays an unread badge on PR status when new card style is on', () => { + mocks.status = 'inactive' const markup = renderToStaticMarkup( { }) it('overlays an unread badge on the branch icon in new card style', () => { + mocks.status = 'inactive' const markup = renderToStaticMarkup( (['active', 'done', 'inactive']) +// Passive identity only replaces status when the workspace is inactive. +const IDENTITY_REPLACEABLE_STATUSES = new Set(['inactive']) // Why: a missing review display can also mean provider state is unavailable, // so the passive label names the identity cue without claiming no review exists. function getDefaultBranchIdentityLabel(): string { @@ -103,16 +104,13 @@ export function WorktreeCardStatusSlot({ const status = useWorktreeActivityStatus(worktreeId) const statusLabel = getWorktreeStatusLabel(status) || status const canShowReviewStatus = - newCardStyle && - showStatus && - prDisplay !== null && - QUIET_REVIEW_REPLACEABLE_STATUSES.has(status) + newCardStyle && showStatus && prDisplay !== null && IDENTITY_REPLACEABLE_STATUSES.has(status) const canShowBranchStatus = newCardStyle && showStatus && hasBranchIdentity && prDisplay === null && - QUIET_REVIEW_REPLACEABLE_STATUSES.has(status) + IDENTITY_REPLACEABLE_STATUSES.has(status) const passiveStatusLabel = canShowReviewStatus && prDisplay ? getReviewStatusTooltip(prDisplay) diff --git a/tests/e2e/worktree-card-status-indicator.spec.ts b/tests/e2e/worktree-card-status-indicator.spec.ts new file mode 100644 index 000000000..78075cb26 --- /dev/null +++ b/tests/e2e/worktree-card-status-indicator.spec.ts @@ -0,0 +1,84 @@ +// Regression coverage for active status taking precedence over passive identity (#8813). + +import type { Locator, Page } from '@stablyai/playwright-test' +import { test, expect } from './helpers/orca-app' +import { + waitForSessionReady, + waitForActiveWorktree, + getAllWorktreeIds, + ensureTerminalVisible +} from './helpers/store' +import { worktreeRow, worktreeRowSurface } from './worktree-row-locators' + +function statusLane(page: Page, worktreeId: string): Locator { + return worktreeRow(page, worktreeId).locator('[data-worktree-card-status-slot]').first() +} + +function statusDot(page: Page, worktreeId: string): Locator { + return statusLane(page, worktreeId).locator('span.bg-emerald-500').first() +} + +function branchIdentityGlyph(page: Page, worktreeId: string): Locator { + return statusLane(page, worktreeId).locator('svg.lucide-git-branch') +} + +// PTY liveness, not the tab row, drives the activity heuristic. +async function waitForLivePty(page: Page, worktreeId: string): Promise { + await expect + .poll( + async () => + page.evaluate((id) => { + const state = window.__store!.getState() + return (state.tabsByWorktree[id] ?? []).some( + (tab) => (state.ptyIdsByTabId[tab.id] ?? []).length > 0 + ) + }, worktreeId), + { timeout: 30_000, message: `No live PTY attached for worktree ${worktreeId}` } + ) + .toBe(true) +} + +test.describe('Worktree card status indicator', () => { + test.beforeEach(async ({ orcaPage }) => { + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + }) + + test('paints the emerald Active dot instead of the grey branch glyph once a workspace goes live', async ({ + orcaPage + }) => { + const liveWorktreeId = await waitForActiveWorktree(orcaPage) + await ensureTerminalVisible(orcaPage) + await waitForLivePty(orcaPage, liveWorktreeId) + + // The regression requires both the new card style and branch identity. + await orcaPage.evaluate(async () => { + const state = window.__store!.getState() + await state.updateSettings({ experimentalNewWorktreeCardStyle: true }) + state.setWorktreeCardProperties(['status', 'unread', 'branch']) + }) + + const quietWorktreeId = (await getAllWorktreeIds(orcaPage)).find((id) => id !== liveWorktreeId) + if (!quietWorktreeId) { + throw new Error('Seeded repo did not expose a second worktree to keep quiet') + } + + await expect(branchIdentityGlyph(orcaPage, quietWorktreeId)).toBeVisible() + await expect(statusLane(orcaPage, quietWorktreeId)).toHaveText('Branch') + + await expect(statusDot(orcaPage, liveWorktreeId)).toBeVisible() + await expect(statusLane(orcaPage, liveWorktreeId)).toHaveText('Active') + await expect(branchIdentityGlyph(orcaPage, liveWorktreeId)).toHaveCount(0) + + await worktreeRowSurface(orcaPage, quietWorktreeId).click() + await expect + .poll(async () => orcaPage.evaluate(() => window.__store!.getState().activeWorktreeId)) + .toBe(quietWorktreeId) + await ensureTerminalVisible(orcaPage) + await waitForLivePty(orcaPage, quietWorktreeId) + + await expect(statusDot(orcaPage, quietWorktreeId)).toBeVisible() + await expect(statusLane(orcaPage, quietWorktreeId)).toHaveText('Active') + await expect(branchIdentityGlyph(orcaPage, quietWorktreeId)).toHaveCount(0) + }) +})