collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
<script lang="ts">
	import { appStore, type HiveAgent } from '$lib/store.svelte.js';

	const RADIUS = 160;
	const NODE_R = 32;
	const CENTER = { x: 200, y: 200 };
	const SVG_SIZE = 400;

	let agents = $derived(Array.from(appStore.hiveAgents.values()));

	// Position agents in a circle around the coordinator node
	function nodePos(index: number, total: number) {
		const angle = (2 * Math.PI * index) / Math.max(total, 1) - Math.PI / 2;
		return {
			x: CENTER.x + RADIUS * Math.cos(angle),
			y: CENTER.y + RADIUS * Math.sin(angle),
		};
	}

	function statusColor(agent: HiveAgent): string {
		if (agent.done && agent.success) return 'var(--green)';
		if (agent.done && !agent.success) return 'var(--red)';
		if (agent.approaching) return 'var(--yellow)';
		if (agent.status === 'started' || agent.iteration > 0) return 'var(--accent)';
		return 'var(--fg-dim)';
	}

	function statusIcon(agent: HiveAgent): string {
		if (agent.done && agent.success) return '\u2713';
		if (agent.done && !agent.success) return '\u2717';
		if (agent.approaching) return '!';
		if (agent.status === 'started' || agent.iteration > 0) return '\u25B6';
		return '\u23F3';
	}

	function isRunning(agent: HiveAgent): boolean {
		return !agent.done && (agent.status === 'started' || agent.iteration > 0);
	}

	function selectAgent(id: string) {
		appStore.selectedAgentId = appStore.selectedAgentId === id ? null : id;
	}

	function shortId(id: string): string {
		return id.length > 6 ? id.slice(0, 6) : id;
	}

	function shortTask(task: string): string {
		return task.length > 20 ? task.slice(0, 20) + '\u2026' : task;
	}
</script>

