fix(agent-map): draw every visible orchestration edge, and let the filter hide them

The agent map dropped real parent -> child dispatch edges and mislabeled the
ones it kept.

- Remove the `child.y <= parent.y` gate on agent lineage. Both endpoints are
  drawn nodes, so the relationship is real whatever the layout ranked them;
  the gate silently hid edges that packing pressure placed side by side.
  `lineagePath` is now direction-aware so an upward edge does not exit the
  wrong side and draw back through both nodes.

- Fix the relation attribute. `parent.card.parentPaneKey ? 'subagent' : ...`
  asked whether the PARENT has a parent and said nothing about the child, so
  a 3-deep chain rendered solid as if it were an in-process subagent. Every
  map node is a top-level pane agent -- build-dashboard-snapshot folds
  subagent rows into the parent card's roster and never makes them cards --
  so every card-to-card edge is an orchestration edge. Dead CSS removed.

- Add a "Orchestration links" toggle under Filter > Map content, default on,
  hiding both same-worktree and cross-worktree edges while keeping the nodes.

Lineage still comes from the existing active-or-recently-settled dispatch
context; making it historical would be a main-process change and is out of
scope here by design.

Extractions are max-lines pressure, not drive-by refactors: the toolbar hit
404/400 and AgentMapCanvas 403/400, and the repo forbids new max-lines
suppressions.
This commit is contained in:
Brennan Benson
2026-08-07 13:32:41 -07:00
parent b9e9811924
commit ecc8cdc4ac
14 changed files with 257 additions and 105 deletions
@@ -62,6 +62,7 @@ export function AgentDashboardMapView({
}: AgentDashboardMapViewProps): React.JSX.Element {
const { agentStates, toggleAgentState, resetAgentStates } = useAgentMapStateFilter()
const [showAgentlessWorkspaces, setShowAgentlessWorkspaces] = useState(false)
const [showOrchestrationLinks, setShowOrchestrationLinks] = useState(true)
const agentlessWorkspaces = useMemo(
() =>
selectAgentlessMapWorkspaces({
@@ -94,6 +95,8 @@ export function AgentDashboardMapView({
showAgentlessWorkspaces={showAgentlessWorkspaces}
agentlessWorkspaceCount={agentlessWorkspaces.length}
onShowAgentlessWorkspacesChange={setShowAgentlessWorkspaces}
showOrchestrationLinks={showOrchestrationLinks}
onShowOrchestrationLinksChange={setShowOrchestrationLinks}
searchInputRef={searchInputRef}
/>
<div className={cn('flex min-h-0 flex-1', dialogCard && 'flex-row-reverse')}>
@@ -107,6 +110,7 @@ export function AgentDashboardMapView({
compact={dialogCard !== null}
selectedPaneKey={dialogCard?.paneKey}
enabledStates={agentStates}
showOrchestrationLinks={showOrchestrationLinks}
launchableAgentsByWorktreeId={snapshot.launchableAgentsByWorktreeId}
workspaceContextMenusEnabled={workspaceContextMenusEnabled}
onWorkspaceContextMenuOpenChange={onWorkspaceContextMenuOpenChange}
@@ -27,6 +27,8 @@ import {
} from './agent-board-filtering'
import { countAgentMapCards, type AgentMapState } from './agent-map-filter'
import { AgentDashboardFilterChips } from './AgentDashboardFilterChips'
import { AgentMapContentFilterItems } from './AgentMapContentFilterItems'
import { FilterOptionCount } from './FilterOptionCount'
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
type FilterOption = { id: string; label: string; count: number; color?: string }
@@ -46,6 +48,9 @@ type AgentDashboardToolbarProps = {
showAgentlessWorkspaces?: boolean
agentlessWorkspaceCount?: number
onShowAgentlessWorkspacesChange?: (show: boolean) => void
/** Map-only: the dashed parent→child dispatch edges. */
showOrchestrationLinks?: boolean
onShowOrchestrationLinksChange?: (show: boolean) => void
searchInputRef: React.RefObject<HTMLInputElement | null>
}
@@ -157,10 +162,6 @@ function reviewStateLabel(state: DashboardReviewFilter): string {
}
}
function OptionCount({ count }: { count: number }): React.JSX.Element {
return <span className="ml-auto text-[11px] tabular-nums text-muted-foreground">{count}</span>
}
export function AgentDashboardToolbar({
cards,
filterOptions,
@@ -175,6 +176,8 @@ export function AgentDashboardToolbar({
showAgentlessWorkspaces,
agentlessWorkspaceCount = 0,
onShowAgentlessWorkspacesChange,
showOrchestrationLinks,
onShowOrchestrationLinksChange,
searchInputRef
}: AgentDashboardToolbarProps): React.JSX.Element {
const isMac = navigator.userAgent.includes('Mac')
@@ -190,7 +193,9 @@ export function AgentDashboardToolbar({
const activeCount =
activeDashboardFilterCount(filters) +
mutedStateCount +
(showAgentlessWorkspaces === true ? 1 : 0)
(showAgentlessWorkspaces === true ? 1 : 0) +
// Links show by default, so only hiding them is a deviation worth badging.
(showOrchestrationLinks === false ? 1 : 0)
const toggleProject = (id: string): void =>
onFiltersChange({ ...filters, projects: toggleDashboardFilter(filters.projects, id) })
const toggleStatus = (id: string): void =>
@@ -207,6 +212,7 @@ export function AgentDashboardToolbar({
onFiltersChange({ projects: [], workspaceStatuses: [], reviewStates: [] })
onAgentStatesReset?.()
onShowAgentlessWorkspacesChange?.(false)
onShowOrchestrationLinksChange?.(true)
}
const reviewLabel = (id: DashboardReviewFilter): string =>
translate('dashboardPopout.filters.reviewChip', 'Review: {{state}}', {
@@ -274,26 +280,16 @@ export function AgentDashboardToolbar({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64" sideOffset={6}>
{showAgentlessWorkspaces !== undefined && onShowAgentlessWorkspacesChange ? (
<>
<DropdownMenuLabel>
{translate('dashboardPopout.map.filters.workspaceVisibility', 'Map content')}
</DropdownMenuLabel>
<DropdownMenuCheckboxItem
checked={showAgentlessWorkspaces}
onCheckedChange={(checked) => onShowAgentlessWorkspacesChange(checked === true)}
onSelect={(event) => event.preventDefault()}
>
<span className="truncate">
{translate(
'dashboardPopout.map.filters.agentlessWorkspaces',
'Workspaces without agents'
)}
</span>
<OptionCount count={agentlessWorkspaceCount} />
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
</>
{showAgentlessWorkspaces !== undefined &&
onShowAgentlessWorkspacesChange &&
onShowOrchestrationLinksChange ? (
<AgentMapContentFilterItems
showAgentlessWorkspaces={showAgentlessWorkspaces}
agentlessWorkspaceCount={agentlessWorkspaceCount}
onShowAgentlessWorkspacesChange={onShowAgentlessWorkspacesChange}
showOrchestrationLinks={showOrchestrationLinks !== false}
onShowOrchestrationLinksChange={onShowOrchestrationLinksChange}
/>
) : null}
{agentStates && agentStateCounts ? (
<>
@@ -309,7 +305,7 @@ export function AgentDashboardToolbar({
>
<AgentStateDot state={dotState} size="md" />
<span className="truncate">{agentStateLabel(state)}</span>
<OptionCount count={agentStateCounts[state]} />
<FilterOptionCount count={agentStateCounts[state]} />
</DropdownMenuCheckboxItem>
))}
<DropdownMenuSeparator />
@@ -326,7 +322,7 @@ export function AgentDashboardToolbar({
onSelect={(event) => event.preventDefault()}
>
<span className="truncate">{option.label}</span>
<OptionCount count={option.count} />
<FilterOptionCount count={option.count} />
</DropdownMenuCheckboxItem>
))}
<DropdownMenuSeparator />
@@ -348,7 +344,7 @@ export function AgentDashboardToolbar({
>
<span className={cn('size-2 rounded-full', meta.swatch)} />
<span className="truncate">{option.label}</span>
<OptionCount count={option.count} />
<FilterOptionCount count={option.count} />
</DropdownMenuCheckboxItem>
)
})}
@@ -364,7 +360,7 @@ export function AgentDashboardToolbar({
onSelect={(event) => event.preventDefault()}
>
<span>{reviewStateLabel(option)}</span>
<OptionCount count={reviewCounts.get(option) ?? 0} />
<FilterOptionCount count={reviewCounts.get(option) ?? 0} />
</DropdownMenuCheckboxItem>
))}
<DropdownMenuSeparator />
@@ -49,6 +49,7 @@ function renderMap(
compact = false,
workspaceContextMenusEnabled = false,
enabledStates,
showOrchestrationLinks,
launchableAgentsByWorktreeId,
onSpawnAgent,
onSleepWorkspace
@@ -58,6 +59,7 @@ function renderMap(
compact?: boolean
workspaceContextMenusEnabled?: boolean
enabledStates?: ReadonlySet<AgentMapState>
showOrchestrationLinks?: boolean
launchableAgentsByWorktreeId?: Record<string, TuiAgent[]>
onSpawnAgent?: (args: DashboardSpawnAgentArgs) => void
onSleepWorkspace?: (args: DashboardSleepWorkspaceArgs) => void
@@ -72,6 +74,7 @@ function renderMap(
compact={compact}
workspaceContextMenusEnabled={workspaceContextMenusEnabled}
enabledStates={enabledStates}
showOrchestrationLinks={showOrchestrationLinks}
launchableAgentsByWorktreeId={launchableAgentsByWorktreeId}
onSpawnAgent={onSpawnAgent}
onSleepWorkspace={onSleepWorkspace}
@@ -350,18 +353,19 @@ describe('AgentMap', () => {
})
const { container } = renderMap([parent, child, nested, orphan])
const workerLink = container.querySelector('[data-child-pane-key="child"]')
const subagentLink = container.querySelector('[data-child-pane-key="nested"]')
const nestedLink = container.querySelector('[data-child-pane-key="nested"]')
expect(screen.getByRole('button', { name: /Coordinator/ })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /^Worker/ })).toBeInTheDocument()
expect(container.querySelectorAll('[data-agent-map-lineage-link]')).toHaveLength(2)
expect(workerLink).toHaveClass('agent-map-lineage-link')
expect(workerLink).not.toHaveClass('is-subagent')
expect(workerLink).toHaveAttribute('data-agent-map-lineage-relation', 'orchestration')
expect(workerLink).toHaveAttribute('data-parent-pane-key', 'parent')
expect(subagentLink).toHaveClass('agent-map-lineage-link', 'is-subagent')
expect(subagentLink).toHaveAttribute('data-agent-map-lineage-relation', 'subagent')
expect(subagentLink).toHaveAttribute('data-parent-pane-key', 'child')
// Why orchestration, not subagent: `nested` is a card, and in-process subagents
// never become cards — so a grandchild dispatch is still an orchestration edge.
expect(nestedLink).toHaveClass('agent-map-lineage-link')
expect(nestedLink).toHaveAttribute('data-agent-map-lineage-relation', 'orchestration')
expect(nestedLink).toHaveAttribute('data-parent-pane-key', 'child')
})
it('connects spawned workers across worktree rings', () => {
@@ -387,13 +391,12 @@ describe('AgentMap', () => {
})
const { container } = renderMap([parent, child, nested])
const workerLink = container.querySelector('[data-child-pane-key="child"]')
const subagentLink = container.querySelector('[data-child-pane-key="nested"]')
const nestedLink = container.querySelector('[data-child-pane-key="nested"]')
expect(workerLink).toHaveClass('agent-map-lineage-link', 'is-cross-worktree')
expect(workerLink).not.toHaveClass('is-subagent')
expect(workerLink).toHaveAttribute('data-agent-map-lineage-relation', 'orchestration')
expect(subagentLink).toHaveClass('agent-map-lineage-link', 'is-cross-worktree', 'is-subagent')
expect(subagentLink).toHaveAttribute('data-agent-map-lineage-relation', 'subagent')
expect(nestedLink).toHaveClass('agent-map-lineage-link', 'is-cross-worktree')
expect(nestedLink).toHaveAttribute('data-agent-map-lineage-relation', 'orchestration')
})
it('keeps lineage styling to one lightweight path per relationship at fleet scale', () => {
@@ -409,13 +412,71 @@ describe('AgentMap', () => {
expect(container.querySelectorAll('[data-agent-map-lineage-link]')).toHaveLength(239)
expect(
container.querySelectorAll('[data-agent-map-lineage-relation="orchestration"]')
).toHaveLength(1)
).toHaveLength(239)
expect(container.querySelectorAll('[data-agent-map-lineage-relation="subagent"]')).toHaveLength(
238
0
)
expect(container.querySelectorAll('filter, animate, animateTransform')).toHaveLength(0)
})
it('hides orchestration links when the filter turns them off, keeping the agents', () => {
const sameWorktree = [
card({ paneKey: 'parent', conversationName: 'Coordinator' }),
card({ paneKey: 'child', parentPaneKey: 'parent', conversationName: 'Worker' })
]
const crossWorktree = [
card({ paneKey: 'far-parent', worktreeId: 'wt-a', worktreeName: 'A' }),
card({
paneKey: 'far-child',
worktreeId: 'wt-b',
worktreeName: 'B',
parentPaneKey: 'far-parent'
})
]
const cards = [...sameWorktree, ...crossWorktree]
const shown = renderMap(cards)
expect(shown.container.querySelectorAll('[data-agent-map-lineage-link]')).toHaveLength(2)
cleanup()
const hidden = renderMap(cards, { showOrchestrationLinks: false })
// Both same-worktree and cross-worktree edges go; the nodes themselves stay.
expect(hidden.container.querySelectorAll('[data-agent-map-lineage-link]')).toHaveLength(0)
expect(screen.getByRole('button', { name: /Coordinator/ })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /^Worker/ })).toBeInTheDocument()
})
it('draws no lineage edge for an agent that lists itself as its own parent', () => {
const { container } = renderMap([
card({ paneKey: 'self', parentPaneKey: 'self', conversationName: 'Self parent' }),
card({ paneKey: 'other', conversationName: 'Unrelated' })
])
expect(container.querySelectorAll('[data-agent-map-lineage-link]')).toHaveLength(0)
expect(screen.getByRole('button', { name: /Self parent/ })).toBeInTheDocument()
})
it('connects lineage between visible nodes even when the child is not ranked below its parent', () => {
// A 2-cycle cannot be ranked consistently, so the bounded layout (>256 agents)
// must place one of its edges pointing upward. Both nodes are drawn, so both
// edges must be too — the old y-ordering gate silently dropped the upward one.
const cards = [
card({ paneKey: 'cycle-a', parentPaneKey: 'cycle-b', conversationName: 'Cycle A' }),
card({ paneKey: 'cycle-b', parentPaneKey: 'cycle-a', conversationName: 'Cycle B' }),
...Array.from({ length: 255 }, (_, index) =>
card({
paneKey: `filler-${index}`,
parentPaneKey: index === 0 ? undefined : `filler-${index - 1}`,
conversationName: `Filler ${index}`
})
)
]
const { container } = renderMap(cards, { selectedPaneKey: 'cycle-a' })
expect(container.querySelector('[data-child-pane-key="cycle-a"]')).not.toBeNull()
expect(container.querySelector('[data-child-pane-key="cycle-b"]')).not.toBeNull()
})
it('connects visible child worktrees beneath their parent ring', () => {
const { container } = renderMap([
card({ paneKey: 'parent', worktreeId: 'parent-worktree', worktreeName: 'Parent' }),
@@ -31,6 +31,8 @@ type AgentMapProps = {
/** Owned by the board so the shared toolbar filter can drive it. Defaults to
* every state, i.e. the map shows whatever it is handed. */
enabledStates?: ReadonlySet<AgentMapState>
/** Owned by the board's filter menu. Defaults to shown. */
showOrchestrationLinks?: boolean
launchableAgentsByWorktreeId?: Record<string, TuiAgent[]>
workspaceContextMenusEnabled?: boolean
onWorkspaceContextMenuOpenChange?: (open: boolean) => void
@@ -72,6 +74,7 @@ export function AgentMap({
compact = false,
selectedPaneKey = null,
enabledStates = ALL_AGENT_STATES,
showOrchestrationLinks = true,
launchableAgentsByWorktreeId,
workspaceContextMenusEnabled = false,
onWorkspaceContextMenuOpenChange,
@@ -169,6 +172,7 @@ export function AgentMap({
repoIconsByRepoId={repoIconsByRepoId}
selectedPaneKey={selectedPaneKey}
allowAggregation
showOrchestrationLinks={showOrchestrationLinks}
launchableAgentsByWorktreeId={launchableAgentsByWorktreeId}
workspaceContextMenusEnabled={workspaceContextMenusEnabled}
onWorkspaceContextMenuOpenChange={onWorkspaceContextMenuOpenChange}
@@ -16,13 +16,9 @@ import type {
} from '../../../../shared/dashboard-snapshot'
import type { RepoIcon } from '../../../../shared/repo-icon'
import type { TuiAgent } from '../../../../shared/types'
import {
AGENT_MAP_AGENT_RADIUS,
type AgentMapAgentNode,
type AgentMapProjectRing,
type AgentMapLayout
} from './agent-map-layout'
import type { AgentMapAgentNode, AgentMapProjectRing, AgentMapLayout } from './agent-map-layout'
import { AgentMapScene } from './AgentMapScene'
import { agentFocusZoom, clamp, MAX_ZOOM, MIN_ZOOM } from './agent-map-canvas-zoom'
import { AgentMapViewportControls } from './AgentMapViewportControls'
import {
agentMapAgents,
@@ -35,10 +31,7 @@ import { useAgentMapCanvasSize } from './useAgentMapCanvasSize'
import { useAgentMapSelectedFocus } from './useAgentMapSelectedFocus'
import { useAgentMapViewportTransition } from './useAgentMapViewportTransition'
const MIN_ZOOM = 0.7
const MAX_ZOOM = 24
const AGENT_FOCUS_DURATION_MS = 240
const AGENT_FOCUS_RADIUS_PX = 24
type Point = { x: number; y: number }
@@ -52,6 +45,7 @@ type AgentMapCanvasProps = {
repoIconsByRepoId?: Record<string, RepoIcon | null>
selectedPaneKey: string | null
allowAggregation: boolean
showOrchestrationLinks: boolean
launchableAgentsByWorktreeId?: Record<string, TuiAgent[]>
workspaceContextMenusEnabled?: boolean
onWorkspaceContextMenuOpenChange?: (open: boolean) => void
@@ -60,23 +54,6 @@ type AgentMapCanvasProps = {
onSleepWorkspace?: (args: DashboardSleepWorkspaceArgs) => void
}
function clamp(value: number, minimum: number, maximum: number): number {
return Math.max(minimum, Math.min(maximum, value))
}
function agentFocusZoom(layout: AgentMapLayout, width: number, height: number): number {
const aspect = width / Math.max(1, height)
const baseWidth = Math.max(layout.width, layout.height * aspect)
return clamp(
Math.max(
2,
(baseWidth * AGENT_FOCUS_RADIUS_PX) / (Math.max(1, width) * AGENT_MAP_AGENT_RADIUS)
),
MIN_ZOOM,
MAX_ZOOM
)
}
export const AgentMapCanvas = forwardRef<AgentMapCanvasHandle, AgentMapCanvasProps>(
function AgentMapCanvas(
{
@@ -84,6 +61,7 @@ export const AgentMapCanvas = forwardRef<AgentMapCanvasHandle, AgentMapCanvasPro
repoIconsByRepoId,
selectedPaneKey,
allowAggregation,
showOrchestrationLinks,
launchableAgentsByWorktreeId,
workspaceContextMenusEnabled = false,
onWorkspaceContextMenuOpenChange,
@@ -391,6 +369,7 @@ export const AgentMapCanvas = forwardRef<AgentMapCanvasHandle, AgentMapCanvasPro
mapScale={mapScale}
selectedPaneKey={selectedPaneKey}
allowAggregation={allowAggregation}
showOrchestrationLinks={showOrchestrationLinks}
launchableAgentsByWorktreeId={launchableAgentsByWorktreeId}
nodeRefs={nodeRefs}
onSelectAgent={onSelectAgent}
@@ -0,0 +1,59 @@
import {
DropdownMenuCheckboxItem,
DropdownMenuLabel,
DropdownMenuSeparator
} from '@/components/ui/dropdown-menu'
import { translate } from '@/i18n/i18n'
import { FilterOptionCount } from './FilterOptionCount'
type AgentMapContentFilterItemsProps = {
showAgentlessWorkspaces: boolean
agentlessWorkspaceCount: number
onShowAgentlessWorkspacesChange: (show: boolean) => void
showOrchestrationLinks: boolean
onShowOrchestrationLinksChange: (show: boolean) => void
}
/**
* The map-only "Map content" rows of the shared dashboard filter menu. These
* govern what the map draws rather than which cards survive filtering, so they
* sit apart from the project/status/review sections.
*/
export function AgentMapContentFilterItems({
showAgentlessWorkspaces,
agentlessWorkspaceCount,
onShowAgentlessWorkspacesChange,
showOrchestrationLinks,
onShowOrchestrationLinksChange
}: AgentMapContentFilterItemsProps): React.JSX.Element {
return (
<>
<DropdownMenuLabel>
{translate('dashboardPopout.map.filters.workspaceVisibility', 'Map content')}
</DropdownMenuLabel>
<DropdownMenuCheckboxItem
checked={showAgentlessWorkspaces}
onCheckedChange={(checked) => onShowAgentlessWorkspacesChange(checked === true)}
onSelect={(event) => event.preventDefault()}
>
<span className="truncate">
{translate(
'dashboardPopout.map.filters.agentlessWorkspaces',
'Workspaces without agents'
)}
</span>
<FilterOptionCount count={agentlessWorkspaceCount} />
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={showOrchestrationLinks}
onCheckedChange={(checked) => onShowOrchestrationLinksChange(checked === true)}
onSelect={(event) => event.preventDefault()}
>
<span className="truncate">
{translate('dashboardPopout.map.filters.orchestrationLinks', 'Orchestration links')}
</span>
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
</>
)
}
@@ -41,6 +41,7 @@ describe('AgentMapScene project labels', () => {
mapScale={0.5}
selectedPaneKey={null}
allowAggregation
showOrchestrationLinks
nodeRefs={{ current: new Map() }}
onSelectAgent={vi.fn()}
onAgentKeyDown={vi.fn()}
@@ -10,7 +10,7 @@ import type {
AgentMapProjectRing,
AgentMapWorktreeRing
} from './agent-map-layout'
import { shouldAggregateAgentMapWorktree } from './agent-map-layout'
import { AGENT_MAP_LINEAGE_RELATION, shouldAggregateAgentMapWorktree } from './agent-map-layout'
import { selectVisibleAgentMapLabels } from './agent-map-label-declutter'
import { AgentMapWorktreeLabel } from './AgentMapWorktreeLabel'
import { AgentMapWorktreeRingNode } from './AgentMapWorktreeRingNode'
@@ -23,6 +23,7 @@ type AgentMapSceneProps = {
mapScale: number
selectedPaneKey: string | null
allowAggregation: boolean
showOrchestrationLinks: boolean
launchableAgentsByWorktreeId?: Record<string, TuiAgent[]>
nodeRefs: MutableRefObject<Map<string, SVGGElement>>
onSelectAgent: (card: DashboardCard) => void
@@ -71,6 +72,7 @@ export const AgentMapScene = memo(function AgentMapScene({
mapScale,
selectedPaneKey,
allowAggregation,
showOrchestrationLinks,
launchableAgentsByWorktreeId,
nodeRefs,
onSelectAgent,
@@ -114,17 +116,19 @@ export const AgentMapScene = memo(function AgentMapScene({
'{{agents}} agents · {{workspaces}} workspaces',
{ agents: project.agentCount, workspaces: project.worktrees.length }
).toUpperCase()
const crossWorktreeLineage = project.worktrees.flatMap((worktree) =>
worktree.agents.flatMap((child) => {
const parent = child.card.parentPaneKey
? visibleAgentsByPaneKey.get(child.card.parentPaneKey)
: undefined
const childLocation = visibleAgentsByPaneKey.get(child.card.paneKey)
return parent && childLocation && parent.worktreeId !== childLocation.worktreeId
? [{ parent: parent.agent, child }]
: []
})
)
const crossWorktreeLineage = !showOrchestrationLinks
? []
: project.worktrees.flatMap((worktree) =>
worktree.agents.flatMap((child) => {
const parent = child.card.parentPaneKey
? visibleAgentsByPaneKey.get(child.card.parentPaneKey)
: undefined
const childLocation = visibleAgentsByPaneKey.get(child.card.paneKey)
return parent && childLocation && parent.worktreeId !== childLocation.worktreeId
? [{ parent: parent.agent, child }]
: []
})
)
return (
<g key={project.id}>
<circle
@@ -159,21 +163,18 @@ export const AgentMapScene = memo(function AgentMapScene({
})}
</g>
<g className="agent-map-lineage-links" aria-hidden>
{crossWorktreeLineage.map(({ parent, child }) => {
const relation = parent.card.parentPaneKey ? 'subagent' : 'orchestration'
return (
<path
key={child.card.paneKey}
className={`agent-map-lineage-link is-cross-worktree${relation === 'subagent' ? ' is-subagent' : ''}`}
data-agent-map-lineage-link=""
data-agent-map-cross-worktree-lineage-link=""
data-agent-map-lineage-relation={relation}
data-parent-pane-key={parent.card.paneKey}
data-child-pane-key={child.card.paneKey}
d={agentLineagePath(parent, child)}
/>
)
})}
{crossWorktreeLineage.map(({ parent, child }) => (
<path
key={child.card.paneKey}
className="agent-map-lineage-link is-cross-worktree"
data-agent-map-lineage-link=""
data-agent-map-cross-worktree-lineage-link=""
data-agent-map-lineage-relation={AGENT_MAP_LINEAGE_RELATION}
data-parent-pane-key={parent.card.paneKey}
data-child-pane-key={child.card.paneKey}
d={agentLineagePath(parent, child)}
/>
))}
</g>
{project.worktrees.map((worktree) => (
<AgentMapWorktreeRingNode
@@ -184,6 +185,7 @@ export const AgentMapScene = memo(function AgentMapScene({
mapScale={mapScale}
selectedPaneKey={selectedPaneKey}
allowAggregation={allowAggregation}
showOrchestrationLinks={showOrchestrationLinks}
launchableAgents={launchableAgentsByWorktreeId?.[worktree.worktreeId]}
nodeRefs={nodeRefs}
onSelectAgent={onSelectAgent}
@@ -13,7 +13,7 @@ import type {
AgentMapProjectRing,
AgentMapWorktreeRing
} from './agent-map-layout'
import { shouldAggregateAgentMapWorktree } from './agent-map-layout'
import { AGENT_MAP_LINEAGE_RELATION, shouldAggregateAgentMapWorktree } from './agent-map-layout'
import { agentMapWorktreeActiveStatus } from './agent-map-worktree-active-status'
type AgentMapWorktreeRingNodeProps = {
@@ -23,6 +23,7 @@ type AgentMapWorktreeRingNodeProps = {
mapScale: number
selectedPaneKey: string | null
allowAggregation: boolean
showOrchestrationLinks: boolean
launchableAgents?: readonly TuiAgent[]
nodeRefs: MutableRefObject<Map<string, SVGGElement>>
onSelectAgent: (card: DashboardCard) => void
@@ -49,9 +50,13 @@ function formatDuration(minutes: number): string {
})
}
// Why direction-aware: packing can place a child level with or above its parent, and
// a fixed downward elbow would then exit the wrong side and draw back through both
// nodes. Leaving the rank tie at +1 keeps the common downward case byte-identical.
function lineagePath(parent: AgentMapAgentNode, child: AgentMapAgentNode): string {
const startY = parent.y + parent.radius
const endY = child.y - child.radius
const direction = child.y < parent.y ? -1 : 1
const startY = parent.y + parent.radius * direction
const endY = child.y - child.radius * direction
const branchY = (startY + endY) / 2
return `M ${parent.x} ${startY} L ${parent.x} ${branchY} L ${child.x} ${branchY} L ${child.x} ${endY}`
}
@@ -178,6 +183,7 @@ export const AgentMapWorktreeRingNode = memo(function AgentMapWorktreeRingNode({
mapScale,
selectedPaneKey,
allowAggregation,
showOrchestrationLinks,
launchableAgents,
nodeRefs,
onSelectAgent,
@@ -268,20 +274,26 @@ export const AgentMapWorktreeRingNode = memo(function AgentMapWorktreeRingNode({
) : (
<>
<g className="agent-map-lineage-links" aria-hidden>
{worktree.agents.map((child) => {
{(showOrchestrationLinks ? worktree.agents : []).map((child) => {
const parent = child.card.parentPaneKey
? agentsByPaneKey.get(child.card.parentPaneKey)
: undefined
if (!parent || child.y <= parent.y) {
// Why no y-gate: both endpoints are drawn nodes, so the relationship is
// real whatever the layout ranked them. Suppressing the edge only hid
// lineage that packing pressure had placed side by side.
//
// Self-parents are dropped explicitly — the layout already ignores them
// (`layoutAgentMapLineage`), and the y-gate used to mask them here by
// accident since a node never ranks below itself.
if (!parent || parent.card.paneKey === child.card.paneKey) {
return null
}
const relation = parent.card.parentPaneKey ? 'subagent' : 'orchestration'
return (
<path
key={child.card.paneKey}
className={`agent-map-lineage-link${relation === 'subagent' ? ' is-subagent' : ''}`}
className="agent-map-lineage-link"
data-agent-map-lineage-link=""
data-agent-map-lineage-relation={relation}
data-agent-map-lineage-relation={AGENT_MAP_LINEAGE_RELATION}
data-parent-pane-key={parent.card.paneKey}
data-child-pane-key={child.card.paneKey}
d={lineagePath(parent, child)}
@@ -0,0 +1,4 @@
/** Right-aligned tally on a filter menu row. */
export function FilterOptionCount({ count }: { count: number }): React.JSX.Element {
return <span className="ml-auto text-[11px] tabular-nums text-muted-foreground">{count}</span>
}
@@ -0,0 +1,25 @@
import { AGENT_MAP_AGENT_RADIUS, type AgentMapLayout } from './agent-map-layout'
export const MIN_ZOOM = 0.7
export const MAX_ZOOM = 24
/** Screen radius a single agent should occupy once focused. */
const AGENT_FOCUS_RADIUS_PX = 24
export function clamp(value: number, minimum: number, maximum: number): number {
return Math.max(minimum, Math.min(maximum, value))
}
/** Zoom that brings one agent up to `AGENT_FOCUS_RADIUS_PX` on screen. */
export function agentFocusZoom(layout: AgentMapLayout, width: number, height: number): number {
const aspect = width / Math.max(1, height)
const baseWidth = Math.max(layout.width, layout.height * aspect)
return clamp(
Math.max(
2,
(baseWidth * AGENT_FOCUS_RADIUS_PX) / (Math.max(1, width) * AGENT_MAP_AGENT_RADIUS)
),
MIN_ZOOM,
MAX_ZOOM
)
}
@@ -25,6 +25,14 @@ export const AGENT_MAP_AGENT_RADIUS = 20
export const AGENT_MAP_AGGREGATE_ZOOM = 1.15
export const AGENT_MAP_RING_HEADER_HEIGHT = 40
/**
* Every map node is a top-level pane agent — in-process subagent rows are folded
* into their parent card's `subagents` roster and never become cards themselves
* (`build-dashboard-snapshot.ts`). So an edge between two nodes is always an
* orchestration dispatch, and there is no second relation to distinguish.
*/
export const AGENT_MAP_LINEAGE_RELATION = 'orchestration'
const PROJECT_PADDING = 12
const WORLD_MARGIN = 32
const RING_CONTENT_OFFSET = AGENT_MAP_RING_HEADER_HEIGHT / 2
@@ -205,10 +205,6 @@
vector-effect: non-scaling-stroke;
}
.agent-map-lineage-link.is-subagent {
stroke-dasharray: none;
}
.agent-map-agent-hit {
fill: transparent;
stroke: transparent;
+1
View File
@@ -14977,6 +14977,7 @@
"showStates": "Agent states",
"canvasSummary": "{{shown}} of {{total}} agents shown",
"agentlessWorkspaces": "Workspaces without agents",
"orchestrationLinks": "Orchestration links",
"workspaceVisibility": "Map content"
},
"liveContainmentMap": "Live containment map",