diff --git a/src/renderer/src/components/dashboard-popout/AgentMap.test.tsx b/src/renderer/src/components/dashboard-popout/AgentMap.test.tsx index ecd2910b01c..66b0d8ff32f 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMap.test.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMap.test.tsx @@ -161,7 +161,7 @@ describe('AgentMap', () => { expect(doneNode).toHaveAccessibleName(/unread/) }) - it('glows working agents and their worktree ring only', () => { + it('lets attention override working on the worktree glow', () => { const done = card({ paneKey: 'done', conversationName: 'Finished agent', @@ -171,9 +171,16 @@ describe('AgentMap', () => { dotState: 'done', finishedAt: NOW - 60_000 }) - const { container } = renderMap([card(), done]) + const waiting = card({ + paneKey: 'waiting', + conversationName: 'Question agent', + bucket: 'attention', + dotState: 'waiting' + }) + const { container } = renderMap([card(), waiting, done]) const workingNode = screen.getByRole('button', { name: /Agent alpha/ }) + const waitingNode = screen.getByRole('button', { name: /Question agent/ }) const doneNode = screen.getByRole('button', { name: /Finished agent/ }) const workingRing = screen.getByRole('button', { name: /Open Agent map worktree details/ @@ -183,10 +190,16 @@ describe('AgentMap', () => { }) expect(workingNode.querySelector('[data-agent-map-agent-working-glow]')).toBeInTheDocument() + expect(waitingNode.querySelector('[data-agent-map-agent-working-glow]')).not.toBeInTheDocument() expect(doneNode.querySelector('[data-agent-map-agent-working-glow]')).not.toBeInTheDocument() - expect(workingRing).toHaveClass('is-working') + expect(workingRing).toHaveClass('is-waiting') + expect(workingRing).not.toHaveClass('is-working') expect(doneRing).not.toHaveClass('is-working') - expect(container.querySelectorAll('[data-agent-map-worktree-working-glow]')).toHaveLength(1) + expect(container.querySelector('[data-agent-map-worktree-status-glow]')).toHaveAttribute( + 'data-worktree-active-status', + 'waiting' + ) + expect(container.querySelectorAll('[data-agent-map-worktree-status-glow]')).toHaveLength(1) }) it('keeps glow markup bounded for a large visible worktree', () => { @@ -206,7 +219,7 @@ describe('AgentMap', () => { const { container } = renderMap(cards, { selectedPaneKey: 'pane-0' }) expect(container.querySelectorAll('[data-agent-map-agent-working-glow]')).toHaveLength(60) - expect(container.querySelectorAll('[data-agent-map-worktree-working-glow]')).toHaveLength(1) + expect(container.querySelectorAll('[data-agent-map-worktree-status-glow]')).toHaveLength(1) expect(container.querySelectorAll('filter')).toHaveLength(0) }) diff --git a/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx b/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx index 50c05b5a975..859414990ed 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx @@ -14,6 +14,7 @@ import type { AgentMapWorktreeRing } from './agent-map-layout' import { shouldAggregateAgentMapWorktree } from './agent-map-layout' +import { agentMapWorktreeActiveStatus } from './agent-map-worktree-active-status' type AgentMapWorktreeRingNodeProps = { project: AgentMapProjectRing @@ -187,7 +188,7 @@ export const AgentMapWorktreeRingNode = memo(function AgentMapWorktreeRingNode({ }: AgentMapWorktreeRingNodeProps): React.JSX.Element { const [detailsOpen, setDetailsOpen] = useState(false) const selected = worktree.agents.some((agent) => agent.card.paneKey === selectedPaneKey) - const working = worktree.statusCounts.working > 0 + const activeStatus = agentMapWorktreeActiveStatus(worktree.statusCounts) const aggregate = !selected && shouldAggregateAgentMapWorktree(worktree, zoom, allowAggregation) const agentsByPaneKey = new Map(worktree.agents.map((agent) => [agent.card.paneKey, agent])) @@ -204,10 +205,11 @@ export const AgentMapWorktreeRingNode = memo(function AgentMapWorktreeRingNode({ } }} > - {working ? ( + {activeStatus ? ( { - it('uses one conditional SVG halo per working entity without filter effects', () => { + it('uses one conditional SVG halo per active entity without filter effects', () => { const component = source('AgentMapWorktreeRingNode.tsx') - expect(component.match(/data-agent-map-worktree-working-glow/g)).toHaveLength(1) + expect(component.match(/data-agent-map-worktree-status-glow/g)).toHaveLength(1) expect(component.match(/data-agent-map-agent-working-glow/g)).toHaveLength(1) expect(component).not.toMatch(/ { const css = source('agent-map.css') - const glowRules = css.match(/\.agent-map-(?:worktree|agent)-working-glow\s*\{[^}]+\}/gs) + const glowRules = css.match(/\.agent-map-(?:worktree-status|agent-working)-glow\s*\{[^}]+\}/gs) expect(glowRules).toHaveLength(2) for (const rule of glowRules ?? []) { diff --git a/src/renderer/src/components/dashboard-popout/agent-map-worktree-active-status.test.ts b/src/renderer/src/components/dashboard-popout/agent-map-worktree-active-status.test.ts new file mode 100644 index 00000000000..c02eb68ed27 --- /dev/null +++ b/src/renderer/src/components/dashboard-popout/agent-map-worktree-active-status.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' +import type { AgentMapStatusCounts } from './agent-map-layout' +import { agentMapWorktreeActiveStatus } from './agent-map-worktree-active-status' + +function counts(overrides: Partial = {}): AgentMapStatusCounts { + return { blocked: 0, waiting: 0, working: 0, done: 0, idle: 0, ...overrides } +} + +describe('agentMapWorktreeActiveStatus', () => { + it('prioritizes attention over working', () => { + expect(agentMapWorktreeActiveStatus(counts({ working: 2, waiting: 1 }))).toBe('waiting') + expect(agentMapWorktreeActiveStatus(counts({ working: 2, waiting: 1, blocked: 1 }))).toBe( + 'blocked' + ) + }) + + it('uses working only when no agent needs attention', () => { + expect(agentMapWorktreeActiveStatus(counts({ working: 1, done: 2 }))).toBe('working') + expect(agentMapWorktreeActiveStatus(counts({ done: 2, idle: 1 }))).toBeNull() + }) +}) diff --git a/src/renderer/src/components/dashboard-popout/agent-map-worktree-active-status.ts b/src/renderer/src/components/dashboard-popout/agent-map-worktree-active-status.ts new file mode 100644 index 00000000000..925a80940e5 --- /dev/null +++ b/src/renderer/src/components/dashboard-popout/agent-map-worktree-active-status.ts @@ -0,0 +1,15 @@ +import type { AgentMapStatusCounts } from './agent-map-layout' + +export type AgentMapWorktreeActiveStatus = 'blocked' | 'waiting' | 'working' + +export function agentMapWorktreeActiveStatus( + counts: AgentMapStatusCounts +): AgentMapWorktreeActiveStatus | null { + if (counts.blocked > 0) { + return 'blocked' + } + if (counts.waiting > 0) { + return 'waiting' + } + return counts.working > 0 ? 'working' : null +} diff --git a/src/renderer/src/components/dashboard-popout/agent-map.css b/src/renderer/src/components/dashboard-popout/agent-map.css index 1491516b307..12e2ad578a2 100644 --- a/src/renderer/src/components/dashboard-popout/agent-map.css +++ b/src/renderer/src/components/dashboard-popout/agent-map.css @@ -95,14 +95,25 @@ vector-effect: non-scaling-stroke; } -.agent-map-worktree-working-glow { +.agent-map-worktree-status-glow { fill: none; pointer-events: none; - stroke: color-mix(in srgb, var(--color-yellow-500) 28%, transparent); stroke-width: 9; vector-effect: non-scaling-stroke; } +.agent-map-worktree-status-glow.fleet-status-blocked { + stroke: color-mix(in srgb, var(--color-red-500) 28%, transparent); +} + +.agent-map-worktree-status-glow.fleet-status-waiting { + stroke: color-mix(in srgb, var(--color-amber-500) 28%, transparent); +} + +.agent-map-worktree-status-glow.fleet-status-working { + stroke: color-mix(in srgb, var(--color-yellow-500) 28%, transparent); +} + .agent-map-worktree-ring:hover { fill: color-mix(in srgb, var(--card) 72%, transparent); stroke: var(--ring); @@ -131,6 +142,14 @@ stroke: var(--color-yellow-500); } +.agent-map-worktree-ring.is-waiting { + stroke: var(--color-amber-500); +} + +.agent-map-worktree-ring.is-blocked { + stroke: var(--color-red-500); +} + .agent-map-worktree-label { font-size: 12px; font-weight: 500;