diff --git a/src/renderer/src/components/AgentStateDot.test.ts b/src/renderer/src/components/AgentStateDot.test.ts index 4b31b5fc8ef..10d2e4a0d08 100644 --- a/src/renderer/src/components/AgentStateDot.test.ts +++ b/src/renderer/src/components/AgentStateDot.test.ts @@ -127,7 +127,11 @@ describe('AgentStateDot', () => { React.createElement(AgentStateDot, { state: 'interrupted', title: null }) ) - expect(markup).not.toContain(' title=') + expect(markup.match(/]*\stitle="([^"]*)"/)?.[1]).toBe(undefined) expect(markup).toContain('aria-label="Interrupted"') + // Differential: the same state still labels itself when the caller omits `title`. + expect(renderMarkup('interrupted').match(/]*\stitle="([^"]*)"/)?.[1]).toBe( + agentStateLabel('interrupted') + ) }) }) diff --git a/src/renderer/src/components/activity/ActivityPrototypePage.thread-grouping.test.ts b/src/renderer/src/components/activity/ActivityPrototypePage.thread-grouping.test.ts index e7cfcd94e57..72a1b6d3355 100644 --- a/src/renderer/src/components/activity/ActivityPrototypePage.thread-grouping.test.ts +++ b/src/renderer/src/components/activity/ActivityPrototypePage.thread-grouping.test.ts @@ -1,11 +1,15 @@ +import { createElement } from 'react' +import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import { formatAgentTypeLabel } from '@/lib/agent-status' +import { TooltipProvider } from '@/components/ui/tooltip' import { buildActivityThreadGroups, buildActivityEvents, buildAgentPaneThreads, - getActivityThreadGroup + getActivityThreadGroup, + ThreadAgentStateIndicator } from './ActivityPrototypePage' import { makeActivityResult, @@ -23,6 +27,28 @@ import { UNKNOWN_PANE_KEY } from './ActivityPrototypePage-test-fixtures' +describe('ThreadAgentStateIndicator', () => { + it('labels the state through its Radix tooltip only', () => { + const threads = makeThreads( + makeActivityResult({ entries: { [PANE_KEY]: makeWorkingEntryWithoutHistory() } }) + ) + + const markup = renderToStaticMarkup( + createElement( + TooltipProvider, + null, + createElement(ThreadAgentStateIndicator, { thread: threads[0]! }) + ) + ) + const titles = [...markup.matchAll(/\stitle="([^"]*)"/g)].map((match) => match[1]) + + expect(markup).toContain('data-slot="tooltip-trigger"') + expect(markup).toContain('aria-label="Working"') + // A native title here would fire alongside the Radix tooltip as a double label. + expect(titles).toEqual([]) + }) +}) + describe('activity thread grouping', () => { it('status grouping separates interrupted done from normal done and keeps Interrupted label', () => { const repo = makeRepo() diff --git a/src/renderer/src/components/activity/ActivityPrototypePage.tsx b/src/renderer/src/components/activity/ActivityPrototypePage.tsx index 4be36a721ec..dcc068d88f8 100644 --- a/src/renderer/src/components/activity/ActivityPrototypePage.tsx +++ b/src/renderer/src/components/activity/ActivityPrototypePage.tsx @@ -1180,7 +1180,11 @@ export function handleActivityFilterFocusShortcut({ return true } -function ThreadAgentStateIndicator({ thread }: { thread: AgentPaneThread }): React.JSX.Element { +export function ThreadAgentStateIndicator({ + thread +}: { + thread: AgentPaneThread +}): React.JSX.Element { const state = threadAgentState(thread) const label = threadAgentStateLabel(thread) return ( diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.test.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.test.tsx index 1aa6ac3a5bb..ae9d9e10750 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.test.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.test.tsx @@ -86,6 +86,22 @@ describe('SessionSubagentsSection', () => { expect(queryByText('First pass')).toBeNull() }) + it('labels the subagent run state exactly once, on the dot itself', async () => { + listSubagentSessions.mockResolvedValueOnce({ + sessions: [makeSubagent('Running task')], + issues: [] + }) + const { container } = render() + await act(async () => {}) + + const titles = [...container.querySelectorAll('[title]')].map((element) => + element.getAttribute('title') + ) + + // A wrapper title would stack a second identical tooltip on the dot's own hit area. + expect(titles).toEqual(['Working', 'Running task', 'View Log']) + }) + it('does not fetch for remote sessions even when the scan counted transcripts', async () => { const { container } = render( match[1]) +} + describe('worktree card agent summary', () => { it('presents passive working as monitoring', () => { const agent = monitoringAgent() @@ -56,6 +60,34 @@ 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 = renderToStaticMarkup( + createElement(CompactAgentRow, { + 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 = renderToStaticMarkup( + createElement(CompactAgentRow, { agent, now: 2000, onActivate: vi.fn() }) + ) + + expect(orderedTitles(eligible)).toEqual([ + 'Monitoring background tasks - Run background checks', + 'Monitoring background tasks', + 'Claude' + ]) + }) + 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..6c37dd13656 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,7 +200,12 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({ ) : reserveDisclosureGutter ? ( ) : null} - + {/* Why: the row's actionable disabled reason must win on every hit area. */} + {!hideIcon && (