fix(agent-map): prioritize attention ring status

This commit is contained in:
Brennan Benson
2026-08-05 20:29:36 -07:00
parent 3fdf36abed
commit 2b4c68c954
6 changed files with 85 additions and 15 deletions
@@ -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)
})
@@ -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 ? (
<circle
className="agent-map-worktree-working-glow"
data-agent-map-worktree-working-glow=""
className={`agent-map-worktree-status-glow fleet-status-${activeStatus}`}
data-agent-map-worktree-status-glow=""
data-worktree-active-status={activeStatus}
cx={worktree.x}
cy={worktree.y}
r={worktree.radius}
@@ -216,7 +218,7 @@ export const AgentMapWorktreeRingNode = memo(function AgentMapWorktreeRingNode({
) : null}
<PopoverTrigger asChild>
<circle
className={`agent-map-worktree-ring${working ? ' is-working' : ''}${selected ? ' is-selected' : ''}${detailsOpen ? ' is-open' : ''}`}
className={`agent-map-worktree-ring${activeStatus ? ` is-${activeStatus}` : ''}${selected ? ' is-selected' : ''}${detailsOpen ? ' is-open' : ''}`}
data-agent-map-worktree=""
data-agent-count={worktree.agents.length}
cx={worktree.x}
@@ -10,17 +10,17 @@ function source(file: string): string {
}
describe('Agent Map glow performance boundary', () => {
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(/<filter|filter=/)
})
it('keeps glow styling free of animated and filtered paint work', () => {
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 ?? []) {
@@ -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> = {}): 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()
})
})
@@ -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
}
@@ -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;