agent-ecosystem/packages/agent-graph/src/canvas/draw-processes.ts
Илия 11bb49c53e
feat(graph): force-directed agent graph visualization with kanban-zone task layout
Force-directed graph visualization for agent teams.

Package: @claude-teams/agent-graph (isolated workspace package)
- Space theme: bloom, particles, hex grid, depth stars
- Members as hexagonal nodes with breathing glow
- Tasks as pill cards in kanban columns (todo/wip/done/review/approved) per owner
- Message particles along edges (real-time only)
- Deterministic layout, Figma-style pan, scroll/pinch zoom
- Clean Architecture: ports/adapters/strategies, ES #private classes

Integration: features/agent-graph/ (adapter + overlay + tab)
- Full-screen overlay (Cmd+Shift+G) + Pin as Tab
- Graph button in Team section header
- Frustum culling, zero per-frame allocations, adaptive fps
- Performance overlay via ?perf query param

Also: CI runs on all PR branches, features/CLAUDE.md architecture guide
2026-03-28 12:03:42 +02:00

65 lines
1.9 KiB
TypeScript

/**
* Process node rendering — small circles for running processes.
* NEW — not from agent-flow.
*/
import type { GraphNode } from '../ports/types';
import { COLORS } from '../constants/colors';
import { NODE } from '../constants/canvas-constants';
import { hexWithAlpha, getGlowSprite } from './render-cache';
/**
* Draw all process nodes as small circles.
*/
export function drawProcesses(
ctx: CanvasRenderingContext2D,
nodes: GraphNode[],
time: number,
selectedId: string | null,
hoveredId: string | null,
): void {
for (const node of nodes) {
if (node.kind !== 'process') continue;
const x = node.x ?? 0;
const y = node.y ?? 0;
const r = NODE.radiusProcess;
const isSelected = node.id === selectedId;
const isHovered = node.id === hoveredId;
ctx.save();
ctx.globalAlpha = 0.8;
// Glow — use cached sprite instead of createRadialGradient per frame
const procColor = node.color ?? COLORS.tool_calling;
const glowSprite = getGlowSprite(procColor, r * 2, 0.19, 0);
ctx.drawImage(glowSprite, x - r * 2, y - r * 2);
// Body
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = isSelected ? COLORS.cardBgSelected : COLORS.cardBg;
ctx.fill();
ctx.strokeStyle = hexWithAlpha(procColor, 0.38);
ctx.lineWidth = isSelected ? 2 : 1;
ctx.stroke();
// Spinning ring for active processes
const spinAngle = time * 2;
ctx.beginPath();
ctx.arc(x, y, r + 3, spinAngle, spinAngle + Math.PI * 0.8);
ctx.strokeStyle = hexWithAlpha(procColor, 0.38);
ctx.lineWidth = 1.5;
ctx.stroke();
// Label
ctx.font = '7px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = COLORS.textDim;
const label = node.label.length > 12 ? node.label.slice(0, 12) + '...' : node.label;
ctx.fillText(label, x, y + r + 4);
ctx.restore();
}
}