diff --git a/mobile/src/components/AgentSpinner.tsx b/mobile/src/components/AgentSpinner.tsx
index a5e81ec07a9..c05e5d2a489 100644
--- a/mobile/src/components/AgentSpinner.tsx
+++ b/mobile/src/components/AgentSpinner.tsx
@@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react'
-import { Radio } from 'lucide-react-native'
+import { Activity } from 'lucide-react-native'
import { Animated, Easing, StyleSheet, View } from 'react-native'
import type { AgentWorkingMode } from '../../../src/shared/agent-status-types'
@@ -50,7 +50,7 @@ export function AgentSpinner({
if (monitoring) {
return (
-
+
)
}
diff --git a/mobile/src/components/AgentStateDot.tsx b/mobile/src/components/AgentStateDot.tsx
index 4d596b7e094..1637296d900 100644
--- a/mobile/src/components/AgentStateDot.tsx
+++ b/mobile/src/components/AgentStateDot.tsx
@@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react'
-import { Radio } from 'lucide-react-native'
+import { Activity } from 'lucide-react-native'
import { Animated, Easing, StyleSheet, View } from 'react-native'
import type { AgentDotState } from '../worktree/agent-row-display'
@@ -49,7 +49,7 @@ export function AgentStateDot({ state }: { state: AgentDotState }) {
if (state === 'monitoring') {
return (
-
+
)
}
diff --git a/mobile/src/components/agent-monitoring-indicators.test.ts b/mobile/src/components/agent-monitoring-indicators.test.ts
index 749d2636b98..eefa2137f14 100644
--- a/mobile/src/components/agent-monitoring-indicators.test.ts
+++ b/mobile/src/components/agent-monitoring-indicators.test.ts
@@ -19,7 +19,7 @@ const { animationLoop, animationTiming, setValue } = vi.hoisted(() => ({
setValue: vi.fn()
}))
-vi.mock('lucide-react-native', () => ({ Radio: 'Radio' }))
+vi.mock('lucide-react-native', () => ({ Activity: 'Activity' }))
vi.mock('react-native', () => ({
Animated: {
Value: function Value() {
@@ -48,12 +48,12 @@ describe('mobile monitoring indicators', () => {
renderer = null
})
- it('renders a static Radio for a monitoring agent', async () => {
+ it('renders a static Activity heartbeat for a monitoring agent', async () => {
await act(async () => {
renderer = create(createElement(AgentStateDot, { state: 'monitoring' }))
})
- expect(renderer?.root.findByType('Radio').props).toMatchObject({
+ expect(renderer?.root.findByType('Activity').props).toMatchObject({
color: DESKTOP_WORKING_COLOR,
size: 10
})
@@ -61,14 +61,14 @@ describe('mobile monitoring indicators', () => {
expect(animationLoop).not.toHaveBeenCalled()
})
- it('renders a static Radio for an all-monitoring workspace', async () => {
+ it('renders a static Activity heartbeat for an all-monitoring workspace', async () => {
await act(async () => {
renderer = create(
createElement(AgentSpinner, { status: 'working', workingMode: 'monitoring' })
)
})
- expect(renderer?.root.findByType('Radio').props).toMatchObject({
+ expect(renderer?.root.findByType('Activity').props).toMatchObject({
color: DESKTOP_WORKING_COLOR,
size: 12
})
diff --git a/src/renderer/src/components/AgentStateDot.test.ts b/src/renderer/src/components/AgentStateDot.test.ts
index 6304b1cfd68..c3819152665 100644
--- a/src/renderer/src/components/AgentStateDot.test.ts
+++ b/src/renderer/src/components/AgentStateDot.test.ts
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
-import { AgentStateDot, type AgentDotState } from './AgentStateDot'
+import { AgentStateDot, agentStateLabel, type AgentDotState } from './AgentStateDot'
function renderMarkup(state: AgentDotState): string {
return renderToStaticMarkup(React.createElement(AgentStateDot, { state }))
@@ -42,11 +42,11 @@ describe('AgentStateDot', () => {
expect(markup).toContain('motion-reduce:border-t-yellow-500')
})
- it('renders monitoring as a static yellow radio glyph', () => {
+ it('renders monitoring as a static yellow heartbeat glyph', () => {
const markup = renderMarkup('monitoring')
expect(markup).toContain('aria-label="Monitoring background tasks"')
- expect(markup).toContain('lucide-radio')
+ expect(markup).toContain('lucide-activity')
expect(markup).toContain('text-yellow-500')
expect(markup).not.toContain('data-agent-spinner')
})
@@ -86,4 +86,39 @@ describe('AgentStateDot', () => {
expect(classNames).not.toContain('bg-amber-500')
}
)
+
+ // Why: aria-label renders no hover tooltip, so every glyph read as unlabeled
+ // to sighted users until each branch also emitted a native title (STA-5794).
+ const ALL_STATES = [
+ 'working',
+ 'monitoring',
+ 'blocked',
+ 'waiting',
+ 'interrupted',
+ 'failed',
+ 'done',
+ 'idle',
+ 'permission'
+ ] satisfies AgentDotState[]
+
+ it.each(ALL_STATES)('labels %s with a native hover tooltip', (state) => {
+ const title = renderMarkup(state).match(/]*\stitle="([^"]*)"/)?.[1]
+
+ expect(title).toBe(agentStateLabel(state))
+ })
+
+ // Typecheck-time guard: a new AgentDotState member that ALL_STATES omits
+ // fails `pnpm tc`, so the tooltip case above can never silently skip a state.
+ type UncoveredState = Exclude
+ const _allStatesAreCovered: UncoveredState extends never ? true : never = true
+ void _allStatesAreCovered
+
+ it('lets a caller override the tooltip', () => {
+ const markup = renderToStaticMarkup(
+ React.createElement(AgentStateDot, { state: 'done', title: 'Finished 2m ago' })
+ )
+
+ expect(markup).toContain('title="Finished 2m ago"')
+ expect(markup).toContain('aria-label="Done"')
+ })
})
diff --git a/src/renderer/src/components/AgentStateDot.tsx b/src/renderer/src/components/AgentStateDot.tsx
index 4f361d01f87..5aa9b5027e4 100644
--- a/src/renderer/src/components/AgentStateDot.tsx
+++ b/src/renderer/src/components/AgentStateDot.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { CircleCheck, Radio } from 'lucide-react'
+import { Activity, CircleCheck } from 'lucide-react'
import { cn } from '@/lib/utils'
import { AgentQuestionIcon } from '@/components/AgentQuestionIcon'
import { AgentWorkingSpinner } from '@/components/AgentWorkingSpinner'
@@ -61,23 +61,30 @@ type Props = {
state: AgentDotState
size?: 'sm' | 'md'
className?: string
+ /** Overrides the native hover tooltip; defaults to the state's label. */
+ title?: string
}
/** Render the compact state glyph used by agent rows and terminal tabs. */
export const AgentStateDot = React.memo(function AgentStateDot({
state,
size = 'sm',
- className
+ className,
+ title
}: Props): React.JSX.Element {
const box = size === 'md' ? 'h-3 w-3' : 'h-2.5 w-2.5'
const inner = size === 'md' ? 'size-2' : 'size-1.5'
const icon = size === 'md' ? 'size-3' : 'size-2.5'
+ // Why: aria-label alone never renders a hover tooltip, so these glyphs read as
+ // unlabeled to sighted users (STA-5794). Mirrors StatusIndicator's `title`.
+ const resolvedTitle = title ?? agentStateLabel(state)
if (state === 'working') {
return (
@@ -89,8 +96,9 @@ export const AgentStateDot = React.memo(function AgentStateDot({
-
+
)
}
@@ -104,6 +112,7 @@ export const AgentStateDot = React.memo(function AgentStateDot({
@@ -115,6 +124,7 @@ export const AgentStateDot = React.memo(function AgentStateDot({
@@ -125,6 +135,7 @@ export const AgentStateDot = React.memo(function AgentStateDot({
{
await render()
expect(testContainer.querySelector('[data-spinner]')).toBeNull()
- expect(testContainer.querySelector('.lucide-radio')?.classList).toContain('text-yellow-500')
+ expect(testContainer.querySelector('.lucide-activity')?.classList).toContain('text-yellow-500')
expect(dotLabels()).toEqual(['Monitoring background tasks'])
})
@@ -276,7 +276,7 @@ describe('palette live status', () => {
})
expect(testContainer.querySelector('[data-spinner]')).toBeNull()
- expect(testContainer.querySelector('.lucide-radio')?.classList).toContain('text-yellow-500')
+ expect(testContainer.querySelector('.lucide-activity')?.classList).toContain('text-yellow-500')
expect(dotLabels()).toEqual(['Monitoring background tasks'])
expect(testContainer.querySelector('[title="Monitoring background tasks"]')).not.toBeNull()
})
diff --git a/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx b/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx
index 9a95a6293e8..85a8a4a45e6 100644
--- a/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx
+++ b/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx
@@ -362,7 +362,7 @@ describe('DashboardAgentRow', () => {
)
expect(markup).toContain('Monitoring background tasks')
- expect(markup).toContain('lucide-radio')
+ expect(markup).toContain('lucide-activity')
expect(classTokens(markup)).toContain('text-yellow-500')
expect(markup).not.toContain('data-agent-spinner')
expect(markup).not.toContain('data-agent-row-tool-slot')
diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.tsx
index 6fbde077a9c..fbea6e9edfe 100644
--- a/src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.tsx
+++ b/src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.tsx
@@ -3,7 +3,7 @@ import type React from 'react'
import { Bot, FileJson } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
-import { AgentStateDot, agentStateLabel, type AgentDotState } from '@/components/AgentStateDot'
+import { AgentStateDot, type AgentDotState } from '@/components/AgentStateDot'
import type { AiVaultSession, AiVaultSubagentRunStatus } from '../../../../shared/ai-vault-types'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import { canOpenAiVaultSessionLogInOrca } from './ai-vault-session-path-actions'
@@ -120,7 +120,7 @@ function SubagentSessionLine({ session }: { session: AiVaultSession }): React.JS
{dotState ? (
// Why: a plain inline span would baseline-align the dot; flex keeps it
// vertically centered with the row text.
-
+
) : null}
diff --git a/src/renderer/src/components/sidebar/StatusIndicator.test.ts b/src/renderer/src/components/sidebar/StatusIndicator.test.ts
index c7ebd3a59ec..e5091cead7f 100644
--- a/src/renderer/src/components/sidebar/StatusIndicator.test.ts
+++ b/src/renderer/src/components/sidebar/StatusIndicator.test.ts
@@ -31,11 +31,11 @@ describe('StatusIndicator', () => {
expect(markup).toContain('motion-reduce:border-t-yellow-500')
})
- it('renders monitoring as a static radio glyph', () => {
+ it('renders monitoring as a static heartbeat glyph', () => {
const markup = renderMarkup('monitoring')
expect(markup).toContain('title="Monitoring background tasks"')
- expect(markup).toContain('lucide-radio')
+ expect(markup).toContain('lucide-activity')
expect(markup).toContain('text-yellow-500')
expect(markup).not.toContain('data-agent-spinner')
})
diff --git a/src/renderer/src/components/sidebar/StatusIndicator.tsx b/src/renderer/src/components/sidebar/StatusIndicator.tsx
index 26493bb7d40..af91ab65487 100644
--- a/src/renderer/src/components/sidebar/StatusIndicator.tsx
+++ b/src/renderer/src/components/sidebar/StatusIndicator.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { Radio } from 'lucide-react'
+import { Activity } from 'lucide-react'
import { cn } from '@/lib/utils'
import { AgentQuestionIcon } from '@/components/AgentQuestionIcon'
import { AgentWorkingSpinner } from '@/components/AgentWorkingSpinner'
@@ -48,7 +48,7 @@ const StatusIndicator = React.memo(function StatusIndicator({
title={resolvedTitle}
{...rest}
>
-
+
)
}
diff --git a/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx
index d5591b9e9d5..15927398b85 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])
- expect(iconTitles).toEqual([])
+ // Variety icons stay identity-free; the lone title is the group's state
+ // label, which every state glyph now carries (STA-5794).
+ expect(iconTitles).toEqual(['Working'])
expect(markup).not.toContain('>5 working<')
expect(markup).toContain('>+2<')
})