From 7296d92526565fc194a48d8814d48ed1dc60a333 Mon Sep 17 00:00:00 2001 From: integrate-your-mind <43152353+integrate-your-mind@users.noreply.github.com> Date: Fri, 30 Jan 2026 02:47:11 -0500 Subject: [PATCH 1/4] feat(layout): scale pinned placement with spatial hash --- public/src/lib/layout.ts | 446 +++++++++++++++++++++++++++++---------- 1 file changed, 336 insertions(+), 110 deletions(-) diff --git a/public/src/lib/layout.ts b/public/src/lib/layout.ts index 3bb730b..57b6825 100644 --- a/public/src/lib/layout.ts +++ b/public/src/lib/layout.ts @@ -5,8 +5,13 @@ import { isoToScreen } from './iso'; const GRID_SCALE = 2; const TILE_W = 96; const TILE_H = 48; +const CELL_W = TILE_W; +const CELL_H = TILE_H; const MAX_PULSE = 7; const MAX_LAYOUT_HEIGHT = 120 + MAX_PULSE; +const CELL_OFFSET = 32768; + +type CellBucket = string | string[]; interface BoundsRect { left: number; @@ -15,20 +20,50 @@ interface BoundsRect { bottom: number; } +interface SpatialIndex { + cells: Map; + bounds: Map; +} + +interface SpiralState { + cx: number; + cy: number; + dir: 0 | 1 | 2 | 3; + legLen: number; + legProgress: number; + legsAtLen: 0 | 1; + started: boolean; +} + +interface GroupState { + anchor: Coordinate; + spiral: SpiralState; + freeStack: number[]; +} + export interface LayoutState { layout: Map; - occupied: Map; - bounds: Map; groupAnchors: Map; + agentGroupKey: Map; + groups: Map; + spatial: SpatialIndex; + seenGen: Map; + generation: number; locked: boolean; } export function createLayoutState(): LayoutState { return { layout: new Map(), - occupied: new Map(), - bounds: new Map(), groupAnchors: new Map(), + agentGroupKey: new Map(), + groups: new Map(), + spatial: { + cells: new Map(), + bounds: new Map(), + }, + seenGen: new Map(), + generation: 0, locked: false, }; } @@ -45,10 +80,66 @@ function hashString(input: string): number { function layoutIdForAgent(agent: AgentSnapshot): string { const identity = agentIdentity(agent); if (identity) return identity; + if (typeof agent.pid === 'number') return `${agent.pid}`; + if (agent.id) return agent.id; const groupKey = groupKeyForAgent(agent); if (groupKey) return groupKey; - if (typeof agent.pid === 'number') return `${agent.pid}`; - return agent.id || 'unknown'; + return 'unknown'; +} + +function worldToCellX(x: number): number { + return Math.floor(x / CELL_W); +} + +function worldToCellY(y: number): number { + return Math.floor(y / CELL_H); +} + +function packCell(cx: number, cy: number): number { + const ux = (cx + CELL_OFFSET) & 0xffff; + const uy = (cy + CELL_OFFSET) & 0xffff; + return ((ux << 16) | uy) >>> 0; +} + +function unpackCell(key: number): { cx: number; cy: number } { + const ux = key >>> 16; + const uy = key & 0xffff; + return { cx: ux - CELL_OFFSET, cy: uy - CELL_OFFSET }; +} + +function cellToWorld(cx: number, cy: number): Coordinate { + return { x: cx * GRID_SCALE, y: cy * GRID_SCALE }; +} + +function gridKeyFromWorld(coord: Coordinate): number { + const cx = Math.round(coord.x / GRID_SCALE); + const cy = Math.round(coord.y / GRID_SCALE); + return packCell(cx, cy); +} + +function bucketAdd(bucket: CellBucket | undefined, id: string): CellBucket { + if (bucket === undefined) return id; + if (typeof bucket === 'string') { + if (bucket === id) return bucket; + return [bucket, id]; + } + for (let i = 0; i < bucket.length; i += 1) { + if (bucket[i] === id) return bucket; + } + bucket.push(id); + return bucket; +} + +function bucketRemove(bucket: CellBucket | undefined, id: string): CellBucket | undefined { + if (bucket === undefined) return undefined; + if (typeof bucket === 'string') return bucket === id ? undefined : bucket; + const index = bucket.indexOf(id); + if (index === -1) return bucket; + const last = bucket.pop(); + if (last === undefined) return undefined; + if (index < bucket.length) bucket[index] = last; + if (bucket.length === 1) return bucket[0]; + return bucket.length ? bucket : undefined; } function boundsForCoord(coord: Coordinate): BoundsRect { @@ -67,46 +158,123 @@ function boundsIntersect(a: BoundsRect, b: BoundsRect): boolean { return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top; } -function hasBoundsOverlap(testBounds: BoundsRect, placedBounds: Map): boolean { - for (const bounds of placedBounds.values()) { - if (boundsIntersect(testBounds, bounds)) return true; +function cellRangeForBounds(bounds: BoundsRect): { + minCx: number; + maxCx: number; + minCy: number; + maxCy: number; +} { + const minCx = worldToCellX(bounds.left); + const maxCx = worldToCellX(bounds.right - 1); + const minCy = worldToCellY(bounds.top); + const maxCy = worldToCellY(bounds.bottom - 1); + return { minCx, maxCx, minCy, maxCy }; +} + +function indexAgent(id: string, coord: Coordinate, spatial: SpatialIndex): void { + const bounds = boundsForCoord(coord); + spatial.bounds.set(id, bounds); + + const range = cellRangeForBounds(bounds); + for (let cx = range.minCx; cx <= range.maxCx; cx += 1) { + for (let cy = range.minCy; cy <= range.maxCy; cy += 1) { + const key = packCell(cx, cy); + const bucket = spatial.cells.get(key); + spatial.cells.set(key, bucketAdd(bucket, id)); + } } - return false; } -function tryPlaceCoordinate( - coord: Coordinate, - nextOccupied: Map, - nextBounds: Map -): { coord: Coordinate; bounds: BoundsRect; cellKey: string } | null { - const cellKey = `${coord.x / GRID_SCALE},${coord.y / GRID_SCALE}`; - if (nextOccupied.has(cellKey)) return null; - const testBounds = boundsForCoord(coord); - if (hasBoundsOverlap(testBounds, nextBounds)) return null; - return { coord, bounds: testBounds, cellKey }; -} - -function findPlacementNearAnchor( - anchor: Coordinate, - maxRadius: number, - nextOccupied: Map, - nextBounds: Map -): { coord: Coordinate; bounds: BoundsRect; cellKey: string } | null { - const baseX = Math.round(anchor.x / GRID_SCALE); - const baseY = Math.round(anchor.y / GRID_SCALE); - - for (let radius = 0; radius <= maxRadius; radius += 1) { - for (let dx = -radius; dx <= radius; dx += 1) { - for (let dy = -radius; dy <= radius; dy += 1) { - if (Math.abs(dx) !== radius && Math.abs(dy) !== radius) continue; - const coord = { x: (baseX + dx) * GRID_SCALE, y: (baseY + dy) * GRID_SCALE }; - const placed = tryPlaceCoordinate(coord, nextOccupied, nextBounds); - if (placed) return placed; +function unindexAgent(id: string, spatial: SpatialIndex): void { + const bounds = spatial.bounds.get(id); + if (!bounds) return; + + const range = cellRangeForBounds(bounds); + for (let cx = range.minCx; cx <= range.maxCx; cx += 1) { + for (let cy = range.minCy; cy <= range.maxCy; cy += 1) { + const key = packCell(cx, cy); + const bucket = spatial.cells.get(key); + const next = bucketRemove(bucket, id); + if (next === undefined) { + spatial.cells.delete(key); + } else { + spatial.cells.set(key, next); } } } - return null; + spatial.bounds.delete(id); +} + +function hasCollision(bounds: BoundsRect, spatial: SpatialIndex): boolean { + const range = cellRangeForBounds(bounds); + for (let cx = range.minCx; cx <= range.maxCx; cx += 1) { + for (let cy = range.minCy; cy <= range.maxCy; cy += 1) { + const key = packCell(cx, cy); + const bucket = spatial.cells.get(key); + if (!bucket) continue; + if (typeof bucket === 'string') { + const other = spatial.bounds.get(bucket); + if (other && boundsIntersect(bounds, other)) return true; + } else { + for (let i = 0; i < bucket.length; i += 1) { + const other = spatial.bounds.get(bucket[i]); + if (other && boundsIntersect(bounds, other)) return true; + } + } + } + } + return false; +} + +function spiralInit(anchor: Coordinate): SpiralState { + const cx = Math.round(anchor.x / GRID_SCALE); + const cy = Math.round(anchor.y / GRID_SCALE); + return { + cx, + cy, + dir: 0, + legLen: 1, + legProgress: 0, + legsAtLen: 0, + started: false, + }; +} + +function spiralNext(state: SpiralState): { cx: number; cy: number } { + if (!state.started) { + state.started = true; + return { cx: state.cx, cy: state.cy }; + } + + switch (state.dir) { + case 0: + state.cx += 1; + break; + case 1: + state.cy -= 1; + break; + case 2: + state.cx -= 1; + break; + case 3: + state.cy += 1; + break; + } + + state.legProgress += 1; + if (state.legProgress === state.legLen) { + state.legProgress = 0; + state.dir = ((state.dir + 1) & 3) as 0 | 1 | 2 | 3; + if (state.legsAtLen === 1) { + state.legsAtLen = 0; + state.legLen += 1; + } else { + state.legsAtLen = 1; + } + } + + return { cx: state.cx, cy: state.cy }; } function hashedAnchorForGroup(groupKey: string): Coordinate { @@ -116,22 +284,97 @@ function hashedAnchorForGroup(groupKey: string): Coordinate { return { x: baseX * GRID_SCALE, y: baseY * GRID_SCALE }; } +function ensureGroupState( + state: LayoutState, + groupKey: string, + fallbackAnchor?: Coordinate +): GroupState { + let group = state.groups.get(groupKey); + if (!group) { + const anchor = state.groupAnchors.get(groupKey) ?? fallbackAnchor ?? hashedAnchorForGroup(groupKey); + group = { + anchor, + spiral: spiralInit(anchor), + freeStack: [], + }; + state.groups.set(groupKey, group); + if (!state.groupAnchors.has(groupKey)) { + state.groupAnchors.set(groupKey, anchor); + } + } else if (!state.groupAnchors.has(groupKey)) { + state.groupAnchors.set(groupKey, group.anchor); + } + return group; +} + +function findPlacement( + group: GroupState, + spatial: SpatialIndex, + maxAttempts = 256 +): Coordinate | null { + while (group.freeStack.length) { + const key = group.freeStack.pop(); + if (key === undefined) break; + const { cx, cy } = unpackCell(key); + const coord = cellToWorld(cx, cy); + const bounds = boundsForCoord(coord); + if (!hasCollision(bounds, spatial)) return coord; + } + + for (let i = 0; i < maxAttempts; i += 1) { + const { cx, cy } = spiralNext(group.spiral); + const coord = cellToWorld(cx, cy); + const bounds = boundsForCoord(coord); + if (!hasCollision(bounds, spatial)) return coord; + } + + return null; +} + +function addAgent( + state: LayoutState, + id: string, + groupKey: string +): void { + const hadGroup = state.groups.has(groupKey); + const group = ensureGroupState(state, groupKey); + const placement = findPlacement(group, state.spatial); + if (!placement) return; + state.layout.set(id, placement); + state.agentGroupKey.set(id, groupKey); + indexAgent(id, placement, state.spatial); + + if (!hadGroup) { + group.anchor = placement; + group.spiral = spiralInit(placement); + state.groupAnchors.set(groupKey, placement); + } +} + +function removeAgent(state: LayoutState, id: string): void { + const coord = state.layout.get(id); + if (coord) { + unindexAgent(id, state.spatial); + } + state.layout.delete(id); + + const groupKey = state.agentGroupKey.get(id); + if (groupKey && coord) { + const group = state.groups.get(groupKey); + if (group) { + group.freeStack.push(gridKeyFromWorld(coord)); + } + } + state.agentGroupKey.delete(id); +} + export function assignCoordinate( state: LayoutState, key: string, baseKey: string ): void { if (state.layout.has(key)) return; - const anchor = state.groupAnchors.get(baseKey) ?? hashedAnchorForGroup(baseKey || key); - const maxRadius = Math.max(24, Math.ceil(Math.sqrt(state.layout.size + 1)) * 32); - const placement = findPlacementNearAnchor(anchor, maxRadius, state.occupied, state.bounds); - if (!placement) return; - state.layout.set(key, placement.coord); - state.occupied.set(placement.cellKey, key); - state.bounds.set(key, placement.bounds); - if (!state.groupAnchors.has(baseKey)) { - state.groupAnchors.set(baseKey, placement.coord); - } + addAgent(state, key, baseKey || key); } export function updateLayout( @@ -139,74 +382,49 @@ export function updateLayout( agents: AgentSnapshot[] ): void { if (state.locked) return; + if (agents.length === 0) { state.layout.clear(); - state.occupied.clear(); - state.bounds.clear(); state.groupAnchors.clear(); + state.agentGroupKey.clear(); + state.groups.clear(); + state.spatial.cells.clear(); + state.spatial.bounds.clear(); + state.seenGen.clear(); return; } - const agentMap = new Map(); + state.generation += 1; + const gen = state.generation; + const added: Array<{ id: string; agent: AgentSnapshot; groupKey: string }> = []; + for (const agent of agents) { const id = layoutIdForAgent(agent); const groupKey = groupKeyForAgent(agent) || id; - agentMap.set(id, { agent, groupKey }); - } + state.seenGen.set(id, gen); + state.agentGroupKey.set(id, groupKey); - for (const id of state.layout.keys()) { - if (!agentMap.has(id)) { - const coord = state.layout.get(id); - if (coord) { - state.occupied.delete(`${coord.x / GRID_SCALE},${coord.y / GRID_SCALE}`); - } - state.layout.delete(id); - state.bounds.delete(id); + const existingCoord = state.layout.get(id); + if (existingCoord) { + ensureGroupState(state, groupKey, existingCoord); + continue; } + added.push({ id, agent, groupKey }); } - state.occupied.clear(); - state.bounds.clear(); - for (const [id, coord] of state.layout.entries()) { - state.occupied.set(`${coord.x / GRID_SCALE},${coord.y / GRID_SCALE}`, id); - state.bounds.set(id, boundsForCoord(coord)); - } - - const activeGroups = new Set(); - for (const { agent, groupKey } of agentMap.values()) { - activeGroups.add(groupKey); - if (!state.groupAnchors.has(groupKey)) { - const coord = state.layout.get(layoutIdForAgent(agent)); - if (coord) state.groupAnchors.set(groupKey, coord); + for (const id of Array.from(state.layout.keys())) { + if (state.seenGen.get(id) !== gen) { + removeAgent(state, id); } } - for (const key of state.groupAnchors.keys()) { - if (!activeGroups.has(key)) state.groupAnchors.delete(key); - } - const addedAgents = Array.from(agentMap.values()) - .filter(({ agent }) => !state.layout.has(layoutIdForAgent(agent))) - .sort((a, b) => { - if (a.groupKey === b.groupKey) { - return layoutIdForAgent(a.agent).localeCompare(layoutIdForAgent(b.agent)); - } - return a.groupKey.localeCompare(b.groupKey); - }); - - const maxRadius = Math.max(32, Math.ceil(Math.sqrt(state.layout.size + addedAgents.length)) * 32); - - for (const entry of addedAgents) { - const layoutId = layoutIdForAgent(entry.agent); - const groupKey = entry.groupKey || layoutId; - const anchor = state.groupAnchors.get(groupKey) ?? hashedAnchorForGroup(groupKey); - const placement = findPlacementNearAnchor(anchor, maxRadius, state.occupied, state.bounds); - if (!placement) continue; - state.layout.set(layoutId, placement.coord); - state.occupied.set(placement.cellKey, layoutId); - state.bounds.set(layoutId, placement.bounds); - if (!state.groupAnchors.has(groupKey)) { - state.groupAnchors.set(groupKey, placement.coord); - } + added.sort((a, b) => { + if (a.groupKey === b.groupKey) return a.id.localeCompare(b.id); + return a.groupKey.localeCompare(b.groupKey); + }); + + for (const entry of added) { + addAgent(state, entry.id, entry.groupKey); } } @@ -231,11 +449,14 @@ export function setLayoutPositions( positions: Array<{ id?: string; pid?: number; x: number; y: number }> ): void { state.layout.clear(); - state.occupied.clear(); - state.bounds.clear(); state.groupAnchors.clear(); + state.agentGroupKey.clear(); + state.groups.clear(); + state.spatial.cells.clear(); + state.spatial.bounds.clear(); + state.seenGen.clear(); state.locked = true; - + const byIdentity = new Map( agents.map((agent) => [agentIdentity(agent), agent]) ); @@ -244,21 +465,26 @@ export function setLayoutPositions( .filter((agent) => typeof agent.pid === 'number') .map((agent) => [`${agent.pid}`, agent]) ); - + for (const entry of positions) { const keyId = entry?.id ?? entry?.pid; if (keyId === undefined || keyId === null) continue; const agent = byIdentity.get(String(keyId)) || byPid.get(String(keyId)) || null; if (!agent) continue; - const key = layoutIdForAgent(agent); + const id = layoutIdForAgent(agent); + const groupKey = groupKeyForAgent(agent) || id; const coord = { x: Number(entry.x) || 0, y: Number(entry.y) || 0 }; - state.layout.set(key, coord); - state.occupied.set(`${coord.x / GRID_SCALE},${coord.y / GRID_SCALE}`, key); - state.bounds.set(key, boundsForCoord(coord)); - const groupKey = groupKeyForAgent(agent) || key; + state.layout.set(id, coord); + state.agentGroupKey.set(id, groupKey); + indexAgent(id, coord, state.spatial); + const group = ensureGroupState(state, groupKey, coord); if (!state.groupAnchors.has(groupKey)) { state.groupAnchors.set(groupKey, coord); } + if (!group.spiral.started) { + group.anchor = coord; + group.spiral = spiralInit(coord); + } } } From f68594c99ccb95d73675e29bed6400e6d94e1a38 Mon Sep 17 00:00:00 2001 From: integrate-your-mind <43152353+integrate-your-mind@users.noreply.github.com> Date: Fri, 30 Jan 2026 02:47:27 -0500 Subject: [PATCH 2/4] perf(ui): stabilize canvas rendering loop --- public/src/components/CanvasScene.tsx | 182 ++------------------------ public/src/hooks/useCanvasRenderer.ts | 142 ++++++++++++++------ 2 files changed, 117 insertions(+), 207 deletions(-) diff --git a/public/src/components/CanvasScene.tsx b/public/src/components/CanvasScene.tsx index d87de34..c9259a6 100644 --- a/public/src/components/CanvasScene.tsx +++ b/public/src/components/CanvasScene.tsx @@ -1,32 +1,10 @@ import { useRef, useEffect, useLayoutEffect, useCallback, useState } from 'react'; import type { AgentSnapshot, ViewState } from '../types'; import { useCanvasRenderer } from '../hooks/useCanvasRenderer'; -import { createLayoutState, updateLayout, getCoordinate } from '../lib/layout'; -import { agentIdentity, keyForAgent } from '../lib/format'; -import { pointInDiamond, pointInQuad, isoToScreen } from '../lib/iso'; +import { createLayoutState, updateLayout } from '../lib/layout'; +import { agentIdentity } from '../lib/format'; import { Tooltip } from './Tooltip'; -const TILE_W = 96; -const TILE_H = 48; -const ROOF_SCALE = 0.28; -const ROOF_HIT_SCALE = 0.44; -const MARKER_SCALE = 0.36; -const MARKER_OFFSET = TILE_H * 0.6; - -interface HitItem { - x: number; - y: number; - roofY: number; - roofW: number; - roofH: number; - roofHitW: number; - roofHitH: number; - height: number; - agent: AgentSnapshot; - key: string; - markerY?: number; -} - interface CanvasSceneProps { agents: AgentSnapshot[]; view: ViewState; @@ -42,21 +20,19 @@ export function CanvasScene({ agents, view, selected, - searchMatches, + searchMatches: _searchMatches, onSelect, onMouseDown, onKeyDown, onWheel, }: CanvasSceneProps) { - const containerRef = useRef(null); const layoutRef = useRef(createLayoutState()); const spawnTimesRef = useRef>(new Map()); const knownIdsRef = useRef>(new Set()); const [hovered, setHovered] = useState(null); const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 }); - const hitListRef = useRef([]); - const { canvasRef, startRender, stopRender } = useCanvasRenderer(); + const { canvasRef, startRender, stopRender, getAgentAtPoint, getHitList } = useCanvasRenderer(); // Update layout and spawn times when agents change useLayoutEffect(() => { @@ -81,99 +57,16 @@ export function CanvasScene({ updateLayout(layoutRef.current, agents); }, [agents]); - // Rebuild hit list whenever agents or view changes - useEffect(() => { - const roofW = TILE_W * ROOF_SCALE; - const roofH = roofW * 0.5; - const roofHitW = TILE_W * ROOF_HIT_SCALE; - const roofHitH = roofHitW * 0.5; - - // Sort by depth (same as render order) - const sortedAgents = [...agents].sort((a, b) => { - const coordA = getCoordinate(layoutRef.current, a) ?? { x: 0, y: 0 }; - const coordB = getCoordinate(layoutRef.current, b) ?? { x: 0, y: 0 }; - return coordA.x + coordA.y - (coordB.x + coordB.y); - }); - - const hitList: HitItem[] = []; - - for (const agent of sortedAgents) { - const coord = getCoordinate(layoutRef.current, agent); - if (!coord) continue; - - const screen = isoToScreen(coord.x, coord.y, TILE_W, TILE_H); - const memMB = agent.mem / (1024 * 1024); - const heightBase = Math.min(120, Math.max(18, memMB * 0.4)); - const idleScale = agent.state === 'idle' ? 0.6 : 1; - const height = heightBase * idleScale; - const roofY = screen.y - height - TILE_H * 0.15; - - hitList.push({ - x: screen.x, - y: screen.y, - roofY, - roofW, - roofH, - roofHitW, - roofHitH, - height, - agent, - key: keyForAgent(agent), - }); - } - - // Calculate obstruction and marker positions - const obstructedIds = new Set(); - for (const a of hitList) { - const roofPoint = { x: a.x, y: a.roofY }; - for (const b of hitList) { - if (a === b) continue; - const topY = b.y - b.height; - const halfW = TILE_W / 2; - const halfH = TILE_H / 2; - - const leftA = { x: b.x - halfW, y: topY }; - const leftB = { x: b.x, y: topY + halfH }; - const leftC = { x: b.x, y: b.y + halfH }; - const leftD = { x: b.x - halfW, y: b.y }; - - const rightA = { x: b.x + halfW, y: topY }; - const rightB = { x: b.x, y: topY + halfH }; - const rightC = { x: b.x, y: b.y + halfH }; - const rightD = { x: b.x + halfW, y: b.y }; - - if ( - pointInQuad(roofPoint, leftA, leftB, leftC, leftD) || - pointInQuad(roofPoint, rightA, rightB, rightC, rightD) - ) { - obstructedIds.add(agentIdentity(a.agent)); - break; - } - } - } - - for (const item of hitList) { - if (obstructedIds.has(agentIdentity(item.agent))) { - item.markerY = item.roofY - MARKER_OFFSET; - } - } - - hitListRef.current = hitList; - }, [agents, view]); - useEffect(() => { const win = window as any; if (!win.__consensusMock) { win.__consensusMock = {}; } - win.__consensusMock.getHitList = () => hitListRef.current; + win.__consensusMock.getHitList = () => getHitList(); win.__consensusMock.getView = () => ({ x: view.x, y: view.y, scale: view.scale }); - }, [view]); + }, [view, getHitList]); - // Start/stop render loop useEffect(() => { - if (!canvasRef.current) return; - startRender(view, agents, { layout: layoutRef.current, hovered, @@ -181,62 +74,19 @@ export function CanvasScene({ spawnTimes: spawnTimesRef.current, deviceScale: window.devicePixelRatio || 1, }); + }, [agents, view, hovered, selected, startRender]); + useEffect(() => { return () => { stopRender(); }; - }, [agents, view, hovered, selected, searchMatches, startRender, stopRender, canvasRef]); - - const findAgentAt = useCallback((canvasX: number, canvasY: number): AgentSnapshot | null => { - // Transform canvas coordinates to world coordinates - const worldX = (canvasX - view.x) / view.scale; - const worldY = (canvasY - view.y) / view.scale; - - const markerW = TILE_W * MARKER_SCALE; - const markerH = markerW * 0.5; - const hitList = hitListRef.current; - - if (!hitList.length) return null; - - // Check markers first (for obstructed agents) - for (let i = hitList.length - 1; i >= 0; i--) { - const item = hitList[i]; - if (!item.markerY) continue; - if (pointInDiamond(worldX, worldY, item.x, item.markerY, markerW, markerH)) { - return item.agent; - } - } - - // Check roofs - for (let i = hitList.length - 1; i >= 0; i--) { - const item = hitList[i]; - if (pointInDiamond(worldX, worldY, item.x, item.roofY, item.roofHitW, item.roofHitH)) { - return item.agent; - } - } - - // Check base tiles - for (let i = hitList.length - 1; i >= 0; i--) { - const item = hitList[i]; - if (pointInDiamond(worldX, worldY, item.x, item.y, TILE_W, TILE_H)) { - return item.agent; - } - } - - return null; - }, [view]); + }, [stopRender]); const handleMouseMove = useCallback((e: React.MouseEvent) => { - const rect = containerRef.current?.getBoundingClientRect(); - if (!rect) return; - - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - const found = findAgentAt(x, y); + const found = getAgentAtPoint(e.clientX, e.clientY); setHovered(found); setTooltipPos({ x: e.clientX + 12, y: e.clientY + 12 }); - }, [findAgentAt]); + }, [getAgentAtPoint]); const handleMouseLeave = useCallback(() => { setHovered(null); @@ -249,18 +99,14 @@ export function CanvasScene({ return; } - // Calculate from click position (for direct clicks without hover) - const rect = (e.target as HTMLCanvasElement).getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - const found = findAgentAt(x, y); + const found = getAgentAtPoint(e.clientX, e.clientY); if (found) { onSelect(found); } - }, [hovered, onSelect, findAgentAt]); + }, [hovered, onSelect, getAgentAtPoint]); return ( -
+
b.x + halfW || + roofPoint.y < topY || + roofPoint.y > b.y + halfH + ) { + continue; + } + const leftA = { x: b.x - halfW, y: topY }; const leftB = { x: b.x, y: topY + halfH }; const leftC = { x: b.x, y: b.y + halfH }; const leftD = { x: b.x - halfW, y: b.y }; - + const rightA = { x: b.x + halfW, y: topY }; const rightB = { x: b.x, y: topY + halfH }; const rightC = { x: b.x, y: b.y + halfH }; const rightD = { x: b.x + halfW, y: b.y }; - + if ( pointInQuad(roofPoint, leftA, leftB, leftC, leftD) || pointInQuad(roofPoint, rightA, rightB, rightC, rightD) @@ -362,6 +372,54 @@ export function useCanvasRenderer() { const hitListRef = useRef([]); const rafRef = useRef(null); const deviceScaleRef = useRef(1); + const ctxRef = useRef(null); + const viewRef = useRef({ x: 0, y: 0, scale: 1 }); + const agentsRef = useRef([]); + const optionsRef = useRef(null); + const reducedMotionRef = useRef(false); + + useEffect(() => { + if (typeof window === 'undefined' || !window.matchMedia) return; + const mql = window.matchMedia('(prefers-reduced-motion: reduce)'); + const update = () => { + reducedMotionRef.current = mql.matches; + }; + update(); + if (mql.addEventListener) { + mql.addEventListener('change', update); + } else { + mql.addListener(update); + } + return () => { + if (mql.removeEventListener) { + mql.removeEventListener('change', update); + } else { + mql.removeListener(update); + } + }; + }, []); + + const syncCanvasSize = useCallback((canvas: HTMLCanvasElement) => { + const dpr = window.devicePixelRatio || 1; + const cssW = window.innerWidth; + const cssH = window.innerHeight; + const nextW = Math.max(1, Math.floor(cssW * dpr)); + const nextH = Math.max(1, Math.floor(cssH * dpr)); + + if ( + canvas.width !== nextW || + canvas.height !== nextH || + deviceScaleRef.current !== dpr + ) { + deviceScaleRef.current = dpr; + canvas.width = nextW; + canvas.height = nextH; + canvas.style.width = `${cssW}px`; + canvas.style.height = `${cssH}px`; + } + + return { width: canvas.width, height: canvas.height, dpr }; + }, []); const startRender = useCallback(( view: ViewState, @@ -371,37 +429,45 @@ export function useCanvasRenderer() { const canvas = canvasRef.current; if (!canvas) return; - const ctx = canvas.getContext('2d'); - if (!ctx) return; + viewRef.current = view; + agentsRef.current = agents; + optionsRef.current = options; + + if (!ctxRef.current) { + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctxRef.current = ctx; + } + + if (rafRef.current) return; - // Update canvas size - deviceScaleRef.current = window.devicePixelRatio || 1; - canvas.width = window.innerWidth * deviceScaleRef.current; - canvas.height = window.innerHeight * deviceScaleRef.current; - canvas.style.width = `${window.innerWidth}px`; - canvas.style.height = `${window.innerHeight}px`; + syncCanvasSize(canvas); const render = () => { - const width = canvas.width; - const height = canvas.height; - + const canvasEl = canvasRef.current; + const ctx = ctxRef.current; + const opts = optionsRef.current; + if (!canvasEl || !ctx || !opts) { + rafRef.current = null; + return; + } + + const { width, height, dpr } = syncCanvasSize(canvasEl); hitListRef.current = renderFrame({ ctx, - view, - agents, - options: { ...options, deviceScale: deviceScaleRef.current }, + view: viewRef.current, + agents: agentsRef.current, + options: { ...opts, deviceScale: dpr }, width, height, + reducedMotion: reducedMotionRef.current, }); - + rafRef.current = requestAnimationFrame(render); }; - if (rafRef.current) { - cancelAnimationFrame(rafRef.current); - } rafRef.current = requestAnimationFrame(render); - }, []); + }, [syncCanvasSize]); const stopRender = useCallback(() => { if (rafRef.current) { @@ -410,19 +476,16 @@ export function useCanvasRenderer() { } }, []); + const getHitList = useCallback(() => hitListRef.current, []); + const getAgentAtPoint = useCallback((screenX: number, screenY: number): AgentSnapshot | null => { const canvas = canvasRef.current; if (!canvas) return null; const rect = canvas.getBoundingClientRect(); - const x = screenX - rect.left; - const y = screenY - rect.top; - - const viewX = (x - canvas.width / 2 / deviceScaleRef.current) / 1; // simplified - const viewY = (y - canvas.height / 2 / deviceScaleRef.current) / 1; - - // Transform to world space (this needs proper view transform) - // For now, return simplified hit detection + const view = viewRef.current; + const worldX = (screenX - rect.left - view.x) / view.scale; + const worldY = (screenY - rect.top - view.y) / view.scale; const hitList = hitListRef.current; if (!hitList.length) return null; @@ -432,7 +495,7 @@ export function useCanvasRenderer() { if (!item.markerY) continue; const markerW = TILE_W * MARKER_SCALE; const markerH = markerW * 0.5; - if (pointInDiamond(viewX, viewY, item.x, item.markerY, markerW, markerH)) { + if (pointInDiamond(worldX, worldY, item.x, item.markerY, markerW, markerH)) { return item.agent; } } @@ -442,7 +505,7 @@ export function useCanvasRenderer() { const item = hitList[i]; const roofHitW = item.roofHitW; const roofHitH = item.roofHitH; - if (pointInDiamond(viewX, viewY, item.x, item.roofY, roofHitW, roofHitH)) { + if (pointInDiamond(worldX, worldY, item.x, item.roofY, roofHitW, roofHitH)) { return item.agent; } } @@ -450,7 +513,7 @@ export function useCanvasRenderer() { // Check base for (let i = hitList.length - 1; i >= 0; i -= 1) { const item = hitList[i]; - if (pointInDiamond(viewX, viewY, item.x, item.y, TILE_W, TILE_H)) { + if (pointInDiamond(worldX, worldY, item.x, item.y, TILE_W, TILE_H)) { return item.agent; } } @@ -469,5 +532,6 @@ export function useCanvasRenderer() { startRender, stopRender, getAgentAtPoint, + getHitList, }; } From 374f06aeb00b41b6741d11ffb7266b5773feb573 Mon Sep 17 00:00:00 2001 From: integrate-your-mind <43152353+integrate-your-mind@users.noreply.github.com> Date: Fri, 30 Jan 2026 02:47:43 -0500 Subject: [PATCH 3/4] chore: update continuity ledger --- CONTINUITY.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTINUITY.md b/CONTINUITY.md index d36e8c7..54c7509 100644 --- a/CONTINUITY.md +++ b/CONTINUITY.md @@ -34,6 +34,7 @@ Key decisions: - React UI is primary; server should serve React build when present. - Agent lane follows vanilla JS behavior: show all open sessions (active + idle) and sort by state rank then CPU. - Layout uses pinned positions per agent identity; only new agents are placed; existing tiles never move; placement uses max-height bounds to prevent overlap. +- Scale placement with spatial hash buckets (cell -> occupants) and persistent per-group spiral frontier; collisions checked by bounds against bucket occupants. - Codex prompt-like titles containing temp paths/turn markers are ignored for lane labels; fallback to repo or codex#pid. - Agent lane now shows only active/error sessions and uses stable sort (state rank then identity) to prevent reordering. - Active lane items now glow via `is-active` class (lane item box-shadow) to make active codex sessions visibly highlighted. @@ -133,6 +134,11 @@ State: - Tests re-run: `npm run test:unit` (149 pass), `npm run test:integration` (54 pass), `npm run test:ui` (19 pass). - Build run after pinned layout changes: `npm run build` (pass). - Copied edited files to clipboard via pbcopy; temp bundle at `/tmp/consensus-edited-files.txt`. + - Implemented spatial hash buckets + persistent group spiral frontier; fixed cell packing and bounds-based collision checks (public/src/lib/layout.ts). + - Renderer now keeps a single RAF loop, exposes hit list, and uses correct view transform for hit tests (public/src/hooks/useCanvasRenderer.ts). + - CanvasScene no longer rebuilds hit list; uses renderer hit list and stable render loop (public/src/components/CanvasScene.tsx). + - Obstruction detection checks only front occluders with AABB guard (public/src/hooks/useCanvasRenderer.ts). + - Tests run after layout/renderer updates: `npm run test:unit` (152 pass), `npm run test:integration` (58 pass), `npm run test:ui` (19 pass). - Now: - Answer user question about hot reload configuration (Vite HMR + server watch + live reload SSE). - Answer whether any additional setup is missing when using Vite. @@ -161,6 +167,7 @@ State: - Claude CLI hooks not firing for real sessions; user reports Claude does not show active when working. - Need to validate live Claude session while recording via agent-browser and inspect hook delivery. - Copy edited layout/test files to clipboard via pbcopy. (DONE) + - Implement spatial hash buckets + persistent group frontier with correct cell packing and bounds checks. (DONE) - User requested a best-practice plan (no code edits) to address review comment: `npm run dev` no longer starts Vite, so TSX client fails unless dev:client or build is run. - Q&A answered for dev workflow: always start Vite; use CONSENSUS_UI_PORT for port; on port conflict try next port. - Review plan requested for new P2/P3 items: Codex in-flight timeout default in codexLogs; TOML [tui] notifications insertion in cli/setup. From 823110dcc5f6454c6eeaa8b249ca48b1ebb2ccfd Mon Sep 17 00:00:00 2001 From: integrate-your-mind <43152353+integrate-your-mind@users.noreply.github.com> Date: Tue, 3 Feb 2026 05:02:25 -0500 Subject: [PATCH 4/4] chore: simplify activity derivation --- public/src/lib/format.ts | 9 +- src/activity.ts | 45 ++++++--- src/activity/machine.ts | 213 +++++++++++++++++++-------------------- 3 files changed, 140 insertions(+), 127 deletions(-) diff --git a/public/src/lib/format.ts b/public/src/lib/format.ts index 955bd8c..16b4a51 100644 --- a/public/src/lib/format.ts +++ b/public/src/lib/format.ts @@ -28,14 +28,7 @@ export function escapeHtml(value: unknown): string { } export function agentIdentity(agent: AgentSnapshot): string { - const identity = agent.identity || agent.sessionPath; - if (identity) return identity; - const kind = typeof agent.kind === 'string' ? agent.kind : ''; - const isServer = kind === 'app-server' || kind === 'opencode-server'; - if (!isServer) { - return agent.id || `${agent.pid}`; - } - return agent.id || `${agent.pid}`; + return agent.identity || agent.sessionPath || agent.id || `${agent.pid}`; } export function groupKeyForAgent(agent: AgentSnapshot): string { diff --git a/src/activity.ts b/src/activity.ts index de99040..1cc7851 100644 --- a/src/activity.ts +++ b/src/activity.ts @@ -14,14 +14,22 @@ export interface ActivityInput { eventWindowMs?: number; } +const getNumber = (value: number | undefined, envKey: string, fallback: number): number => + value ?? Number(process.env[envKey] || fallback); + export function deriveState(input: ActivityInput): AgentState { if (input.hasError) return "error"; const now = input.now ?? Date.now(); - const cpuThreshold = - input.cpuThreshold ?? Number(process.env.CONSENSUS_CPU_ACTIVE || DEFAULT_CPU_THRESHOLD); - const eventWindowMs = - input.eventWindowMs ?? - Number(process.env.CONSENSUS_EVENT_ACTIVE_MS || DEFAULT_EVENT_WINDOW_MS); + const cpuThreshold = getNumber( + input.cpuThreshold, + "CONSENSUS_CPU_ACTIVE", + DEFAULT_CPU_THRESHOLD + ); + const eventWindowMs = getNumber( + input.eventWindowMs, + "CONSENSUS_EVENT_ACTIVE_MS", + DEFAULT_EVENT_WINDOW_MS + ); const cpuActive = input.cpu > cpuThreshold; const eventActive = typeof input.lastEventAt === "number" && @@ -44,17 +52,30 @@ export interface ActivityHoldResult { export function deriveStateWithHold(input: ActivityHoldInput): ActivityHoldResult { const now = input.now ?? Date.now(); - const holdMs = - input.holdMs ?? Number(process.env.CONSENSUS_ACTIVE_HOLD_MS || DEFAULT_ACTIVE_HOLD_MS); - const cpuThreshold = - input.cpuThreshold ?? Number(process.env.CONSENSUS_CPU_ACTIVE || DEFAULT_CPU_THRESHOLD); - const eventWindowMs = - input.eventWindowMs ?? Number(process.env.CONSENSUS_EVENT_ACTIVE_MS || DEFAULT_EVENT_WINDOW_MS); + const holdMs = getNumber( + input.holdMs, + "CONSENSUS_ACTIVE_HOLD_MS", + DEFAULT_ACTIVE_HOLD_MS + ); + const cpuThreshold = getNumber( + input.cpuThreshold, + "CONSENSUS_CPU_ACTIVE", + DEFAULT_CPU_THRESHOLD + ); + const eventWindowMs = getNumber( + input.eventWindowMs, + "CONSENSUS_EVENT_ACTIVE_MS", + DEFAULT_EVENT_WINDOW_MS + ); const cpuActive = input.cpu > cpuThreshold; const eventActive = typeof input.lastEventAt === "number" && now - input.lastEventAt <= eventWindowMs; const inFlight = !!input.inFlight; - const baseState = deriveState({ ...input, now, cpuThreshold, eventWindowMs }); + const baseState: AgentState = input.hasError + ? "error" + : cpuActive || eventActive || inFlight + ? "active" + : "idle"; let reason = "idle"; if (input.hasError) { reason = "error"; diff --git a/src/activity/machine.ts b/src/activity/machine.ts index d949a6c..a99647c 100644 --- a/src/activity/machine.ts +++ b/src/activity/machine.ts @@ -3,7 +3,7 @@ * Provides declarative, priority-ordered state transitions */ -import { Effect, Option } from "effect" +import { Effect } from "effect" import type { ActivityContext, State, StateResult } from "./types.js" // ============================================================================ @@ -70,42 +70,18 @@ const hasInFlightGrace = (ctx: ActivityContext): boolean => * Determine state using pattern matching and effect composition * This replaces nested conditionals with a clear priority order */ -export const deriveState = (ctx: ActivityContext): Effect.Effect => - Effect.gen(function* () { - // Priority 1: Error state (highest) - if (hasError(ctx)) { - return { - state: "error" as State, - reason: "error", - lastActiveAt: ctx.now, - } - } - - // Priority 2: Strict in-flight mode - if (ctx.strictInFlight) { - if (hasInFlightSignal(ctx)) { - return { - state: "active" as State, - reason: "in_flight", - lastActiveAt: ctx.now, - } - } - - if (hasInFlightGrace(ctx)) { - return { - state: "active" as State, - reason: "in_flight_grace", - lastActiveAt: ctx.now, - } - } - - return { - state: "idle" as State, - reason: "no_in_flight", - } +const resolveState = (ctx: ActivityContext): StateResult => { + // Priority 1: Error state (highest) + if (hasError(ctx)) { + return { + state: "error" as State, + reason: "error", + lastActiveAt: ctx.now, } + } - // Priority 3: Active signals (in-flight, CPU spike, recent activity) + // Priority 2: Strict in-flight mode + if (ctx.strictInFlight) { if (hasInFlightSignal(ctx)) { return { state: "active" as State, @@ -114,52 +90,77 @@ export const deriveState = (ctx: ActivityContext): Effect.Effect => } } - if (hasCpuSpike(ctx)) { + if (hasInFlightGrace(ctx)) { return { state: "active" as State, - reason: "cpu_spike", + reason: "in_flight_grace", lastActiveAt: ctx.now, } } - if (hasRecentActivity(ctx)) { - return { - state: "active" as State, - reason: "recent_event", - lastActiveAt: ctx.lastActivityAt, - } + return { + state: "idle" as State, + reason: "no_in_flight", } + } - if (hasSustainedCpu(ctx)) { - return { - state: "active" as State, - reason: "sustained_cpu", - lastActiveAt: ctx.now, - } + // Priority 3: Active signals (in-flight, CPU spike, recent activity) + if (hasInFlightSignal(ctx)) { + return { + state: "active" as State, + reason: "in_flight", + lastActiveAt: ctx.now, } + } - // Priority 4: Hold active state - if (!isHoldExpired(ctx.previousActiveAt, ctx.now, ctx.holdMs)) { - return { - state: "active" as State, - reason: "hold_active", - lastActiveAt: ctx.previousActiveAt, - } + if (hasCpuSpike(ctx)) { + return { + state: "active" as State, + reason: "cpu_spike", + lastActiveAt: ctx.now, } + } - // Default: idle + if (hasRecentActivity(ctx)) { return { - state: "idle" as State, - reason: "no_signal", + state: "active" as State, + reason: "recent_event", + lastActiveAt: ctx.lastActivityAt, } - }) + } + + if (hasSustainedCpu(ctx)) { + return { + state: "active" as State, + reason: "sustained_cpu", + lastActiveAt: ctx.now, + } + } + + // Priority 4: Hold active state + if (!isHoldExpired(ctx.previousActiveAt, ctx.now, ctx.holdMs)) { + return { + state: "active" as State, + reason: "hold_active", + lastActiveAt: ctx.previousActiveAt, + } + } + + // Default: idle + return { + state: "idle" as State, + reason: "no_signal", + } +} + +export const deriveState = (ctx: ActivityContext): Effect.Effect => + Effect.succeed(resolveState(ctx)) /** * Derive state synchronously (for non-Effect contexts) */ export const deriveStateSync = (ctx: ActivityContext): StateResult => { - const runnable = Effect.runSync(deriveState(ctx)) - return runnable + return resolveState(ctx) } // ============================================================================ @@ -188,28 +189,27 @@ export const deriveOpenCodeState = ( status?: string isServer?: boolean } -): Effect.Effect => - Effect.gen(function* () { - const ctx: ActivityContext = { - ...input, - spikeMultiplier: input.spikeMultiplier ?? 10, - spikeMinimum: input.spikeMinimum ?? 25, - sustainMs: input.sustainMs ?? 500, - inFlightGraceMs: input.inFlightGraceMs ?? 0, - strictInFlight: input.strictInFlight ?? true, - } +): Effect.Effect => { + const ctx: ActivityContext = { + ...input, + spikeMultiplier: input.spikeMultiplier ?? 10, + spikeMinimum: input.spikeMinimum ?? 25, + sustainMs: input.sustainMs ?? 500, + inFlightGraceMs: input.inFlightGraceMs ?? 0, + strictInFlight: input.strictInFlight ?? true, + } - // Check for idle status - const statusIsIdle = input.status && /idle|stopped|paused/.test(input.status) - if (statusIsIdle && !hasInFlightSignal(ctx)) { - return { - state: "idle", - reason: "status_idle", - } - } + // Check for idle status + const statusIsIdle = input.status && /idle|stopped|paused/.test(input.status) + if (statusIsIdle && !hasInFlightSignal(ctx)) { + return Effect.succeed({ + state: "idle", + reason: "status_idle", + }) + } - return yield* deriveState(ctx) - }) + return deriveState(ctx) +} /** Claude-specific activity derivation */ export const deriveClaudeState = ( @@ -217,30 +217,29 @@ export const deriveClaudeState = ( Partial> & { startGraceMs?: number } -): Effect.Effect => - Effect.gen(function* () { - const startGraceMs = input.startGraceMs ?? 1200 - const startedRecently = input.lastActivityAt && - (input.now - input.lastActivityAt <= startGraceMs) - - const ctx: ActivityContext = { - ...input, - spikeMultiplier: input.spikeMultiplier ?? 10, - spikeMinimum: input.spikeMinimum ?? 25, - sustainMs: input.sustainMs ?? 1000, - inFlightGraceMs: input.inFlightGraceMs ?? 0, - strictInFlight: input.strictInFlight ?? false, - holdMs: input.holdMs ?? 0, - } +): Effect.Effect => { + const startGraceMs = input.startGraceMs ?? 1200 + const startedRecently = + input.lastActivityAt && input.now - input.lastActivityAt <= startGraceMs - // Grace period after start - if (startedRecently) { - return { - state: "active", - reason: "start_grace", - lastActiveAt: ctx.now, - } - } + const ctx: ActivityContext = { + ...input, + spikeMultiplier: input.spikeMultiplier ?? 10, + spikeMinimum: input.spikeMinimum ?? 25, + sustainMs: input.sustainMs ?? 1000, + inFlightGraceMs: input.inFlightGraceMs ?? 0, + strictInFlight: input.strictInFlight ?? false, + holdMs: input.holdMs ?? 0, + } - return yield* deriveState(ctx) - }) + // Grace period after start + if (startedRecently) { + return Effect.succeed({ + state: "active", + reason: "start_grace", + lastActiveAt: ctx.now, + }) + } + + return deriveState(ctx) +}