Show branch cues in experimental worktree cards (#5737)

This commit is contained in:
Brennan Benson 2026-06-18 16:17:28 -07:00 committed by GitHub
parent afde3a3741
commit fe8b60b11c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 102 additions and 10 deletions

View File

@ -1209,6 +1209,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
onToggleUnread={handleToggleUnreadQuick}
prDisplay={hoverReview}
newCardStyle={newCardStyle}
hasBranchIdentity={!isFolder && branch.length > 0}
/>
</div>
) : null}

View File

@ -148,6 +148,46 @@ describe('WorktreeCardStatusSlot', () => {
expect(markup).not.toContain('bg-neutral-500/40')
})
it('uses a branch icon instead of the quiet active dot when new card style has no review', () => {
const markup = renderToStaticMarkup(
<WorktreeCardStatusSlot
worktreeId="wt-1"
showStatus
showUnreadAction={false}
isUnread={false}
unreadTooltip="Mark as unread"
onPointerDown={vi.fn()}
onToggleUnread={vi.fn()}
newCardStyle
/>
)
expect(markup).toContain('Branch')
expect(markup).toContain('lucide-git-branch')
expect(markup).toContain('text-muted-foreground/70')
expect(markup).not.toContain('bg-emerald-500')
})
it('keeps the quiet dot when the row has no branch identity', () => {
const markup = renderToStaticMarkup(
<WorktreeCardStatusSlot
worktreeId="wt-1"
showStatus
showUnreadAction={false}
isUnread={false}
unreadTooltip="Mark as unread"
onPointerDown={vi.fn()}
onToggleUnread={vi.fn()}
newCardStyle
hasBranchIdentity={false}
/>
)
expect(markup).toContain('Active')
expect(markup).toContain('bg-emerald-500')
expect(markup).not.toContain('lucide-git-branch')
})
it('keeps working activity ahead of PR status in new card style', () => {
mocks.status = 'working'
const markup = renderToStaticMarkup(
@ -237,23 +277,26 @@ describe('WorktreeCardStatusSlot', () => {
expect(markup).not.toContain('bg-emerald-500')
})
it('keeps the new style unread status dot in the stable lane footprint', () => {
it('overlays unread on the no-review branch icon in new card style', () => {
const markup = renderToStaticMarkup(
<WorktreeCardStatusSlot
worktreeId="wt-1"
showStatus
showUnreadAction
isUnread={false}
unreadTooltip="Mark as unread"
isUnread
unreadTooltip="Mark as read"
onPointerDown={vi.fn()}
onToggleUnread={vi.fn()}
newCardStyle
/>
)
expect(markup).toContain('Active · Mark as unread')
expect(markup).toContain('Branch · Mark as read')
expect(markup).toContain(
'group/unread relative flex cursor-pointer items-center justify-center rounded transition-all size-5'
)
expect(markup).toContain('lucide-git-branch')
expect(markup).toContain('absolute -right-1 -top-1 size-[13px] text-amber-500')
expect(markup).not.toContain('bg-emerald-500')
})
})

View File

@ -1,5 +1,5 @@
import React from 'react'
import { Bell } from 'lucide-react'
import { Bell, GitBranch } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { getWorktreeStatusLabel, type WorktreeStatus } from '@/lib/worktree-status'
@ -19,10 +19,15 @@ type WorktreeCardStatusSlotProps = {
onPointerDown: React.PointerEventHandler<HTMLButtonElement>
prDisplay?: WorktreeCardPrDisplay | null
newCardStyle?: boolean
hasBranchIdentity?: boolean
className?: string
}
const QUIET_REVIEW_REPLACEABLE_STATUSES = new Set<WorktreeStatus>(['active', 'done', 'inactive'])
// Why: a missing review display can also mean provider state is unavailable,
// so the passive label names the branch cue without claiming no review exists.
const BRANCH_STATUS_LABEL = 'Branch'
const branchStatusIconClassName = 'size-4 text-muted-foreground/70'
function getReviewStatusTooltip(review: WorktreeCardPrDisplay): string {
const label = getReviewLabel(review)
@ -57,6 +62,7 @@ export function WorktreeCardStatusSlot({
onPointerDown,
prDisplay = null,
newCardStyle = false,
hasBranchIdentity = true,
className
}: WorktreeCardStatusSlotProps): React.JSX.Element | null {
const status = useWorktreeActivityStatus(worktreeId)
@ -66,9 +72,20 @@ export function WorktreeCardStatusSlot({
showStatus &&
prDisplay !== null &&
QUIET_REVIEW_REPLACEABLE_STATUSES.has(status)
const canShowBranchStatus =
newCardStyle &&
showStatus &&
hasBranchIdentity &&
prDisplay === null &&
QUIET_REVIEW_REPLACEABLE_STATUSES.has(status)
const passiveStatusLabel =
canShowReviewStatus && prDisplay ? getReviewStatusTooltip(prDisplay) : statusLabel
canShowReviewStatus && prDisplay
? getReviewStatusTooltip(prDisplay)
: canShowBranchStatus
? BRANCH_STATUS_LABEL
: statusLabel
const reviewStatusIconClassName = 'size-4'
const branchStatusIcon = <GitBranch className={branchStatusIconClassName} aria-hidden="true" />
const passiveStatus =
canShowReviewStatus && prDisplay ? (
<Tooltip>
@ -82,6 +99,18 @@ export function WorktreeCardStatusSlot({
<span>{passiveStatusLabel}</span>
</TooltipContent>
</Tooltip>
) : canShowBranchStatus ? (
<Tooltip>
<TooltipTrigger asChild>
<span className={cn('inline-flex size-5 items-center justify-center p-0.5', className)}>
{branchStatusIcon}
<span className="sr-only">{passiveStatusLabel}</span>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
<span>{passiveStatusLabel}</span>
</TooltipContent>
</Tooltip>
) : newCardStyle && showStatus ? (
<>
<span className={cn('inline-flex size-5 items-center justify-center', className)}>
@ -106,7 +135,7 @@ export function WorktreeCardStatusSlot({
const actionLabel = isUnread ? 'Mark as read' : 'Mark as unread'
const tooltip =
showStatus && (!isUnread || (newCardStyle && canShowReviewStatus && prDisplay))
showStatus && (!isUnread || (newCardStyle && (canShowBranchStatus || canShowReviewStatus)))
? `${passiveStatusLabel} · ${unreadTooltip}`
: unreadTooltip
@ -135,6 +164,13 @@ export function WorktreeCardStatusSlot({
</span>
<FilledBellIcon className="absolute -right-1 -top-1 size-[13px] text-amber-500 drop-shadow-sm" />
</>
) : isUnread && showStatus && canShowBranchStatus ? (
<>
<span className="inline-flex size-5 items-center justify-center p-0.5">
{branchStatusIcon}
</span>
<FilledBellIcon className="absolute -right-1 -top-1 size-[13px] text-amber-500 drop-shadow-sm" />
</>
) : isUnread ? (
<FilledBellIcon className="size-[13px] text-amber-500 drop-shadow-sm" />
) : showStatus && canShowReviewStatus && prDisplay ? (
@ -144,6 +180,13 @@ export function WorktreeCardStatusSlot({
</span>
<Bell className="absolute size-3 text-muted-foreground/40 opacity-0 transition-opacity group-hover/unread:opacity-100 group-focus-within/unread:opacity-100" />
</>
) : showStatus && canShowBranchStatus ? (
<>
<span className="inline-flex size-5 items-center justify-center p-0.5 transition-opacity group-hover/unread:opacity-0 group-focus-within/unread:opacity-0">
{branchStatusIcon}
</span>
<Bell className="absolute size-3 text-muted-foreground/40 opacity-0 transition-opacity group-hover/unread:opacity-100 group-focus-within/unread:opacity-100" />
</>
) : showStatus ? (
<>
<StatusIndicator

View File

@ -3667,6 +3667,7 @@
"1a0eec0d35": "Status",
"b5536d5a88": "Tasks",
"8d62c68b35": "Notes",
"automation": "Automation",
"2d74665a56": "Ports",
"65a9820bd1": "Agent statuses",
"219ebf1961": "Branch name"

View File

@ -3653,6 +3653,7 @@
"1a0eec0d35": "Estado",
"b5536d5a88": "Tareas",
"8d62c68b35": "Notas",
"automation": "Automation",
"2d74665a56": "Puertos",
"65a9820bd1": "Estados del agente",
"219ebf1961": "Nombre de rama"

View File

@ -3636,7 +3636,8 @@
"8d62c68b35": "ノート",
"2d74665a56": "ポート",
"65a9820bd1": "Agent ステータス",
"219ebf1961": "ブランチ名"
"219ebf1961": "ブランチ名",
"automation": "Automation"
},
"SshDisconnectedDialog": {
"ca4a7892af": "接続中...",

View File

@ -3636,7 +3636,8 @@
"8d62c68b35": "메모",
"2d74665a56": "포트",
"65a9820bd1": "Agent 상태",
"219ebf1961": "브랜치 이름"
"219ebf1961": "브랜치 이름",
"automation": "Automation"
},
"SshDisconnectedDialog": {
"ca4a7892af": "연결 중...",

View File

@ -3636,7 +3636,8 @@
"8d62c68b35": "笔记",
"2d74665a56": "端口",
"65a9820bd1": "Agent 状态",
"219ebf1961": "分支名称"
"219ebf1961": "分支名称",
"automation": "Automation"
},
"SshDisconnectedDialog": {
"ca4a7892af": "正在连接...",