fix(sidebar): preserve meaningful workspace status indicators (#12658)

This commit is contained in:
Neil 2026-08-04 23:58:36 -07:00 committed by GitHub
parent 003114dfad
commit e5f49e0e1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 142 additions and 20 deletions

View File

@ -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', () => {
<WorktreeCard worktree={makeWorktree({ linkedPR: 456 })} repo={makeRepo()} isActive={false} />
)
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)

View File

@ -21,6 +21,7 @@ let tabsByWorktree: Record<string, { id: string }[]> = {}
let ptyIdsByTabId: Record<string, string[]> = {}
let browserTabsByWorktree: Record<string, { id: string }[]> = {}
let settings: Partial<GlobalSettings> | null = null
let activityStatus = 'idle'
let projectGroups: unknown[] = []
let workspaceDeleteModifierPressed = false
let gitConflictOperationByWorktree: Record<string, GitConflictOperation> = {}
@ -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(
<WorktreeCard worktree={makeWorktree()} repo={makeRepo()} isActive={false} />
)
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(
<WorktreeCard

View File

@ -1406,7 +1406,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
onToggleUnread={handleToggleUnreadQuick}
prDisplay={statusLaneReview}
newCardStyle={newCardStyle}
hasBranchIdentity={Boolean(branchIdentityDisplay)}
hasBranchIdentity={hasPathIdentityEnabled && Boolean(branchIdentityDisplay)}
/>
</div>
) : null}

View File

@ -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(
<WorktreeCardStatusSlot
worktreeId="wt-1"
@ -207,14 +207,34 @@ describe('WorktreeCardStatusSlot', () => {
/>
)
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(
<WorktreeCardStatusSlot
worktreeId="wt-1"
showStatus
showUnreadAction={false}
isUnread={false}
unreadTooltip="Mark as unread"
onPointerDown={vi.fn()}
onToggleUnread={vi.fn()}
newCardStyle
hasBranchIdentity
/>
)
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(
<WorktreeCardStatusSlot
worktreeId="wt-1"
@ -236,7 +256,7 @@ describe('WorktreeCardStatusSlot', () => {
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(
<WorktreeCardStatusSlot
@ -252,8 +272,9 @@ describe('WorktreeCardStatusSlot', () => {
/>
)
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(
<WorktreeCardStatusSlot
worktreeId="wt-1"
@ -301,6 +323,7 @@ describe('WorktreeCardStatusSlot', () => {
})
it('uses context-aware branch or folder path tooltip copy', () => {
mocks.status = 'inactive'
const markup = renderToStaticMarkup(
<WorktreeCardStatusSlot
worktreeId="wt-1"
@ -407,6 +430,7 @@ describe('WorktreeCardStatusSlot', () => {
})
it('overlays an unread badge on PR status when new card style is on', () => {
mocks.status = 'inactive'
const markup = renderToStaticMarkup(
<WorktreeCardStatusSlot
worktreeId="wt-1"
@ -436,6 +460,7 @@ describe('WorktreeCardStatusSlot', () => {
})
it('overlays an unread badge on the branch icon in new card style', () => {
mocks.status = 'inactive'
const markup = renderToStaticMarkup(
<WorktreeCardStatusSlot
worktreeId="wt-1"

View File

@ -25,7 +25,8 @@ type WorktreeCardStatusSlotProps = {
className?: string
}
const QUIET_REVIEW_REPLACEABLE_STATUSES = new Set<WorktreeStatus>(['active', 'done', 'inactive'])
// Passive identity only replaces status when the workspace is inactive.
const IDENTITY_REPLACEABLE_STATUSES = new Set<WorktreeStatus>(['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)

View File

@ -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<void> {
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)
})
})