From a5a6a43ce77a3a2d7b2b53d6d2cbf19f3acf81a0 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:23:28 -0700 Subject: [PATCH] Route agent lineage links diagonally --- .../dashboard-popout/AgentMapScene.tsx | 15 ++----------- .../AgentMapWorktreeRingNode.tsx | 16 ++------------ .../agent-map-lineage-chevron-path.test.ts | 19 ++++++++++++++++ .../agent-map-lineage-chevron-path.ts | 22 +++++++++++++++++++ 4 files changed, 45 insertions(+), 27 deletions(-) create mode 100644 src/renderer/src/components/dashboard-popout/agent-map-lineage-chevron-path.test.ts diff --git a/src/renderer/src/components/dashboard-popout/AgentMapScene.tsx b/src/renderer/src/components/dashboard-popout/AgentMapScene.tsx index 9831f77a302..251e7181802 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMapScene.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMapScene.tsx @@ -12,7 +12,7 @@ import type { } from './agent-map-layout' import { AGENT_MAP_LINEAGE_RELATION, shouldAggregateAgentMapWorktree } from './agent-map-layout' import { selectVisibleAgentMapLabels } from './agent-map-label-declutter' -import { agentMapLineageChevronPath } from './agent-map-lineage-chevron-path' +import { agentMapDirectLineageChevronPath } from './agent-map-lineage-chevron-path' import { AgentMapWorktreeLabel } from './AgentMapWorktreeLabel' import { AgentMapWorktreeRingNode } from './AgentMapWorktreeRingNode' @@ -53,18 +53,7 @@ type VisibleAgentLocation = { } function agentLineagePath(parent: AgentMapAgentNode, child: AgentMapAgentNode): string { - const dx = child.x - parent.x - const dy = child.y - parent.y - const distance = Math.hypot(dx, dy) - if (distance === 0) { - return `M ${parent.x} ${parent.y}` - } - const unitX = dx / distance - const unitY = dy / distance - return agentMapLineageChevronPath([ - { x: parent.x + unitX * parent.radius, y: parent.y + unitY * parent.radius }, - { x: child.x - unitX * child.radius, y: child.y - unitY * child.radius } - ]) + return agentMapDirectLineageChevronPath(parent, child) } /** Memoization keeps pointer panning to one SVG viewBox write, not a map rerender. */ diff --git a/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx b/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx index 090fbf7f6fb..9642cf16155 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx @@ -14,7 +14,7 @@ import type { AgentMapWorktreeRing } from './agent-map-layout' import { AGENT_MAP_LINEAGE_RELATION, shouldAggregateAgentMapWorktree } from './agent-map-layout' -import { agentMapLineageChevronPath } from './agent-map-lineage-chevron-path' +import { agentMapDirectLineageChevronPath } from './agent-map-lineage-chevron-path' import { agentMapWorktreeActiveStatus } from './agent-map-worktree-active-status' type AgentMapWorktreeRingNodeProps = { @@ -51,20 +51,8 @@ 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 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 agentMapLineageChevronPath([ - { x: parent.x, y: startY }, - { x: parent.x, y: branchY }, - { x: child.x, y: branchY }, - { x: child.x, y: endY } - ]) + return agentMapDirectLineageChevronPath(parent, child) } function agentName(card: DashboardCard): string { diff --git a/src/renderer/src/components/dashboard-popout/agent-map-lineage-chevron-path.test.ts b/src/renderer/src/components/dashboard-popout/agent-map-lineage-chevron-path.test.ts new file mode 100644 index 00000000000..88dcbe4e9cd --- /dev/null +++ b/src/renderer/src/components/dashboard-popout/agent-map-lineage-chevron-path.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest' +import { agentMapDirectLineageChevronPath } from './agent-map-lineage-chevron-path' + +describe('agentMapDirectLineageChevronPath', () => { + it('runs every chevron directly from the parent toward the child', () => { + const path = agentMapDirectLineageChevronPath( + { x: 0, y: 0, radius: 4 }, + { x: 40, y: 40, radius: 4 } + ) + const tips = [...path.matchAll(/M [-\d.]+ [-\d.]+ L ([-\d.]+) ([-\d.]+) L/g)].map((match) => ({ + x: Number(match[1]), + y: Number(match[2]) + })) + + expect(tips.length).toBeGreaterThan(1) + expect(tips.every((tip) => tip.x === tip.y)).toBe(true) + expect(tips.at(-1)?.x).toBeGreaterThan(tips[0].x) + }) +}) diff --git a/src/renderer/src/components/dashboard-popout/agent-map-lineage-chevron-path.ts b/src/renderer/src/components/dashboard-popout/agent-map-lineage-chevron-path.ts index ef4fa890636..b4888fdd21b 100644 --- a/src/renderer/src/components/dashboard-popout/agent-map-lineage-chevron-path.ts +++ b/src/renderer/src/components/dashboard-popout/agent-map-lineage-chevron-path.ts @@ -3,6 +3,10 @@ export type AgentMapLineagePoint = { y: number } +type AgentMapLineageNode = AgentMapLineagePoint & { + radius: number +} + type LineageSegment = { start: AgentMapLineagePoint unitX: number @@ -64,3 +68,21 @@ export function agentMapLineageChevronPath(points: AgentMapLineagePoint[]): stri } return commands.join(' ') } + +export function agentMapDirectLineageChevronPath( + parent: AgentMapLineageNode, + child: AgentMapLineageNode +): string { + const dx = child.x - parent.x + const dy = child.y - parent.y + const distance = Math.hypot(dx, dy) + if (distance === 0) { + return agentMapLineageChevronPath([parent]) + } + const unitX = dx / distance + const unitY = dy / distance + return agentMapLineageChevronPath([ + { x: parent.x + unitX * parent.radius, y: parent.y + unitY * parent.radius }, + { x: child.x - unitX * child.radius, y: child.y - unitY * child.radius } + ]) +}