ModsCode / Gallery / Designs / Subagent swimlanes
Subagent swimlanes
The main agent and its subagents as lanes over time: tool calls are segments on each lane, a failed call is red and a running call stays open to the NOW line.


hooks/designs/agent-lanes.mjs
// agent-lanes: one Claude Code task as swimlanes, the main agent on top and five subagents below it in
// the order they were spawned, over a 12-minute window. Each lane's life is gantt's 8px track at 14% ink
// and every tool call is a segment on it at candle's 55% fill, so the busy stretches read as solid ink
// and the waits as track. The main lane's short calls line up with the start of each subagent (the
// Task call that spawned it), which shows the hand-offs without drawing links across other lanes.
// 40 calls in all: main 8 (hand-placed), the subagents 7 / 5 / 9 / 5 / 6 from a seeded split of their
// span. One test-runner call failed (npm test, exit 1) and is the one red segment; the reviewer's last
// call (54s so far) is still running and stays open up to the NOW line at 11m 48s, the one Claude orange item. A finished
// subagent ends in a 2px up-coloured dot. Times are minutes from the start, as plain numbers.
// Sample data only.
const W = 380
const H = 232
const STAGE = '#FAF9F5'
const INK = '#141413'
const MUTED = '#73726C'
const UP = '#2C7A39'
const DOWN = '#AB2B3F'
const CLAUDE = '#D77757'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const f = n => n.toFixed(1)
const PLOT = { left: 96, right: 368 } // the names and call counts sit in the column before it
const MINUTES = 12
const NOW = 11.8 // 11m 48s
const LANE0 = 54 // the centre line of the main lane
const PITCH = 27
const BAR = 8
const TICK_Y = 202 // the minute stubs; their labels share the caption line
// Seeded so the picture is the same on every render
function rng(seed) {
return () => (seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff) / 0x7fffffff
}
const text = (x, y, s, { size = 9, fill = MUTED, anchor = 'start' } = {}) =>
`<text x="${f(x)}" y="${f(y)}"${anchor === 'start' ? '' : ` text-anchor="${anchor}"`} font-size="${size}" font-family="${FONT}" fill="${fill}">${s}</text>`
const x = m => PLOT.left + ((PLOT.right - PLOT.left) * m) / MINUTES
// The main agent's calls in minutes; the ones ending at 0.6, 2.0, 4.2, 5.0 and 7.5 are the Task calls
// that spawn each subagent, so they end where the subagent's lane begins.
const MAIN = [[0.05, 0.35], [0.45, 0.6], [1.2, 1.5], [1.85, 2.0], [4.05, 4.2], [4.85, 5.0], [7.3, 7.5], [9.3, 9.8]]
const SUBS = [
{ name: 'explore', start: 0.6, end: 3.4, calls: 7 },
{ name: 'plan', start: 2.0, end: 4.6, calls: 5 },
{ name: 'test-runner', start: 4.2, end: 9.0, calls: 9, failed: 4, note: 'npm test, exit 1' },
{ name: 'docs', start: 5.0, end: 8.2, calls: 5 },
{ name: 'reviewer', start: 7.5, end: NOW, calls: 6, running: 10.9 }, // the open call began at 10m 54s
]
// Splits a lane's span into calls and the waits between them. A call weighs 0.5 to 1.5 and a wait 0.3
// to 1.5, so calls are a little longer than gaps on average; the lane starts and ends with a call.
function split(lane, rnd) {
// A running lane keeps its open call whole and splits only the time before it
if (lane.running) return [...split({ ...lane, end: lane.running - 0.3, calls: lane.calls - 1, running: 0 }, rnd), [lane.running, lane.end]]
const calls = Array.from({ length: lane.calls }, () => 0.5 + rnd())
const waits = Array.from({ length: lane.calls - 1 }, () => 0.3 + rnd() * 1.2)
const scale = (lane.end - lane.start) / [...calls, ...waits].reduce((s, v) => s + v, 0)
const segs = []
let t = lane.start
calls.forEach((c, i) => {
segs.push([t, t + c * scale])
t += c * scale + (waits[i] ?? 0) * scale
})
return segs
}
function draw() {
const rnd = rng(20261025)
const lanes = [{ name: 'main', start: 0, end: NOW, segs: MAIN }, ...SUBS.map(s => ({ ...s, segs: split(s, rnd) }))]
const total = lanes.reduce((n, l) => n + l.segs.length, 0)
const done = SUBS.filter(s => !s.running).length
const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
// Header: the call count on the left, a two-item key on the right (both counted from the data)
parts.push(text(12, 24, String(total), { size: 13, fill: INK }))
parts.push(text(12 + String(total).length * 8 + 4, 24, 'TOOL CALLS'))
parts.push(`<rect x="320" y="18" width="6" height="6" fill="${DOWN}" fill-opacity="0.55"/>`)
parts.push(text(W - 12, 24, `${SUBS.filter(s => s.failed !== undefined).length} FAILED`, { anchor: 'end' }))
parts.push(`<circle cx="272" cy="21" r="2" fill="${UP}"/>`)
parts.push(text(279, 24, `${done} DONE`))
lanes.forEach((lane, i) => {
const cy = LANE0 + i * PITCH
const top = cy - BAR / 2
parts.push(text(12, cy + 3.5, lane.name, { size: 10, fill: INK }))
parts.push(text(PLOT.left - 8, cy + 3, String(lane.segs.length), { anchor: 'end' }))
// The lane's life: from spawn to finish (or to now) as a track
parts.push(`<rect x="${f(x(lane.start))}" y="${f(top)}" width="${f(x(lane.end) - x(lane.start))}" height="${BAR}" rx="1" fill="${INK}" fill-opacity="0.14"/>`)
lane.segs.forEach(([a, b], k) => {
const failed = k === lane.failed
const w = Math.max(2, x(b) - x(a))
parts.push(`<rect x="${f(x(a))}" y="${f(top)}" width="${f(w)}" height="${BAR}" rx="1" fill="${failed ? DOWN : INK}" fill-opacity="0.55"/>`)
if (failed) parts.push(text(x(a), top - 4, lane.note, { fill: DOWN }))
})
if (lane.running) {
parts.push(text(x(NOW) - 4, top - 4, `RUNNING ${Math.round((NOW - lane.running) * 60)}s`, { anchor: 'end' }))
} else if (i > 0) {
parts.push(`<circle cx="${f(x(lane.end) + 5)}" cy="${f(cy)}" r="2" fill="${UP}"/>`)
}
})
// Minute stubs every minute, labels every two, on the caption line like bars' hours
for (let m = 0; m <= MINUTES; m++) {
if (Math.abs(x(m) - x(NOW)) < 6) continue // a stub this close to NOW reads as a doubled line
parts.push(`<line x1="${f(x(m))}" x2="${f(x(m))}" y1="${TICK_Y}" y2="${TICK_Y + 4}" stroke="${MUTED}" stroke-width="1"/>`)
if (m % 2 === 0 && m < MINUTES) parts.push(text(x(m), H - 8, m ? `${m}m` : '0', { anchor: 'middle' }))
}
// The NOW line runs over the lanes so it stays visible where it crosses the running call
parts.push(`<line x1="${f(x(NOW))}" x2="${f(x(NOW))}" y1="40" y2="${TICK_Y + 4}" stroke="${CLAUDE}" stroke-width="1" stroke-dasharray="3 3"/>`)
parts.push(text(x(NOW), H - 8, 'NOW', { fill: CLAUDE, anchor: 'middle' }))
parts.push(text(12, H - 8, 'SAMPLE AGENTS'))
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}
let source // drawn on first use and kept: the pane loads every design, so none pays at load time
export default {
id: 'agent-lanes',
title: 'Subagent swimlanes',
category: 'work',
description: 'The main agent and its subagents as lanes over time: tool calls are segments on each lane, a failed call is red and a running call stays open to the NOW line.',
element: ui => ui.Svg({
source: (source ??= draw()),
alt: `Swimlanes of the main agent and ${SUBS.length} subagents over ${MINUTES} minutes, each tool call a segment, a failed call in red and the running call open to the NOW line, sample data`,
width: W,
height: H,
}),
}
To use it in a mod: copy the file to your mod's hooks/designs/ and return its element(ui) from a pane's ui.render. How a pane is built.
Code: PolyForm Noncommercial License 1.0.0: noncommercial use only. Required Notice: Copyright 2026 Haruki Nakada (https://modscode.com).