infinite-agents-public/claude_code_devtools/claude_devtool_11.html

1381 lines
53 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3 Agent Coordination Graph - Claude Code DevTools</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #1a1b26;
--bg-secondary: #24283b;
--bg-tertiary: #414868;
--text-primary: #c0caf5;
--text-secondary: #a9b1d6;
--text-muted: #565f89;
--accent-primary: #7aa2f7;
--accent-secondary: #bb9af7;
--accent-success: #9ece6a;
--accent-warning: #e0af68;
--accent-error: #f7768e;
--border-color: #414868;
--code-bg: #1f2335;
--agent-color: #7aa2f7;
--tool-color: #bb9af7;
--subagent-color: #9ece6a;
--user-color: #e0af68;
}
body {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden;
}
header {
background: var(--bg-secondary);
padding: 2rem;
border-bottom: 2px solid var(--border-color);
}
h1 {
font-size: 2rem;
color: var(--accent-primary);
margin-bottom: 0.5rem;
}
.tagline {
color: var(--text-secondary);
font-size: 1rem;
}
main {
max-width: 1600px;
margin: 0 auto;
padding: 2rem;
}
.controls {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 600;
}
input[type="range"] {
width: 100%;
accent-color: var(--accent-primary);
}
.range-value {
color: var(--accent-primary);
font-size: 0.85rem;
text-align: right;
}
button {
background: var(--accent-primary);
color: var(--bg-primary);
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
font-family: inherit;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: var(--accent-secondary);
transform: translateY(-1px);
}
button.secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
}
button.secondary:hover {
background: var(--border-color);
}
.button-group {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.graph-container {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
margin-bottom: 2rem;
position: relative;
}
#graph {
width: 100%;
height: 700px;
border-radius: 4px;
background: var(--code-bg);
}
.legend {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.legend h3 {
color: var(--accent-primary);
margin-bottom: 1rem;
font-size: 1.1rem;
}
.legend-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.legend-section {
background: var(--code-bg);
padding: 1rem;
border-radius: 4px;
}
.legend-section h4 {
color: var(--accent-secondary);
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
font-size: 0.85rem;
}
.legend-circle {
width: 16px;
height: 16px;
border-radius: 50%;
flex-shrink: 0;
}
.legend-line {
width: 30px;
height: 2px;
flex-shrink: 0;
}
.tooltip {
position: absolute;
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 6px;
padding: 1rem;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
max-width: 350px;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
.tooltip.show {
opacity: 1;
}
.tooltip-title {
color: var(--accent-primary);
font-weight: 600;
font-size: 1rem;
margin-bottom: 0.5rem;
}
.tooltip-content {
color: var(--text-secondary);
font-size: 0.85rem;
line-height: 1.6;
}
.tooltip-detail {
display: flex;
justify-content: space-between;
margin-bottom: 0.25rem;
}
.tooltip-label {
color: var(--text-muted);
}
.tooltip-value {
color: var(--accent-success);
font-weight: 600;
}
.timeline-controls {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.timeline-controls h3 {
color: var(--accent-primary);
margin-bottom: 1rem;
font-size: 1.1rem;
}
.timeline-slider {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.timeline-slider input[type="range"] {
flex: 1;
}
.timeline-label {
color: var(--text-secondary);
font-size: 0.85rem;
min-width: 100px;
text-align: right;
}
.playback-controls {
display: flex;
gap: 0.75rem;
align-items: center;
}
.stats-panel {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
.stat-item {
background: var(--code-bg);
padding: 1rem;
border-radius: 4px;
text-align: center;
}
.stat-value {
color: var(--accent-primary);
font-size: 2rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.stat-label {
color: var(--text-muted);
font-size: 0.85rem;
}
.docs {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 2rem;
}
.docs h2 {
color: var(--accent-primary);
margin-bottom: 1.5rem;
font-size: 1.5rem;
}
.docs h3 {
color: var(--accent-secondary);
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-size: 1.1rem;
}
.docs p {
color: var(--text-secondary);
margin-bottom: 1rem;
}
.docs ul, .docs ol {
color: var(--text-secondary);
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.docs li {
margin-bottom: 0.5rem;
}
.docs code {
background: var(--code-bg);
color: var(--accent-success);
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9rem;
}
.docs strong {
color: var(--accent-primary);
}
.docs a {
color: var(--accent-primary);
text-decoration: none;
}
.docs a:hover {
text-decoration: underline;
}
footer {
text-align: center;
padding: 2rem;
color: var(--text-muted);
font-size: 0.85rem;
border-top: 1px solid var(--border-color);
}
footer a {
color: var(--accent-primary);
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
.node {
cursor: pointer;
stroke-width: 2;
}
.link {
stroke-opacity: 0.6;
}
.node-label {
font-size: 11px;
pointer-events: none;
text-anchor: middle;
fill: var(--text-primary);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.node.active {
animation: pulse 1.5s ease-in-out infinite;
}
.file-input-wrapper {
position: relative;
display: inline-block;
}
.file-input-wrapper input[type="file"] {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
input[type="file"]::-webkit-file-upload-button {
visibility: hidden;
}
.ws-status {
display: inline-block;
padding: 0.5rem 1rem;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 600;
}
.ws-status.connected {
background: var(--accent-success);
color: var(--bg-primary);
}
.ws-status.disconnected {
background: var(--accent-error);
color: var(--text-primary);
}
</style>
</head>
<body>
<header>
<h1>D3 Agent Coordination Graph</h1>
<p class="tagline">Force-directed visualization of multi-agent relationships, tool usage, and coordination patterns</p>
</header>
<main>
<section class="controls">
<div class="control-group">
<label for="charge-strength">Node Repulsion</label>
<input type="range" id="charge-strength" min="-500" max="-50" value="-150" step="10">
<div class="range-value" id="charge-value">-150</div>
</div>
<div class="control-group">
<label for="link-distance">Link Distance</label>
<input type="range" id="link-distance" min="30" max="200" value="80" step="10">
<div class="range-value" id="link-value">80</div>
</div>
<div class="control-group">
<label for="link-strength">Link Strength</label>
<input type="range" id="link-strength" min="0" max="1" value="0.5" step="0.1">
<div class="range-value" id="strength-value">0.5</div>
</div>
<div class="control-group">
<label>Data Source</label>
<div class="button-group">
<button onclick="loadSampleData()">Sample Data</button>
<div class="file-input-wrapper">
<button class="secondary">Import Events</button>
<input type="file" id="file-input" accept=".json,.jsonl" onchange="handleFileImport(event)">
</div>
</div>
</div>
<div class="control-group">
<label>Graph Actions</label>
<div class="button-group">
<button class="secondary" onclick="resetSimulation()">Reset Layout</button>
<button class="secondary" onclick="clearGraph()">Clear</button>
</div>
</div>
<div class="control-group">
<label>WebSocket Connection</label>
<div id="ws-status" class="ws-status disconnected">Disconnected</div>
<button class="secondary" onclick="toggleWebSocket()" id="ws-toggle">Connect</button>
</div>
</section>
<section class="stats-panel">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value" id="stat-nodes">0</div>
<div class="stat-label">Total Nodes</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-agents">0</div>
<div class="stat-label">Agents</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-tools">0</div>
<div class="stat-label">Tools</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-subagents">0</div>
<div class="stat-label">Subagents</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-links">0</div>
<div class="stat-label">Connections</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-events">0</div>
<div class="stat-label">Events</div>
</div>
</div>
</section>
<section class="timeline-controls">
<h3>Timeline Replay</h3>
<div class="timeline-slider">
<span class="timeline-label">Event 0 / 0</span>
<input type="range" id="timeline-slider" min="0" max="0" value="0" step="1">
</div>
<div class="playback-controls">
<button onclick="playTimeline()">▶ Play</button>
<button class="secondary" onclick="pauseTimeline()">⏸ Pause</button>
<button class="secondary" onclick="resetTimeline()">⏮ Reset</button>
<div class="control-group" style="flex: 1;">
<label for="playback-speed">Speed</label>
<input type="range" id="playback-speed" min="100" max="2000" value="500" step="100">
<div class="range-value" id="speed-value">500ms</div>
</div>
</div>
</section>
<section class="graph-container">
<svg id="graph"></svg>
<div class="tooltip" id="tooltip">
<div class="tooltip-title" id="tooltip-title"></div>
<div class="tooltip-content" id="tooltip-content"></div>
</div>
</section>
<section class="legend">
<h3>Legend</h3>
<div class="legend-grid">
<div class="legend-section">
<h4>Node Types</h4>
<div class="legend-item">
<div class="legend-circle" style="background: var(--agent-color);"></div>
<span>Agent Session</span>
</div>
<div class="legend-item">
<div class="legend-circle" style="background: var(--tool-color);"></div>
<span>Tool</span>
</div>
<div class="legend-item">
<div class="legend-circle" style="background: var(--subagent-color);"></div>
<span>Subagent</span>
</div>
<div class="legend-item">
<div class="legend-circle" style="background: var(--user-color);"></div>
<span>User Prompt</span>
</div>
</div>
<div class="legend-section">
<h4>Link Types</h4>
<div class="legend-item">
<div class="legend-line" style="background: var(--accent-primary);"></div>
<span>Uses Tool</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: var(--accent-success);"></div>
<span>Spawns Subagent</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: var(--accent-warning); opacity: 0.5;"></div>
<span>Temporal Sequence</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: var(--accent-error);"></div>
<span>Error Propagation</span>
</div>
</div>
<div class="legend-section">
<h4>Node Size</h4>
<p style="font-size: 0.85rem; color: var(--text-muted);">
Node size represents activity level (event count). Larger nodes indicate more events/interactions.
</p>
</div>
<div class="legend-section">
<h4>Interactions</h4>
<p style="font-size: 0.85rem; color: var(--text-muted);">
• Drag nodes to reposition<br>
• Scroll to zoom in/out<br>
• Hover for details<br>
• Click to focus
</p>
</div>
</div>
</section>
<section class="docs">
<h2>About This Tool</h2>
<h3>Purpose</h3>
<p>The D3 Agent Coordination Graph visualizes the complex relationships between Claude Code agents, tools, and subagents in multi-agent systems. It uses D3's force-directed graph layout to reveal coordination patterns, tool usage frequencies, and agent spawning hierarchies through an interactive network visualization.</p>
<h3>Features</h3>
<ul>
<li><strong>Force-Directed Layout</strong> - Automatic physics-based node positioning with configurable forces</li>
<li><strong>Interactive Node Dragging</strong> - Manually reposition nodes to explore relationships</li>
<li><strong>Zoom & Pan</strong> - Navigate large coordination graphs with smooth zoom and pan</li>
<li><strong>Real-time Updates</strong> - Optional WebSocket connection for live event streaming</li>
<li><strong>Timeline Replay</strong> - Step through events to see graph evolution over time</li>
<li><strong>Multi-Source Data</strong> - Import from files or connect to live observability server</li>
<li><strong>Activity-Based Sizing</strong> - Node size reflects event count and usage frequency</li>
<li><strong>Color-Coded Entities</strong> - Visual distinction between agents, tools, and subagents</li>
<li><strong>Rich Tooltips</strong> - Detailed information on hover with event counts and metadata</li>
<li><strong>Configurable Physics</strong> - Adjust repulsion, link distance, and connection strength</li>
</ul>
<h3>Web Research Integration</h3>
<p><strong>Source:</strong> <a href="https://observablehq.com/@d3/force-directed-graph" target="_blank">D3 Force-Directed Graph - Observable</a></p>
<p><strong>Techniques Applied:</strong></p>
<ul>
<li><strong>Force Simulation Setup</strong> - Implemented <code>d3.forceSimulation()</code> with <code>forceLink()</code> for connection strength, <code>forceManyBody()</code> for node repulsion, and <code>forceCenter()</code> for centering. Each force is independently configurable via UI controls.</li>
<li><strong>Node Drag Behavior</strong> - Applied <code>d3.drag()</code> with three event handlers: <code>dragstarted</code> fixes node position and restarts simulation alpha, <code>dragged</code> updates coordinates in real-time, and <code>dragended</code> releases constraints while allowing simulation to settle.</li>
<li><strong>Tick-Based Animation</strong> - Utilized simulation's <code>tick</code> event to continuously update SVG element positions (links via <code>x1, y1, x2, y2</code> attributes and nodes via transform translation), creating smooth physics-based animation.</li>
</ul>
<h3>Graph Data Structure</h3>
<p>The visualization processes Claude Code hook events into a graph structure:</p>
<ul>
<li><strong>Nodes</strong> - Represent entities: Agent sessions (source_app + session_id), Tools (from PreToolUse/PostToolUse), Subagents (from SubagentStop), User prompts (from UserPromptSubmit)</li>
<li><strong>Links</strong> - Represent relationships: Agent→Tool (usage), Agent→Subagent (spawning), Event→Event (temporal sequence), Agent→Error (error propagation)</li>
<li><strong>Node Properties</strong> - id (unique identifier), type (agent/tool/subagent/user), label (display name), eventCount (activity level), metadata (additional context)</li>
<li><strong>Link Properties</strong> - source (from node), target (to node), type (relationship kind), strength (connection weight), events (supporting events)</li>
</ul>
<h3>Usage</h3>
<ol>
<li><strong>Load Data</strong> - Click "Sample Data" to load example multi-agent scenario, or use "Import Events" to load .json/.jsonl hook event files</li>
<li><strong>Adjust Physics</strong> - Tune node repulsion, link distance, and link strength sliders to optimize layout for your data size</li>
<li><strong>Explore Graph</strong> - Drag nodes to reposition, scroll to zoom, hover for tooltips showing event counts and details</li>
<li><strong>Timeline Replay</strong> - Use timeline slider to step through events chronologically, or click Play to auto-replay graph evolution</li>
<li><strong>Connect Live</strong> - Click "Connect" under WebSocket to stream real-time events from observability server (requires server at localhost:4000)</li>
<li><strong>Analyze Patterns</strong> - Identify heavily-used tools (large purple nodes), agent hierarchies (green subagent connections), and coordination bottlenecks</li>
</ol>
<h3>Hook Event Mapping</h3>
<p>The tool processes these Claude Code hook events:</p>
<ul>
<li><strong>PreToolUse</strong> - Creates Agent→Tool link, increases tool activity count</li>
<li><strong>PostToolUse</strong> - Updates link with success/error status, adds error propagation links on failure</li>
<li><strong>SubagentStop</strong> - Creates Subagent node and Agent→Subagent spawn link</li>
<li><strong>UserPromptSubmit</strong> - Creates User node and Agent→User interaction link</li>
<li><strong>Notification</strong> - Creates coordination checkpoint markers in timeline</li>
</ul>
<h3>Sample Data Scenario</h3>
<p>The included sample data demonstrates a realistic multi-agent workflow:</p>
<ul>
<li>Main agent (infinite-agents session) orchestrates the workflow</li>
<li>Uses Read, Write, Bash, and Grep tools extensively</li>
<li>Spawns 3 parallel subagents for concurrent code generation tasks</li>
<li>Each subagent uses different tool combinations (Read+Write, Grep+Edit, Bash+Write)</li>
<li>User provides 2 prompts that trigger agent coordination</li>
<li>One subagent encounters an error, creating error propagation path</li>
</ul>
<h3>WebSocket Integration</h3>
<p>Connect to the Claude Code hooks observability server for real-time updates:</p>
<ol>
<li>Start the observability server: <code>./scripts/start-system.sh</code></li>
<li>Click "Connect" in the WebSocket Connection section</li>
<li>Graph will update automatically as new events arrive</li>
<li>New nodes animate in with transitions, links strengthen with repeated usage</li>
<li>Compatible with the multi-agent observability system architecture</li>
</ol>
</section>
</main>
<footer>
<p>Claude Code DevTools | D3 Agent Coordination Graph (Iteration 11)</p>
<p>Web Source: <a href="https://observablehq.com/@d3/force-directed-graph" target="_blank">D3 Force-Directed Graph - Observable</a></p>
</footer>
<script>
// Graph state
let nodes = [];
let links = [];
let allEvents = [];
let simulation;
let svg;
let g;
let nodeElements;
let linkElements;
let labelElements;
let zoom;
let wsConnection = null;
let timelineInterval = null;
let currentTimelineIndex = 0;
// Color mapping
const nodeColors = {
agent: 'var(--agent-color)',
tool: 'var(--tool-color)',
subagent: 'var(--subagent-color)',
user: 'var(--user-color)'
};
const linkColors = {
uses: 'var(--accent-primary)',
spawns: 'var(--accent-success)',
temporal: 'var(--accent-warning)',
error: 'var(--accent-error)'
};
// Initialize SVG
function initializeSVG() {
const container = document.getElementById('graph');
const width = container.clientWidth;
const height = container.clientHeight;
svg = d3.select('#graph')
.attr('width', width)
.attr('height', height);
// Clear existing content
svg.selectAll('*').remove();
// Add zoom behavior
zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
svg.call(zoom);
// Create container group for zoom/pan
g = svg.append('g');
// Initialize force simulation
initializeSimulation();
}
// Initialize D3 force simulation
function initializeSimulation() {
const width = document.getElementById('graph').clientWidth;
const height = document.getElementById('graph').clientHeight;
simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links)
.id(d => d.id)
.distance(parseFloat(document.getElementById('link-distance').value))
.strength(parseFloat(document.getElementById('link-strength').value)))
.force('charge', d3.forceManyBody()
.strength(parseFloat(document.getElementById('charge-strength').value)))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(d => getNodeRadius(d) + 5))
.on('tick', ticked);
}
// Tick function - updates positions on each simulation step
function ticked() {
if (linkElements) {
linkElements
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
}
if (nodeElements) {
nodeElements
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
if (labelElements) {
labelElements
.attr('x', d => d.x)
.attr('y', d => d.y + 4);
}
}
// Get node radius based on activity
function getNodeRadius(node) {
const baseSize = 8;
const activityMultiplier = Math.sqrt(node.eventCount || 1);
return baseSize + activityMultiplier * 2;
}
// Render the graph
function renderGraph() {
// Remove existing elements
g.selectAll('*').remove();
// Create links
linkElements = g.append('g')
.selectAll('line')
.data(links)
.join('line')
.attr('class', 'link')
.attr('stroke', d => linkColors[d.type] || 'var(--border-color)')
.attr('stroke-width', d => Math.sqrt(d.strength || 1) * 2)
.attr('stroke-opacity', d => d.type === 'temporal' ? 0.3 : 0.6);
// Create nodes
nodeElements = g.append('g')
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('class', 'node')
.attr('r', d => getNodeRadius(d))
.attr('fill', d => nodeColors[d.type] || 'var(--text-muted)')
.attr('stroke', d => d.active ? 'var(--accent-warning)' : 'var(--bg-primary)')
.call(dragBehavior())
.on('mouseover', handleNodeMouseOver)
.on('mouseout', handleNodeMouseOut)
.on('click', handleNodeClick);
// Create labels
labelElements = g.append('g')
.selectAll('text')
.data(nodes)
.join('text')
.attr('class', 'node-label')
.text(d => d.label)
.style('font-size', d => `${Math.max(10, getNodeRadius(d))}px`);
// Update simulation
simulation.nodes(nodes);
simulation.force('link').links(links);
simulation.alpha(1).restart();
// Update stats
updateStats();
}
// Drag behavior implementation
function dragBehavior() {
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
}
// Handle node mouse over
function handleNodeMouseOver(event, d) {
const tooltip = document.getElementById('tooltip');
const title = document.getElementById('tooltip-title');
const content = document.getElementById('tooltip-content');
title.textContent = d.label;
let details = `
<div class="tooltip-detail">
<span class="tooltip-label">Type:</span>
<span class="tooltip-value">${d.type}</span>
</div>
<div class="tooltip-detail">
<span class="tooltip-label">Events:</span>
<span class="tooltip-value">${d.eventCount || 0}</span>
</div>
`;
if (d.metadata) {
Object.entries(d.metadata).forEach(([key, value]) => {
details += `
<div class="tooltip-detail">
<span class="tooltip-label">${key}:</span>
<span class="tooltip-value">${value}</span>
</div>
`;
});
}
content.innerHTML = details;
tooltip.style.left = `${event.pageX + 10}px`;
tooltip.style.top = `${event.pageY + 10}px`;
tooltip.classList.add('show');
// Highlight connected nodes
d3.select(event.currentTarget).attr('stroke-width', 4);
}
// Handle node mouse out
function handleNodeMouseOut(event, d) {
const tooltip = document.getElementById('tooltip');
tooltip.classList.remove('show');
d3.select(event.currentTarget).attr('stroke-width', 2);
}
// Handle node click
function handleNodeClick(event, d) {
console.log('Node clicked:', d);
// Highlight connected links
linkElements
.attr('stroke-opacity', link =>
(link.source.id === d.id || link.target.id === d.id) ? 1 : 0.2
);
// Highlight connected nodes
const connectedIds = new Set();
links.forEach(link => {
if (link.source.id === d.id) connectedIds.add(link.target.id);
if (link.target.id === d.id) connectedIds.add(link.source.id);
});
nodeElements
.attr('opacity', node =>
node.id === d.id || connectedIds.has(node.id) ? 1 : 0.3
);
labelElements
.attr('opacity', node =>
node.id === d.id || connectedIds.has(node.id) ? 1 : 0.3
);
}
// Update statistics
function updateStats() {
document.getElementById('stat-nodes').textContent = nodes.length;
document.getElementById('stat-agents').textContent =
nodes.filter(n => n.type === 'agent').length;
document.getElementById('stat-tools').textContent =
nodes.filter(n => n.type === 'tool').length;
document.getElementById('stat-subagents').textContent =
nodes.filter(n => n.type === 'subagent').length;
document.getElementById('stat-links').textContent = links.length;
document.getElementById('stat-events').textContent = allEvents.length;
}
// Process events into graph structure
function processEvents(events) {
const nodeMap = new Map();
const linkMap = new Map();
events.forEach((event, index) => {
const sessionKey = `${event.source_app}-${event.session_id}`;
// Create/update agent node
if (!nodeMap.has(sessionKey)) {
nodeMap.set(sessionKey, {
id: sessionKey,
type: 'agent',
label: event.source_app || 'Agent',
eventCount: 0,
metadata: { session: event.session_id }
});
}
nodeMap.get(sessionKey).eventCount++;
// Process based on event type
if (event.hook_event_type === 'PreToolUse' || event.hook_event_type === 'PostToolUse') {
const toolName = event.payload?.tool_name || 'Unknown';
const toolKey = `tool-${toolName}`;
// Create/update tool node
if (!nodeMap.has(toolKey)) {
nodeMap.set(toolKey, {
id: toolKey,
type: 'tool',
label: toolName,
eventCount: 0,
metadata: {}
});
}
nodeMap.get(toolKey).eventCount++;
// Create agent->tool link
const linkKey = `${sessionKey}-${toolKey}`;
if (!linkMap.has(linkKey)) {
linkMap.set(linkKey, {
source: sessionKey,
target: toolKey,
type: 'uses',
strength: 0,
events: []
});
}
linkMap.get(linkKey).strength++;
linkMap.get(linkKey).events.push(event);
// Check for errors
if (event.hook_event_type === 'PostToolUse' && event.payload?.error) {
const errorKey = `error-${index}`;
nodeMap.set(errorKey, {
id: errorKey,
type: 'error',
label: 'Error',
eventCount: 1,
metadata: { message: event.payload.error }
});
linkMap.set(`${sessionKey}-${errorKey}`, {
source: sessionKey,
target: errorKey,
type: 'error',
strength: 1,
events: [event]
});
}
} else if (event.hook_event_type === 'SubagentStop') {
const subagentKey = `subagent-${event.session_id}-${index}`;
nodeMap.set(subagentKey, {
id: subagentKey,
type: 'subagent',
label: event.payload?.task || 'Subagent',
eventCount: 1,
metadata: { parent: sessionKey }
});
linkMap.set(`${sessionKey}-${subagentKey}`, {
source: sessionKey,
target: subagentKey,
type: 'spawns',
strength: 1,
events: [event]
});
} else if (event.hook_event_type === 'UserPromptSubmit') {
const userKey = `user-${index}`;
nodeMap.set(userKey, {
id: userKey,
type: 'user',
label: 'User Prompt',
eventCount: 1,
metadata: { prompt: event.payload?.prompt?.substring(0, 50) + '...' }
});
linkMap.set(`${userKey}-${sessionKey}`, {
source: userKey,
target: sessionKey,
type: 'temporal',
strength: 1,
events: [event]
});
}
});
return {
nodes: Array.from(nodeMap.values()),
links: Array.from(linkMap.values())
};
}
// Load sample data
function loadSampleData() {
allEvents = [
// Main agent session starts
{ source_app: 'infinite-agents', session_id: 'main-001', hook_event_type: 'UserPromptSubmit',
payload: { prompt: 'Generate 3 variations of the UI component' }, timestamp: '2025-10-09T10:00:00Z' },
// Main agent uses Read tool
{ source_app: 'infinite-agents', session_id: 'main-001', hook_event_type: 'PreToolUse',
payload: { tool_name: 'Read', tool_input: { file: 'specs/ui_spec.md' } }, timestamp: '2025-10-09T10:00:05Z' },
{ source_app: 'infinite-agents', session_id: 'main-001', hook_event_type: 'PostToolUse',
payload: { tool_name: 'Read' }, timestamp: '2025-10-09T10:00:06Z' },
// Main agent spawns 3 subagents
{ source_app: 'infinite-agents', session_id: 'sub-001', hook_event_type: 'SubagentStop',
payload: { task: 'Generate variant 1', parent_session: 'main-001' }, timestamp: '2025-10-09T10:00:10Z' },
{ source_app: 'infinite-agents', session_id: 'sub-002', hook_event_type: 'SubagentStop',
payload: { task: 'Generate variant 2', parent_session: 'main-001' }, timestamp: '2025-10-09T10:00:11Z' },
{ source_app: 'infinite-agents', session_id: 'sub-003', hook_event_type: 'SubagentStop',
payload: { task: 'Generate variant 3', parent_session: 'main-001' }, timestamp: '2025-10-09T10:00:12Z' },
// Subagent 1 uses tools
{ source_app: 'infinite-agents', session_id: 'sub-001', hook_event_type: 'PreToolUse',
payload: { tool_name: 'Read', tool_input: { file: 'templates/base.html' } }, timestamp: '2025-10-09T10:00:15Z' },
{ source_app: 'infinite-agents', session_id: 'sub-001', hook_event_type: 'PostToolUse',
payload: { tool_name: 'Read' }, timestamp: '2025-10-09T10:00:16Z' },
{ source_app: 'infinite-agents', session_id: 'sub-001', hook_event_type: 'PreToolUse',
payload: { tool_name: 'Write', tool_input: { file: 'output/variant1.html' } }, timestamp: '2025-10-09T10:00:20Z' },
{ source_app: 'infinite-agents', session_id: 'sub-001', hook_event_type: 'PostToolUse',
payload: { tool_name: 'Write' }, timestamp: '2025-10-09T10:00:21Z' },
// Subagent 2 uses tools
{ source_app: 'infinite-agents', session_id: 'sub-002', hook_event_type: 'PreToolUse',
payload: { tool_name: 'Grep', tool_input: { pattern: 'class=' } }, timestamp: '2025-10-09T10:00:15Z' },
{ source_app: 'infinite-agents', session_id: 'sub-002', hook_event_type: 'PostToolUse',
payload: { tool_name: 'Grep' }, timestamp: '2025-10-09T10:00:17Z' },
{ source_app: 'infinite-agents', session_id: 'sub-002', hook_event_type: 'PreToolUse',
payload: { tool_name: 'Edit', tool_input: { file: 'output/variant2.html' } }, timestamp: '2025-10-09T10:00:22Z' },
{ source_app: 'infinite-agents', session_id: 'sub-002', hook_event_type: 'PostToolUse',
payload: { tool_name: 'Edit' }, timestamp: '2025-10-09T10:00:23Z' },
// Subagent 3 uses tools (with error)
{ source_app: 'infinite-agents', session_id: 'sub-003', hook_event_type: 'PreToolUse',
payload: { tool_name: 'Bash', tool_input: { command: 'npm test' } }, timestamp: '2025-10-09T10:00:16Z' },
{ source_app: 'infinite-agents', session_id: 'sub-003', hook_event_type: 'PostToolUse',
payload: { tool_name: 'Bash', error: 'Command failed' }, timestamp: '2025-10-09T10:00:18Z' },
{ source_app: 'infinite-agents', session_id: 'sub-003', hook_event_type: 'PreToolUse',
payload: { tool_name: 'Write', tool_input: { file: 'output/variant3.html' } }, timestamp: '2025-10-09T10:00:25Z' },
{ source_app: 'infinite-agents', session_id: 'sub-003', hook_event_type: 'PostToolUse',
payload: { tool_name: 'Write' }, timestamp: '2025-10-09T10:00:26Z' },
// Main agent collects results
{ source_app: 'infinite-agents', session_id: 'main-001', hook_event_type: 'PreToolUse',
payload: { tool_name: 'Bash', tool_input: { command: 'ls output/' } }, timestamp: '2025-10-09T10:00:30Z' },
{ source_app: 'infinite-agents', session_id: 'main-001', hook_event_type: 'PostToolUse',
payload: { tool_name: 'Bash' }, timestamp: '2025-10-09T10:00:31Z' },
// User reviews results
{ source_app: 'infinite-agents', session_id: 'main-001', hook_event_type: 'UserPromptSubmit',
payload: { prompt: 'Looks good! Generate 2 more variations based on variant 1' }, timestamp: '2025-10-09T10:01:00Z' }
];
const graph = processEvents(allEvents);
nodes = graph.nodes;
links = graph.links;
// Setup timeline
const timelineSlider = document.getElementById('timeline-slider');
timelineSlider.max = allEvents.length;
timelineSlider.value = allEvents.length;
currentTimelineIndex = allEvents.length;
updateTimelineLabel();
renderGraph();
}
// Handle file import
function handleFileImport(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
const events = file.name.endsWith('.jsonl')
? content.split('\n').filter(l => l.trim()).map(l => JSON.parse(l))
: JSON.parse(content);
allEvents = Array.isArray(events) ? events : [events];
const graph = processEvents(allEvents);
nodes = graph.nodes;
links = graph.links;
const timelineSlider = document.getElementById('timeline-slider');
timelineSlider.max = allEvents.length;
timelineSlider.value = allEvents.length;
currentTimelineIndex = allEvents.length;
updateTimelineLabel();
renderGraph();
} catch (error) {
alert('Error parsing file: ' + error.message);
}
};
reader.readAsText(file);
}
// Reset simulation
function resetSimulation() {
if (simulation) {
// Clear fixed positions
nodes.forEach(node => {
node.fx = null;
node.fy = null;
});
simulation.alpha(1).restart();
}
}
// Clear graph
function clearGraph() {
nodes = [];
links = [];
allEvents = [];
currentTimelineIndex = 0;
renderGraph();
}
// Update timeline label
function updateTimelineLabel() {
const slider = document.getElementById('timeline-slider');
const label = document.querySelector('.timeline-label');
label.textContent = `Event ${currentTimelineIndex} / ${allEvents.length}`;
}
// Timeline controls
function playTimeline() {
if (timelineInterval) return;
const speed = parseInt(document.getElementById('playback-speed').value);
timelineInterval = setInterval(() => {
if (currentTimelineIndex >= allEvents.length) {
pauseTimeline();
return;
}
currentTimelineIndex++;
const slider = document.getElementById('timeline-slider');
slider.value = currentTimelineIndex;
updateTimelineFromSlider();
}, speed);
}
function pauseTimeline() {
if (timelineInterval) {
clearInterval(timelineInterval);
timelineInterval = null;
}
}
function resetTimeline() {
pauseTimeline();
currentTimelineIndex = 0;
const slider = document.getElementById('timeline-slider');
slider.value = 0;
updateTimelineFromSlider();
}
function updateTimelineFromSlider() {
const slider = document.getElementById('timeline-slider');
currentTimelineIndex = parseInt(slider.value);
updateTimelineLabel();
const eventsUpToNow = allEvents.slice(0, currentTimelineIndex);
const graph = processEvents(eventsUpToNow);
nodes = graph.nodes;
links = graph.links;
renderGraph();
}
// WebSocket connection
function toggleWebSocket() {
const button = document.getElementById('ws-toggle');
const status = document.getElementById('ws-status');
if (wsConnection) {
wsConnection.close();
wsConnection = null;
button.textContent = 'Connect';
status.textContent = 'Disconnected';
status.className = 'ws-status disconnected';
} else {
try {
wsConnection = new WebSocket('ws://localhost:4000/stream');
wsConnection.onopen = () => {
button.textContent = 'Disconnect';
status.textContent = 'Connected';
status.className = 'ws-status connected';
};
wsConnection.onmessage = (event) => {
try {
const newEvent = JSON.parse(event.data);
allEvents.push(newEvent);
const graph = processEvents(allEvents);
nodes = graph.nodes;
links = graph.links;
const timelineSlider = document.getElementById('timeline-slider');
timelineSlider.max = allEvents.length;
currentTimelineIndex = allEvents.length;
updateTimelineLabel();
renderGraph();
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
};
wsConnection.onerror = (error) => {
console.error('WebSocket error:', error);
alert('WebSocket connection failed. Make sure the server is running at localhost:4000');
};
wsConnection.onclose = () => {
wsConnection = null;
button.textContent = 'Connect';
status.textContent = 'Disconnected';
status.className = 'ws-status disconnected';
};
} catch (error) {
alert('Failed to connect: ' + error.message);
}
}
}
// Control panel event listeners
document.getElementById('charge-strength').addEventListener('input', (e) => {
document.getElementById('charge-value').textContent = e.target.value;
if (simulation) {
simulation.force('charge').strength(parseFloat(e.target.value));
simulation.alpha(0.3).restart();
}
});
document.getElementById('link-distance').addEventListener('input', (e) => {
document.getElementById('link-value').textContent = e.target.value;
if (simulation) {
simulation.force('link').distance(parseFloat(e.target.value));
simulation.alpha(0.3).restart();
}
});
document.getElementById('link-strength').addEventListener('input', (e) => {
document.getElementById('strength-value').textContent = e.target.value;
if (simulation) {
simulation.force('link').strength(parseFloat(e.target.value));
simulation.alpha(0.3).restart();
}
});
document.getElementById('playback-speed').addEventListener('input', (e) => {
document.getElementById('speed-value').textContent = e.target.value + 'ms';
if (timelineInterval) {
pauseTimeline();
playTimeline();
}
});
document.getElementById('timeline-slider').addEventListener('input', updateTimelineFromSlider);
// Initialize on load
window.addEventListener('load', () => {
initializeSVG();
loadSampleData();
});
// Handle window resize
window.addEventListener('resize', () => {
const container = document.getElementById('graph');
const width = container.clientWidth;
const height = container.clientHeight;
svg.attr('width', width).attr('height', height);
if (simulation) {
simulation.force('center', d3.forceCenter(width / 2, height / 2));
simulation.alpha(0.3).restart();
}
});
</script>
</body>
</html>