<div class="swarm-graph">
	<div class="graph-header">
		<span class="graph-title">Swarm</span>
		<span class="agent-count">{agents.length} agents</span>
		<button class="close-btn" onclick={() => (appStore.showSwarmGraph = false)}>\u00D7</button>
	</div>

	<svg
		viewBox="0 0 {SVG_SIZE} {SVG_SIZE}"
		xmlns="http://www.w3.org/2000/svg"
		class="graph-svg"
	>
		<defs>
			<marker id="arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
				<path d="M0,0 L8,3 L0,6" fill="var(--fg-dim)" opacity="0.4" />
			</marker>
		</defs>

		<!-- Dependency edges -->
		{#each agents as agent, i}
			{@const pos = nodePos(i, agents.length)}
			{#each agent.dependencies as depId}
				{@const depIdx = agents.findIndex((a) => a.id === depId)}
				{#if depIdx >= 0}
					{@const depPos = nodePos(depIdx, agents.length)}
					<line
						x1={depPos.x}
						y1={depPos.y}
						x2={pos.x}
						y2={pos.y}
						stroke="var(--fg-dim)"
						stroke-width="1.5"
						opacity="0.3"
						marker-end="url(#arrow)"
					/>
				{/if}
			{/each}
		{/each}

		<!-- Connector lines from coordinator to agents -->
		{#each agents as _, i}
			{@const pos = nodePos(i, agents.length)}
			<line
				x1={CENTER.x}
				y1={CENTER.y}
				x2={pos.x}
				y2={pos.y}
				stroke="var(--fg-dim)"
				stroke-width="1"
				opacity="0.15"
				stroke-dasharray="4,4"
			/>
		{/each}

		<!-- Coordinator center node -->
		<circle cx={CENTER.x} cy={CENTER.y} r={NODE_R * 0.7} fill="var(--bg-elevated)" stroke="var(--accent)" stroke-width="2" />
		<text x={CENTER.x} y={CENTER.y + 4} text-anchor="middle" fill="var(--accent)" font-size="10" font-weight="bold">COORD</text>

		<!-- Agent nodes -->
		{#each agents as agent, i}
			{@const pos = nodePos(i, agents.length)}
			{@const color = statusColor(agent)}
			{@const selected = appStore.selectedAgentId === agent.id}

			<!-- svelte-ignore a11y_click_events_have_key_events -->
			<!-- svelte-ignore a11y_no_static_element_interactions -->
			<g class="agent-node" class:running={isRunning(agent)} onclick={() => selectAgent(agent.id)}>
				<!-- Selection ring -->
				{#if selected}
					<circle cx={pos.x} cy={pos.y} r={NODE_R + 4} fill="none" stroke={color} stroke-width="2" opacity="0.6" />
				{/if}

				<!-- Main circle -->
				<circle
					cx={pos.x}
					cy={pos.y}
					r={NODE_R}
					fill="var(--bg-elevated)"
					stroke={color}
					stroke-width={selected ? 3 : 2}
				/>

				<!-- Status icon -->
				<text x={pos.x} y={pos.y - 6} text-anchor="middle" fill={color} font-size="14" font-weight="bold">
					{statusIcon(agent)}
				</text>

				<!-- Agent name / short ID -->
				<text x={pos.x} y={pos.y + 8} text-anchor="middle" fill="var(--fg)" font-size="8" font-weight="600">
					{agent.name || shortId(agent.id)}
				</text>

				<!-- Iteration count -->
				{#if agent.iteration > 0}
					<text x={pos.x} y={pos.y + 18} text-anchor="middle" fill="var(--fg-dim)" font-size="7">
						#{agent.iteration}
					</text>
				{/if}

				<!-- Approaching warning badge -->
				{#if agent.approaching && !agent.done}
					<circle cx={pos.x + NODE_R * 0.7} cy={pos.y - NODE_R * 0.7} r="8" fill="var(--yellow)" />
					<text x={pos.x + NODE_R * 0.7} y={pos.y - NODE_R * 0.7 + 3} text-anchor="middle" fill="var(--bg)" font-size="8" font-weight="bold">
						{agent.approachingRemaining}
					</text>
				{/if}
			</g>
		{/each}
	</svg>

	<!-- Agent task labels below graph -->
	{#if agents.length > 0}
		<div class="agent-list">
			{#each agents as agent}
				<!-- svelte-ignore a11y_click_events_have_key_events -->
				<!-- svelte-ignore a11y_no_static_element_interactions -->
				<div
					class="agent-label"
					class:selected={appStore.selectedAgentId === agent.id}
					onclick={() => selectAgent(agent.id)}
				>
					<span class="dot" style="background: {statusColor(agent)}"></span>
					<span class="name">{agent.name || shortId(agent.id)}</span>
					<span class="task">{shortTask(agent.task)}</span>
				</div>
			{/each}
		</div>
	{/if}
</div>

<style>
	.swarm-graph {
		padding: 8px;
		background: var(--bg-surface);
		border-radius: 8px;
		border: 1px solid var(--fg-dim, #333);
		margin-bottom: 8px;
	}
	.graph-header {
		display: flex;
		align-items: center;
		gap: 8px;
		margin-bottom: 4px;
		padding: 0 4px;
	}
	.graph-title {
		font-weight: 700;
		font-size: 13px;
		color: var(--accent);
	}
	.agent-count {
		font-size: 11px;
		color: var(--fg-dim);
		flex: 1;
	}
	.close-btn {
		background: none;
		border: none;
		color: var(--fg-dim);
		font-size: 16px;
		cursor: pointer;
		padding: 0 4px;
	}
	.close-btn:hover {
		color: var(--fg);
	}
	.graph-svg {
		width: 100%;
		max-height: 360px;
	}
	.agent-node {
		cursor: pointer;
		transition: opacity 0.15s;
	}
	.agent-node:hover {
		opacity: 0.85;
	}

	/* Pulsing animation for running nodes */
	.agent-node.running circle:nth-child(2) {
		animation: pulse 1.8s ease-in-out infinite;
	}
	@keyframes pulse {
		0%, 100% { opacity: 1; }
		50% { opacity: 0.6; }
	}

	.agent-list {
		display: flex;
		flex-direction: column;
		gap: 3px;
		padding: 4px;
		max-height: 120px;
		overflow-y: auto;
	}
	.agent-label {
		display: flex;
		align-items: center;
		gap: 6px;
		padding: 3px 6px;
		border-radius: 4px;
		font-size: 11px;
		cursor: pointer;
		transition: background 0.1s;
	}
	.agent-label:hover,
	.agent-label.selected {
		background: var(--bg-elevated);
	}
	.dot {
		width: 6px;
		height: 6px;
		border-radius: 50%;
		flex-shrink: 0;
	}
	.name {
		color: var(--cyan);
		font-weight: 600;
		min-width: 40px;
	}
	.task {
		color: var(--fg-dim);
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}
</style>