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') })