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)
+ })
+})