]*data-worktree-card-surface="true"[^>]*>/)?.[0]
const triggerTag = markup.match(/
]*data-worktree-card-hover-trigger=""[^>]*>/)?.[0]
@@ -236,7 +236,7 @@ describe('WorktreeCard compact hover details', () => {
)
expect(markup).toContain('data-worktree-title-inline-rename=""')
- expectParentBodyIsHoverTrigger(markup)
+ expectIdentityBodyIsHoverTrigger(markup)
expect(markup).toContain('data-hover-open-delay="100"')
expect(markup).toContain('PR #456')
expect(markup).toContain('Fix stale GH PR')
@@ -288,7 +288,7 @@ describe('WorktreeCard compact hover details', () => {
)
expect(markup).toContain('data-hover-open-delay="100"')
- expectParentBodyIsHoverTrigger(markup)
+ expectIdentityBodyIsHoverTrigger(markup)
expect(markup).toContain('Issue #123')
expect(markup).toContain('Linear ENG-123')
expect(markup).toContain('Reviewer handoff note')
@@ -397,7 +397,7 @@ describe('WorktreeCard compact hover details', () => {
expect(markup).toContain('Human title')
})
- it('uses one whole-card hover even when detailed metadata icons are visible when new card style is on', async () => {
+ it('uses one identity hover even when detailed metadata icons are visible when new card style is on', async () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
worktreeCardProperties = ['status', 'issue', 'linear-issue', 'comment', 'ports']
const { default: WorktreeCard } = await import('./WorktreeCard')
@@ -417,12 +417,12 @@ describe('WorktreeCard compact hover details', () => {
expect(markup).toContain('Workspace metadata')
expect(markup).not.toContain('data-worktree-card-meta-row=""')
- expectParentBodyIsHoverTrigger(markup)
+ expectIdentityBodyIsHoverTrigger(markup)
expect(markup.match(/data-hover-open-delay="100"/g)).toHaveLength(1)
expect(markup).toContain('Reviewer handoff note')
})
- it('keeps long workspace and branch identity in whole-card hover details when the branch row is hidden', async () => {
+ it('keeps long workspace and branch identity in hover details when the branch row is hidden', async () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
worktreeCardProperties = ['status', 'comment']
const { default: WorktreeCard } = await import('./WorktreeCard')
@@ -440,13 +440,13 @@ describe('WorktreeCard compact hover details', () => {
)
expect(markup).not.toContain('data-worktree-card-meta-row=""')
- expectParentBodyIsHoverTrigger(markup)
+ expectIdentityBodyIsHoverTrigger(markup)
expect(markup).toContain('[Bug]: Hold-to-talk speech-to-text option no longer works')
expect(markup).toContain('bug-hold-to-talk-speech-to-text-option-no-longer-works')
expect(markup).toContain('Reviewer handoff note')
})
- it('repeats a long workspace title inside the whole-card hover when branch is already visible', async () => {
+ it('repeats a long workspace title inside the identity hover when branch is already visible', async () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
worktreeCardProperties = ['status', 'branch', 'comment']
const longTitle =
@@ -461,13 +461,13 @@ describe('WorktreeCard compact hover details', () => {
/>
)
- expectParentBodyIsHoverTrigger(markup)
+ expectIdentityBodyIsHoverTrigger(markup)
expect(markup.match(new RegExp(longTitle, 'g'))).toHaveLength(2)
expect(markup).toContain('feature/local-branch')
expect(markup).toContain('Reviewer handoff note')
})
- it('uses whole-card hover for identity-only new card worktrees with branch row visible', async () => {
+ it('uses identity hover for identity-only new card worktrees with branch row visible', async () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
worktreeCardProperties = ['status', 'branch']
const { default: WorktreeCard } = await import('./WorktreeCard')
@@ -481,7 +481,7 @@ describe('WorktreeCard compact hover details', () => {
)
expect(markup).toContain('data-worktree-card-meta-row=""')
- expectParentBodyIsHoverTrigger(markup)
+ expectIdentityBodyIsHoverTrigger(markup)
expect(markup.match(/data-hover-open-delay="100"/g)).toHaveLength(1)
expect(markup.match(/Readable identity only/g)).toHaveLength(2)
expect(markup).toContain('feature/local-branch')
@@ -502,7 +502,7 @@ describe('WorktreeCard compact hover details', () => {
/>
)
- expectParentBodyIsHoverTrigger(markup)
+ expectIdentityBodyIsHoverTrigger(markup)
expect(markup.match(/feature\/local-branch/g)).toHaveLength(3)
})
@@ -605,6 +605,28 @@ describe('WorktreeCard compact hover details', () => {
)
})
+ it('keeps status and agent tooltip targets outside the worktree details hover trigger', async () => {
+ settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
+ worktreeCardProperties = ['status', 'inline-agents']
+ agentActivityDisplayMode = 'compact'
+ mockInlineAgentRows = [{} as DashboardAgentRowData]
+ const { default: WorktreeCard } = await import('./WorktreeCard')
+
+ const markup = renderToStaticMarkup(
+
+ )
+ const statusIndex = markup.indexOf('data-worktree-card-status-slot=""')
+ const triggerIndex = markup.indexOf('data-worktree-card-hover-trigger=""')
+ const hoverContentIndex = markup.indexOf('data-hover-card-content=""')
+ const agentsIndex = markup.indexOf('data-worktree-agents=""')
+
+ expectIdentityBodyIsHoverTrigger(markup)
+ expect(statusIndex).toBeGreaterThanOrEqual(0)
+ expect(statusIndex).toBeLessThan(triggerIndex)
+ expect(hoverContentIndex).toBeGreaterThan(triggerIndex)
+ expect(agentsIndex).toBeGreaterThan(hoverContentIndex)
+ })
+
it('preserves the aggregate cache timer when compact inline agents are enabled but absent', async () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
worktreeCardProperties = ['status', 'inline-agents']
@@ -643,7 +665,7 @@ describe('WorktreeCard compact hover details', () => {
const hoverContentIndex = markup.indexOf('data-hover-card-content=""')
const childIndex = markup.indexOf('data-lineage-child-card=""')
- expectParentBodyIsHoverTrigger(markup)
+ expectIdentityBodyIsHoverTrigger(markup)
expect(markup).toContain('data-worktree-lineage-children=""')
expect(markup).toContain('group/worktree-card')
expect(markup).not.toContain('group relative flex cursor-pointer')
diff --git a/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx
index 90ac7cdf38c..5af1a13a884 100644
--- a/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx
+++ b/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx
@@ -5,6 +5,7 @@ import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
+import { TooltipProvider } from '@/components/ui/tooltip'
import type * as ActivateTabAndFocusPaneModule from '@/lib/activate-tab-and-focus-pane'
import { makePaneKey } from '../../../../shared/stable-pane-id'
@@ -395,7 +396,11 @@ describe('WorktreeCardAgents activation', () => {
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
await act(async () => {
- root.render(
)
+ root.render(
+
+
+
+ )
})
const row = host.querySelector('.compact-agent-row')
expect(row).toBeInstanceOf(HTMLElement)
diff --git a/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx
index d5591b9e9d5..4bf3a8dcd17 100644
--- a/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx
+++ b/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx
@@ -771,7 +771,9 @@ describe('WorktreeCardAgents', () => {
const markup = renderToStaticMarkup(
)
const iconTitles = [...markup.matchAll(/title="([^"]+)"/g)].map((match) => match[1])
+ // Variety icons stay identity-free; the state label belongs to the shared tooltip.
expect(iconTitles).toEqual([])
+ expect(markup).toContain('>Working<')
expect(markup).not.toContain('>5 working<')
expect(markup).toContain('>+2<')
})
diff --git a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx
index b9a32ddeb8f..9baed34e995 100644
--- a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx
+++ b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx
@@ -9,8 +9,10 @@ const mocks = vi.hoisted(() => ({
}))
vi.mock('@/components/ui/tooltip', () => ({
- Tooltip: ({ children }: { children: ReactNode }) => <>{children}>,
- TooltipContent: ({ children }: { children: ReactNode }) => <>{children}>,
+ Tooltip: ({ children }: { children: ReactNode }) =>
{children},
+ TooltipContent: ({ children }: { children: ReactNode }) => (
+
{children}
+ ),
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}>
}))
@@ -171,6 +173,7 @@ describe('WorktreeCardStatusSlot', () => {
expect(markup).toContain('Active · Mark as unread')
expect(markup).toContain('bg-emerald-500')
+ expect(markup.match(/data-tooltip-root/g)).toHaveLength(1)
})
it('keeps the quiet active dot ahead of PR status by default', () => {
@@ -212,6 +215,7 @@ describe('WorktreeCardStatusSlot', () => {
expect(markup).toContain('size-[13px] translate-x-px')
expect(markup).toContain('text-rose-500/85')
expect(markup).not.toContain('bg-emerald-500')
+ expect(markup).not.toContain('data-tooltip-root')
})
it('uses the unified compact review glyph for GitLab MR status', () => {
@@ -277,7 +281,7 @@ 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 accessible copy by default', () => {
const markup = renderToStaticMarkup(
{
expect(markup).toContain('size-[13px] translate-x-px text-muted-foreground/70')
expect(markup).toContain('text-muted-foreground/70')
expect(markup).not.toContain('bg-emerald-500')
+ expect(markup).not.toContain('data-tooltip-root')
})
- it('uses context-aware branch or folder path tooltip copy', () => {
+ it('uses context-aware branch or folder path accessible copy', () => {
const markup = renderToStaticMarkup(
{
expect(markup).toContain('Branch or folder path')
expect(markup).toContain('lucide-git-branch')
+ expect(markup).not.toContain('data-tooltip-root')
})
it('keeps the quiet dot when the row has no branch identity', () => {
@@ -338,6 +344,7 @@ describe('WorktreeCardStatusSlot', () => {
expect(markup).toContain('Active')
expect(markup).toContain('bg-emerald-500')
expect(markup).not.toContain('lucide-git-branch')
+ expect(markup).not.toContain('data-tooltip-root')
})
it('keeps working activity ahead of PR status in new card style', () => {
@@ -359,6 +366,8 @@ describe('WorktreeCardStatusSlot', () => {
expect(markup).toContain('Working')
expect(markup).toContain('inline-flex size-5 items-center justify-center')
expect(markup).toContain('border-yellow-500')
+ expect(markup).toContain('data-tooltip-root')
+ expect(markup).toContain('data-tooltip-content="">Working')
expect(markup).not.toContain('PR checks: Failed')
})
@@ -381,6 +390,8 @@ describe('WorktreeCardStatusSlot', () => {
expect(markup).toContain('Needs permission')
expect(markup).toContain('lucide-message-circle-question-mark')
expect(markup).toContain('text-agent-question')
+ expect(markup).toContain('data-tooltip-root')
+ expect(markup).toContain('data-tooltip-content="">Needs permission')
expect(markup).not.toContain('PR checks: Failed')
})
@@ -433,6 +444,7 @@ describe('WorktreeCardStatusSlot', () => {
expect(markup).not.toContain('lucide-bell')
expect(markup).not.toContain('text-amber-500')
expect(markup).not.toContain('bg-emerald-500')
+ expect(markup).not.toContain('data-tooltip-root')
})
it('overlays an unread badge on the branch icon in new card style', () => {
@@ -461,5 +473,6 @@ describe('WorktreeCardStatusSlot', () => {
expect(markup).not.toContain('lucide-bell')
expect(markup).not.toContain('text-amber-500')
expect(markup).not.toContain('bg-emerald-500')
+ expect(markup).not.toContain('data-tooltip-root')
})
})
diff --git a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx
index 9f8dc175652..72b04b22b1f 100644
--- a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx
+++ b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx
@@ -63,7 +63,7 @@ function overlayNewCardUnreadStatus(
)
}
-function getReviewStatusTooltip(review: WorktreeCardPrDisplay): string {
+function getReviewStatusLabel(review: WorktreeCardPrDisplay): string {
const label = getReviewLabel(review)
if (review.state === 'merged') {
return `${label}: Merged`
@@ -115,57 +115,44 @@ export function WorktreeCardStatusSlot({
QUIET_REVIEW_REPLACEABLE_STATUSES.has(status)
const passiveStatusLabel =
canShowReviewStatus && prDisplay
- ? getReviewStatusTooltip(prDisplay)
+ ? getReviewStatusLabel(prDisplay)
: canShowBranchStatus
? (branchIdentityLabel ?? getDefaultBranchIdentityLabel())
: statusLabel
- const passiveStatusTooltip =
+ const passiveStatusAnnouncement =
newCardStyle && isUnread ? `${passiveStatusLabel} · Unread` : passiveStatusLabel
// Why: working and permission already own the new-card status lane, but
- // unread state should still surface in tooltip/sr-only copy and reappear afterward.
+ // unread state should still surface to assistive technology and reappear afterward.
const showNewCardUnreadAlert =
newCardStyle && isUnread && showStatus && status !== 'working' && status !== 'permission'
const reviewStatusIconClassName = compactReviewAndBranchStatusIconClassName
const branchStatusIcon =
const passiveStatus =
canShowReviewStatus && prDisplay ? (
-
-
-
-
- {passiveStatusTooltip}
-
-
-
- {passiveStatusTooltip}
-
-
+
+
+ {passiveStatusAnnouncement}
+
) : canShowBranchStatus ? (
-
-
-
- {branchStatusIcon}
- {passiveStatusTooltip}
-
-
-
- {passiveStatusTooltip}
-
-
+
+ {branchStatusIcon}
+ {passiveStatusAnnouncement}
+
) : newCardStyle && showStatus ? (
<>
-
+
- {passiveStatusTooltip}
+ {passiveStatusAnnouncement}
>
) : (
<>
-
+
{statusLabel}
>
)
@@ -216,7 +203,7 @@ export function WorktreeCardStatusSlot({
{branchStatusIcon}
) : showStatus ? (
-
+
) : (
{actionLabel}
)
@@ -227,6 +214,7 @@ export function WorktreeCardStatusSlot({
diff --git a/src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts b/src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts
index 87096cf9532..f6dc1aa335d 100644
--- a/src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts
+++ b/src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts
@@ -1,6 +1,7 @@
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
+import { TooltipProvider } from '@/components/ui/tooltip'
import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
import { CompactAgentRow, getCompactAgentSecondary } from './worktree-card-compact-agent-row'
import { getAgentDotState, summarizeAgents } from './worktree-card-agent-summary'
@@ -33,6 +34,16 @@ function monitoringAgent(): DashboardAgentRowData {
}
}
+function orderedTitles(markup: string): string[] {
+ return [...markup.matchAll(/\stitle="([^"]*)"/g)].map((match) => match[1])
+}
+
+function renderCompactAgentRow(props: React.ComponentProps): string {
+ return renderToStaticMarkup(
+ createElement(TooltipProvider, null, createElement(CompactAgentRow, props))
+ )
+}
+
describe('worktree card agent summary', () => {
it('presents passive working as monitoring', () => {
const agent = monitoringAgent()
@@ -46,9 +57,7 @@ describe('worktree card agent summary', () => {
const agent = monitoringAgent()
agent.entry.prompt = 'Run background checks'
- const markup = renderToStaticMarkup(
- createElement(CompactAgentRow, { agent, now: 2000, onActivate: vi.fn() })
- )
+ const markup = renderCompactAgentRow({ agent, now: 2000, onActivate: vi.fn() })
expect(markup).toContain('title="Monitoring background tasks - Run background checks"')
expect(markup).toMatch(
@@ -56,6 +65,30 @@ describe('worktree card agent summary', () => {
)
})
+ it('hands the whole row to the send-target reason, and only then', () => {
+ const agent = monitoringAgent()
+ agent.entry.prompt = 'Run background checks'
+
+ const disabled = renderCompactAgentRow({
+ agent,
+ now: 2000,
+ onActivate: vi.fn(),
+ sendTargetStatus: 'disabled',
+ sendTargetDisabledReason: 'Agent needs permission'
+ })
+
+ // The dot sits inside the row, so its own state title would shadow the reason on hover.
+ expect(orderedTitles(disabled)).toEqual(['Agent needs permission', 'Claude'])
+
+ const eligible = renderCompactAgentRow({ agent, now: 2000, onActivate: vi.fn() })
+
+ expect(orderedTitles(eligible)).toEqual([
+ 'Claude',
+ 'Monitoring background tasks - Run background checks'
+ ])
+ expect(eligible).toContain('data-slot="tooltip-trigger"')
+ })
+
it('lists interrupted outcomes before clean completions', () => {
const done = monitoringAgent()
done.state = 'done'
diff --git a/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx b/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx
index 63163bfe207..36f865c87a3 100644
--- a/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx
+++ b/src/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsx
@@ -200,13 +200,22 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
) : reserveDisclosureGutter ? (
) : null}
-
+ {/* Why: the row's actionable disabled reason must win on every hit area. */}
+
{!hideIcon && (
)}
-
+
{/* Why: the selected-row fill is strong enough to wash out the dimmed
prompt/secondary text, so lift both toward full foreground when focused. */}
@@ -278,7 +287,7 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
role={agent.lineage ? 'treeitem' : undefined}
aria-level={agent.lineage ? agent.lineage.depth + 1 : undefined}
aria-expanded={hasChildDisclosure ? childAgentsExpanded : undefined}
- title={sendTargetDisabledReason ?? rowTitle}
+ title={sendTargetDisabledReason}
>
{rowBody}
diff --git a/src/renderer/src/components/sidebar/worktree-card-compact-agents.tsx b/src/renderer/src/components/sidebar/worktree-card-compact-agents.tsx
index 8eb6dc8a594..9787707bace 100644
--- a/src/renderer/src/components/sidebar/worktree-card-compact-agents.tsx
+++ b/src/renderer/src/components/sidebar/worktree-card-compact-agents.tsx
@@ -150,7 +150,7 @@ export function CompactAgentSummaryButton({
key={group.state}
className="inline-flex min-w-0 shrink-0 items-center gap-0.5 rounded-sm bg-worktree-sidebar/70 px-1 py-0.5"
>
-
{/* Why: same-state agent identities read as one status cluster;
overlapping them saves width without merging different states. */}