diff --git a/src/renderer/src/components/AgentStateDot.test.ts b/src/renderer/src/components/AgentStateDot.test.ts
index 40c2bea60..72fbc6223 100644
--- a/src/renderer/src/components/AgentStateDot.test.ts
+++ b/src/renderer/src/components/AgentStateDot.test.ts
@@ -47,12 +47,14 @@ describe('AgentStateDot', () => {
})
it.each(['permission', 'waiting'] satisfies AgentDotState[])(
- 'renders %s as an amber attention dot',
+ 'renders %s as an amber question glyph',
(state) => {
- const classNames = renderDotClassNames(state)
+ const markup = renderMarkup(state)
- expect(classNames).toContain('bg-amber-500')
- expect(classNames).not.toContain('bg-red-500')
+ expect(markup).toContain('lucide-message-circle-question-mark')
+ expect(markup).toContain('text-amber-500')
+ expect(markup).not.toContain('bg-amber-500')
+ expect(markup).not.toContain('data-agent-spinner')
}
)
diff --git a/src/renderer/src/components/AgentStateDot.tsx b/src/renderer/src/components/AgentStateDot.tsx
index a5c50dad1..97d919dca 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 } from 'lucide-react'
+import { CircleCheck, MessageCircleQuestion } from 'lucide-react'
import { cn } from '@/lib/utils'
import { AgentWorkingSpinner } from '@/components/AgentWorkingSpinner'
@@ -95,6 +95,17 @@ export const AgentStateDot = React.memo(function AgentStateDot({
)
}
+ if (state === 'permission' || state === 'waiting') {
+ return (
+
+
+
+ )
+ }
+
return (
diff --git a/src/renderer/src/components/dashboard-popout/AgentKanbanCard.test.tsx b/src/renderer/src/components/dashboard-popout/AgentKanbanCard.test.tsx
index d338a21d3..ebcfee3d2 100644
--- a/src/renderer/src/components/dashboard-popout/AgentKanbanCard.test.tsx
+++ b/src/renderer/src/components/dashboard-popout/AgentKanbanCard.test.tsx
@@ -60,6 +60,30 @@ describe('AgentKanbanCard', () => {
expect(screen.queryByText(/\d+d/)).not.toBeInTheDocument()
})
+ it('shows the question glyph once when a summary is available', () => {
+ const attentionCard = card({
+ bucket: 'attention',
+ dotState: 'waiting',
+ askSummary: 'Approve deploy?'
+ })
+ const { container, rerender } = render(
+
+ )
+
+ expect(screen.queryByTestId('state-dot')).not.toBeInTheDocument()
+ expect(container.querySelectorAll('.lucide-message-circle-question-mark')).toHaveLength(1)
+
+ rerender(
+
+ )
+ expect(screen.getByTestId('state-dot')).toBeInTheDocument()
+ expect(container.querySelector('.lucide-message-circle-question-mark')).toBeNull()
+ })
+
it('skips structured-clone rerenders until visible card data or its age changes', () => {
const onOpenTerminal = vi.fn()
const initial = card({ startedAt: 1_000 })
diff --git a/src/renderer/src/components/dashboard-popout/AgentKanbanCard.tsx b/src/renderer/src/components/dashboard-popout/AgentKanbanCard.tsx
index 2ae818b5d..e92dbcb06 100644
--- a/src/renderer/src/components/dashboard-popout/AgentKanbanCard.tsx
+++ b/src/renderer/src/components/dashboard-popout/AgentKanbanCard.tsx
@@ -98,7 +98,8 @@ export const AgentKanbanCard = memo(
>
{card.worktreeName}
-
+ {/* The summary pill already carries the attention glyph. */}
+ {card.askSummary ? null : }
{card.lastUserMessage || card.lastAgentMessage ? (
diff --git a/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx b/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx
index 931409f13..93238b8fc 100644
--- a/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx
+++ b/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx
@@ -247,12 +247,13 @@ describe('DashboardAgentRow', () => {
expect(classes.every((className) => !/\bgroup-hover:/.test(className))).toBe(true)
})
- it('renders waiting rows with the amber permission color', () => {
+ it('renders waiting rows with the amber question glyph', () => {
const markup = renderRow(makeAgent({}, { state: 'waiting' }))
const tokens = classTokens(markup)
expect(markup).toContain('aria-label="Waiting for input"')
- expect(tokens).toContain('bg-amber-500')
+ expect(markup).toContain('lucide-message-circle-question-mark')
+ expect(tokens).toContain('text-amber-500')
expect(tokens).not.toContain('bg-red-500')
})
diff --git a/src/renderer/src/components/sidebar/SidebarNav.test.tsx b/src/renderer/src/components/sidebar/SidebarNav.test.tsx
index a436942bd..42e7ee20f 100644
--- a/src/renderer/src/components/sidebar/SidebarNav.test.tsx
+++ b/src/renderer/src/components/sidebar/SidebarNav.test.tsx
@@ -20,6 +20,7 @@ const mocks = vi.hoisted(() => ({
refreshPreflightStatus: vi.fn(),
checkLinearConnection: vi.fn(),
hasPairedMobileDevice: false,
+ agentBucketCounts: { attention: 0, working: 0, idle: 0 },
dismissMobileOnboardingBadge: vi.fn(),
setSetupGuideSidebarDismissed: vi.fn()
}))
@@ -39,6 +40,10 @@ vi.mock('@/components/activity/useActivityUnreadCount', () => ({
useActivityUnreadCount: () => 0
}))
+vi.mock('@/components/dashboard/useAgentBucketCounts', () => ({
+ useAgentBucketCounts: () => mocks.agentBucketCounts
+}))
+
vi.mock('@/hooks/useShortcutLabel', () => ({
useShortcutKeyComboDetails: () => [{ keys: ['⌘', 'J'], doubleTap: false }]
}))
@@ -203,6 +208,7 @@ describe('SidebarNav', () => {
vi.clearAllMocks()
await i18n.changeLanguage('en')
mocks.hasPairedMobileDevice = false
+ mocks.agentBucketCounts = { attention: 0, working: 0, idle: 0 }
setSidebarState()
})
@@ -252,6 +258,26 @@ describe('SidebarNav', () => {
expect(queryButtonByText(container, 'Agent Dashboard')).not.toBeNull()
})
+ it('uses a question glyph only for the Needs You count', async () => {
+ mocks.agentBucketCounts = { attention: 2, working: 3, idle: 4 }
+ setSidebarState({
+ settings: {
+ ...getDefaultSettings('/tmp'),
+ experimentalAgentDashboardPopout: true
+ }
+ })
+ const container = await renderSidebarNav()
+
+ const attention = container.querySelector('[aria-label="Needs You: 2"]')
+ const working = container.querySelector('[aria-label="Working: 3"]')
+ const idle = container.querySelector('[aria-label="Idle: 4"]')
+ expect(attention?.querySelector('.lucide-message-circle-question-mark')).not.toBeNull()
+ expect(working?.querySelector('.rounded-full')).not.toBeNull()
+ expect(idle?.querySelector('.rounded-full')).not.toBeNull()
+ expect(working?.querySelector('svg')).toBeNull()
+ expect(idle?.querySelector('svg')).toBeNull()
+ })
+
it('shows the Mobile entry by default for older settings', () => {
expect(shouldShowMobileButton(null)).toBe(true)
expect(shouldShowMobileButton({})).toBe(true)
diff --git a/src/renderer/src/components/sidebar/SidebarNav.tsx b/src/renderer/src/components/sidebar/SidebarNav.tsx
index 0572150a5..ccd04bf73 100644
--- a/src/renderer/src/components/sidebar/SidebarNav.tsx
+++ b/src/renderer/src/components/sidebar/SidebarNav.tsx
@@ -1,5 +1,13 @@
import React from 'react'
-import { Bell, CalendarClock, EyeOff, LayoutDashboard, Search, Smartphone } from 'lucide-react'
+import {
+ Bell,
+ CalendarClock,
+ EyeOff,
+ LayoutDashboard,
+ MessageCircleQuestion,
+ Search,
+ Smartphone
+} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useAppStore } from '@/store'
import { cn } from '@/lib/utils'
@@ -44,10 +52,7 @@ export function shouldShowAutomationsButton(
return settings?.showAutomationsButton !== false
}
-// Per-state dot colors mirror AgentStateDot so the sidebar counts read the same
-// as the board (amber = needs you, yellow = working, neutral = idle, emerald = done).
-const DASHBOARD_BUCKET_DOT_CLASS: Record = {
- attention: 'bg-amber-500',
+const DASHBOARD_BUCKET_DOT_CLASS: Record<'working' | 'idle', string> = {
working: 'bg-yellow-500',
idle: 'bg-neutral-500/50'
}
@@ -80,7 +85,11 @@ function DashboardBucketCounts({
aria-label={`${dashboardBucketLabel(bucket)}: ${counts[bucket]}`}
className="inline-flex items-center gap-1 text-[10px] tabular-nums text-worktree-sidebar-foreground/55"
>
-
+ {bucket === 'attention' ? (
+
+ ) : (
+
+ )}
{counts[bucket]}
))}
diff --git a/src/renderer/src/components/sidebar/StatusIndicator.test.ts b/src/renderer/src/components/sidebar/StatusIndicator.test.ts
index 18ae2d1f1..871687ccb 100644
--- a/src/renderer/src/components/sidebar/StatusIndicator.test.ts
+++ b/src/renderer/src/components/sidebar/StatusIndicator.test.ts
@@ -32,11 +32,13 @@ describe('StatusIndicator', () => {
expect(markup).not.toContain('animation:spin')
})
- it('renders permission as an amber attention dot', () => {
- const classNames = renderDotClassNames('permission')
+ it('renders permission as an amber question glyph', () => {
+ const markup = renderMarkup('permission')
- expect(classNames).toContain('bg-amber-500')
- expect(classNames).not.toContain('bg-red-500')
+ expect(markup).toContain('lucide-message-circle-question-mark')
+ expect(markup).toContain('text-amber-500')
+ expect(markup).not.toContain('bg-amber-500')
+ expect(markup).not.toContain('data-agent-spinner')
})
it('renders active as full emerald dot', () => {
diff --git a/src/renderer/src/components/sidebar/StatusIndicator.tsx b/src/renderer/src/components/sidebar/StatusIndicator.tsx
index ff4c4c25c..7fffc9f97 100644
--- a/src/renderer/src/components/sidebar/StatusIndicator.tsx
+++ b/src/renderer/src/components/sidebar/StatusIndicator.tsx
@@ -1,4 +1,5 @@
import React from 'react'
+import { MessageCircleQuestion } from 'lucide-react'
import { cn } from '@/lib/utils'
import { AgentWorkingSpinner } from '@/components/AgentWorkingSpinner'
import { getWorktreeStatusLabel, type WorktreeStatus } from '@/lib/worktree-status'
@@ -39,6 +40,18 @@ const StatusIndicator = React.memo(function StatusIndicator({
)
}
+ if (status === 'permission') {
+ return (
+
+
+
+ )
+ }
+
return (
diff --git a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx
index d06efb037..8911c1783 100644
--- a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx
+++ b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx
@@ -126,7 +126,8 @@ describe('WorktreeCardStatusSlot', () => {
)
expect(markup).toContain('Needs permission · Unread')
- expect(markup).toContain('bg-amber-500')
+ expect(markup).toContain('lucide-message-circle-question-mark')
+ expect(markup).toContain('text-amber-500')
expect(markup).not.toContain('data-worktree-status-lane-unread=""')
expect(markup).not.toContain('data-worktree-unread-alert=""')
expect(markup).not.toContain('aria-label="Mark as read"')
@@ -378,7 +379,8 @@ describe('WorktreeCardStatusSlot', () => {
)
expect(markup).toContain('Needs permission')
- expect(markup).toContain('bg-amber-500')
+ expect(markup).toContain('lucide-message-circle-question-mark')
+ expect(markup).toContain('text-amber-500')
expect(markup).not.toContain('PR checks: Failed')
})
diff --git a/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.test.tsx b/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.test.tsx
index 58d57669f..6c65ba566 100644
--- a/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.test.tsx
+++ b/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.test.tsx
@@ -36,11 +36,12 @@ describe('TerminalTabLeadingIcon', () => {
expect(markup).toContain('data-agent-icon="codex"')
})
- it('shows a needs-input (permission) state as an amber dot', () => {
+ it('shows a needs-input (permission) state as an amber question glyph', () => {
const markup = renderStatus('permission')
expect(markup).toContain('data-agent-activity-status="permission"')
- expect(markup).toContain('bg-amber-500')
+ expect(markup).toContain('lucide-message-circle-question-mark')
+ expect(markup).toContain('text-amber-500')
expect(markup).not.toContain('bg-red-500')
})