fix(dashboard): hold agent map orchestration chevrons at a fixed pitch

CHEVRON_SPACING only chose how many chevrons to draw; placement then divided
the edge evenly, so the pitch grew with the distance between agents and, past
the 32-chevron cap, grew without bound. Step at a literal 8px pitch instead,
centering the run so it never overhangs either node.

Fixed pitch makes length drive the chevron count, so cap coverage now decides
how far the run reaches: at 32 it spanned only 248px and every longer link
would have shown a chevron cluster stranded mid-edge. Raise it to 256 (~2048px,
past any real in-project link) and memoize the generated path per endpoint
coordinates, since the scene rebuilds every path string on each zoom frame and
on 4Hz snapshot refreshes while world positions stay put.
This commit is contained in:
Brennan Benson
2026-08-13 01:28:28 -07:00
parent 2c28b6c92c
commit 67ebdd9b64
2 changed files with 90 additions and 5 deletions
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest'
import { agentMapDirectLineageChevronPath } from './agent-map-lineage-chevron-path'
import {
agentMapDirectLineageChevronPath,
agentMapLineagePathCacheSize
} from './agent-map-lineage-chevron-path'
describe('agentMapDirectLineageChevronPath', () => {
it('runs every chevron directly from the parent toward the child', () => {
@@ -41,6 +44,50 @@ describe('agentMapDirectLineageChevronPath', () => {
{ x: 10_000, y: 0, radius: 0 }
)
expect(path.match(/\bM\b/g)).toHaveLength(32)
expect(path.match(/\bM\b/g)).toHaveLength(256)
})
it('keeps the same chevron pitch however far apart the nodes are', () => {
const pitches = [60, 200, 900].map((distance) => {
const tips = [
...agentMapDirectLineageChevronPath(
{ x: 0, y: 0, radius: 0 },
{ x: distance, y: 0, radius: 0 }
).matchAll(/M [-\d.]+ [-\d.]+ L ([-\d.]+) [-\d.]+ L/g)
].map((match) => Number(match[1]))
expect(tips.length).toBeGreaterThan(2)
return tips.slice(1).map((tip, index) => tip - tips[index])
})
expect(pitches.flat().every((pitch) => pitch === 8)).toBe(true)
})
it('serves an unmoved edge from cache instead of rebuilding it', () => {
const parent = { x: 3, y: 5, radius: 20 }
const child = { x: 903, y: 5, radius: 20 }
const before = agentMapLineagePathCacheSize()
const first = agentMapDirectLineageChevronPath(parent, child)
const afterMiss = agentMapLineagePathCacheSize()
const second = agentMapDirectLineageChevronPath({ ...parent }, { ...child })
expect(afterMiss).toBe(before + 1) // first call was a miss
expect(agentMapLineagePathCacheSize()).toBe(afterMiss) // second stored nothing → hit
expect(second).toBe(first)
})
it('bounds the cache as edges churn', () => {
const hot = [
{ x: -1, y: -1, radius: 2 },
{ x: -1, y: -400, radius: 2 }
] as const
for (let i = 0; i < 600; i += 1) {
agentMapDirectLineageChevronPath({ x: i, y: 1_000, radius: 2 }, { x: i, y: 1_200, radius: 2 })
if (i % 10 === 0) {
agentMapDirectLineageChevronPath(hot[0], hot[1])
}
}
expect(agentMapLineagePathCacheSize()).toBe(512)
})
})
@@ -17,7 +17,7 @@ type LineageSegment = {
const CHEVRON_SPACING = 8
const CHEVRON_DEPTH = 3.5
const CHEVRON_HALF_WIDTH = 2.25
const MAX_CHEVRONS_PER_PATH = 32
const MAX_CHEVRONS_PER_PATH = 256
function svgNumber(value: number): number {
return Math.round(value * 1_000) / 1_000
@@ -46,11 +46,14 @@ export function agentMapLineageChevronPath(points: AgentMapLineagePoint[]): stri
MAX_CHEVRONS_PER_PATH,
Math.max(1, Math.floor(totalLength / CHEVRON_SPACING))
)
// Fixed pitch, centered run: spacing must read identically on a short link and a long
// one. Dividing the length by the count instead stretched the gaps as nodes moved apart.
const firstDistance = (totalLength - (chevronCount - 1) * CHEVRON_SPACING) / 2
const commands: string[] = []
let segmentIndex = 0
let segmentStartDistance = 0
for (let index = 0; index < chevronCount; index += 1) {
const distance = (totalLength * (index + 1)) / (chevronCount + 1)
const distance = firstDistance + index * CHEVRON_SPACING
while (
segmentIndex < segments.length - 1 &&
distance > segmentStartDistance + segments[segmentIndex].length
@@ -73,7 +76,7 @@ export function agentMapLineageChevronPath(points: AgentMapLineagePoint[]): stri
return commands.join(' ')
}
export function agentMapDirectLineageChevronPath(
function buildDirectLineageChevronPath(
parent: AgentMapLineageNode,
child: AgentMapLineageNode
): string {
@@ -90,3 +93,38 @@ export function agentMapDirectLineageChevronPath(
{ x: child.x - unitX * child.radius, y: child.y - unitY * child.radius }
])
}
// Keyed on world coordinates, which a zoom gesture never changes — so the scene's
// per-frame rerender reuses every path instead of rebuilding kilobytes of `d` at
// 60fps. Only enter/exit motion, which really does move nodes, misses. LRU-bounded
// because a removed agent's key is never revisited.
const MAX_CACHED_LINEAGE_PATHS = 512
const lineagePathCache = new Map<string, string>()
export function agentMapDirectLineageChevronPath(
parent: AgentMapLineageNode,
child: AgentMapLineageNode
): string {
const key = `${parent.x},${parent.y},${parent.radius},${child.x},${child.y},${child.radius}`
const cached = lineagePathCache.get(key)
if (cached !== undefined) {
lineagePathCache.delete(key)
lineagePathCache.set(key, cached)
return cached
}
const path = buildDirectLineageChevronPath(parent, child)
lineagePathCache.set(key, path)
while (lineagePathCache.size > MAX_CACHED_LINEAGE_PATHS) {
const oldest = lineagePathCache.keys().next().value
if (oldest === undefined) {
break
}
lineagePathCache.delete(oldest)
}
return path
}
/** The cache is module-private, so its bound is only observable through here. */
export function agentMapLineagePathCacheSize(): number {
return lineagePathCache.size
}