consensus-cli 0.1.2 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js CHANGED
@@ -9,8 +9,10 @@ const panelClose = document.getElementById("panel-close");
9
9
  const statusEl = document.getElementById("status");
10
10
  const countEl = document.getElementById("count");
11
11
  const activeList = document.getElementById("active-list");
12
+ const serverList = document.getElementById("server-list");
12
13
  const searchInput = document.getElementById("search");
13
14
  const laneTitle = document.querySelector(".lane-title");
15
+ const serverTitle = document.querySelector(".server-title");
14
16
 
15
17
  const tileW = 96;
16
18
  const tileH = 48;
@@ -19,10 +21,55 @@ const gridScale = 2;
19
21
  const query = new URLSearchParams(window.location.search);
20
22
  const mockMode = query.get("mock") === "1";
21
23
 
22
- const statePalette = {
23
- active: { top: "#3d8f7f", left: "#2d6d61", right: "#275b52", stroke: "#54cdb1" },
24
- idle: { top: "#384a57", left: "#2b3943", right: "#25323b", stroke: "#4f6b7a" },
25
- error: { top: "#82443c", left: "#6d3530", right: "#5a2c28", stroke: "#d1584b" },
24
+ const cliPalette = {
25
+ codex: {
26
+ agent: {
27
+ active: { top: "#3d8f7f", left: "#2d6d61", right: "#275b52", stroke: "#54cdb1" },
28
+ idle: { top: "#384a57", left: "#2b3943", right: "#25323b", stroke: "#4f6b7a" },
29
+ error: { top: "#82443c", left: "#6d3530", right: "#5a2c28", stroke: "#d1584b" },
30
+ },
31
+ server: {
32
+ active: { top: "#4e665e", left: "#3d524b", right: "#32453f", stroke: "#79b8a8" },
33
+ idle: { top: "#353f48", left: "#2a323a", right: "#232a30", stroke: "#526577" },
34
+ error: { top: "#82443c", left: "#6d3530", right: "#5a2c28", stroke: "#d1584b" },
35
+ },
36
+ accent: "#57f2c6",
37
+ accentStrong: "rgba(87, 242, 198, 0.6)",
38
+ accentSoft: "rgba(87, 242, 198, 0.35)",
39
+ glow: "87, 242, 198",
40
+ },
41
+ opencode: {
42
+ agent: {
43
+ active: { top: "#8a6a2f", left: "#6f5626", right: "#5b4621", stroke: "#f1bd4f" },
44
+ idle: { top: "#3c3a37", left: "#2f2d2a", right: "#262322", stroke: "#7f6f56" },
45
+ error: { top: "#86443b", left: "#70352f", right: "#5c2c28", stroke: "#e0705c" },
46
+ },
47
+ server: {
48
+ active: { top: "#7d6a2b", left: "#665725", right: "#54481f", stroke: "#f5c453" },
49
+ idle: { top: "#353b42", left: "#272c33", right: "#1f242a", stroke: "#6b7380" },
50
+ error: { top: "#86443b", left: "#70352f", right: "#5c2c28", stroke: "#e0705c" },
51
+ },
52
+ accent: "#f5c453",
53
+ accentStrong: "rgba(245, 196, 83, 0.6)",
54
+ accentSoft: "rgba(245, 196, 83, 0.35)",
55
+ glow: "245, 196, 83",
56
+ },
57
+ claude: {
58
+ agent: {
59
+ active: { top: "#3f6fa3", left: "#2f5580", right: "#25476a", stroke: "#7fb7ff" },
60
+ idle: { top: "#374252", left: "#2a323f", right: "#232a35", stroke: "#5c6f85" },
61
+ error: { top: "#7f4140", left: "#683334", right: "#552a2b", stroke: "#e06b6a" },
62
+ },
63
+ server: {
64
+ active: { top: "#4b5f74", left: "#3a4a5c", right: "#2f3d4d", stroke: "#91b4d6" },
65
+ idle: { top: "#323b47", left: "#262d36", right: "#20262d", stroke: "#556577" },
66
+ error: { top: "#7f4140", left: "#683334", right: "#552a2b", stroke: "#e06b6a" },
67
+ },
68
+ accent: "#7fb7ff",
69
+ accentStrong: "rgba(127, 183, 255, 0.6)",
70
+ accentSoft: "rgba(127, 183, 255, 0.35)",
71
+ glow: "127, 183, 255",
72
+ },
26
73
  };
27
74
  const stateOpacity = {
28
75
  active: 1,
@@ -119,12 +166,16 @@ function hashString(input) {
119
166
  return Math.abs(hash);
120
167
  }
121
168
 
122
- function keyForAgent(agent) {
169
+ function groupKeyForAgent(agent) {
123
170
  return agent.repo || agent.cwd || agent.cmd || agent.id;
124
171
  }
125
172
 
126
- function assignCoordinate(key) {
127
- const hash = hashString(key);
173
+ function keyForAgent(agent) {
174
+ return `${groupKeyForAgent(agent)}::${agent.id}`;
175
+ }
176
+
177
+ function assignCoordinate(key, baseKey) {
178
+ const hash = hashString(baseKey || key);
128
179
  const baseX = (hash % 16) - 8;
129
180
  const baseY = ((hash >> 4) % 16) - 8;
130
181
  const maxRadius = 20;
@@ -152,9 +203,10 @@ function updateLayout(newAgents) {
152
203
  const activeKeys = new Set();
153
204
  for (const agent of newAgents) {
154
205
  const key = keyForAgent(agent);
206
+ const baseKey = groupKeyForAgent(agent);
155
207
  activeKeys.add(key);
156
208
  if (!layout.has(key)) {
157
- assignCoordinate(key);
209
+ assignCoordinate(key, baseKey);
158
210
  }
159
211
  }
160
212
 
@@ -170,8 +222,14 @@ function setStatus(text) {
170
222
  statusEl.textContent = text;
171
223
  }
172
224
 
173
- function setCount(count) {
174
- countEl.textContent = `${count} agent${count === 1 ? "" : "s"}`;
225
+ function setCount(agentCount, serverCount) {
226
+ const agentLabel = `${agentCount} agent${agentCount === 1 ? "" : "s"}`;
227
+ if (typeof serverCount === "number") {
228
+ const serverLabel = `${serverCount} server${serverCount === 1 ? "" : "s"}`;
229
+ countEl.textContent = `${agentLabel} • ${serverLabel}`;
230
+ return;
231
+ }
232
+ countEl.textContent = agentLabel;
175
233
  }
176
234
 
177
235
  function formatBytes(bytes) {
@@ -200,6 +258,45 @@ function escapeHtml(value) {
200
258
  .replace(/'/g, "'");
201
259
  }
202
260
 
261
+ function isServerKind(kind) {
262
+ return kind === "app-server" || kind === "opencode-server";
263
+ }
264
+
265
+ function cliForAgent(agent) {
266
+ const kind = agent.kind || "";
267
+ if (kind.startsWith("opencode")) return "opencode";
268
+ if (kind.startsWith("claude")) return "claude";
269
+ return "codex";
270
+ }
271
+
272
+ function paletteFor(agent) {
273
+ const cli = cliForAgent(agent);
274
+ const palette = cliPalette[cli] || cliPalette.codex;
275
+ const scope = isServerKind(agent.kind) ? palette.server : palette.agent;
276
+ return scope[agent.state] || scope.idle;
277
+ }
278
+
279
+ function accentFor(agent) {
280
+ const cli = cliForAgent(agent);
281
+ return (cliPalette[cli] || cliPalette.codex).accent;
282
+ }
283
+
284
+ function accentStrongFor(agent) {
285
+ const cli = cliForAgent(agent);
286
+ return (cliPalette[cli] || cliPalette.codex).accentStrong;
287
+ }
288
+
289
+ function accentSoftFor(agent) {
290
+ const cli = cliForAgent(agent);
291
+ return (cliPalette[cli] || cliPalette.codex).accentSoft;
292
+ }
293
+
294
+ function accentGlow(agent, alpha) {
295
+ const cli = cliForAgent(agent);
296
+ const tint = (cliPalette[cli] || cliPalette.codex).glow;
297
+ return `rgba(${tint}, ${alpha})`;
298
+ }
299
+
203
300
  function labelFor(agent) {
204
301
  if (agent.title) return agent.title;
205
302
  if (agent.repo) return agent.repo;
@@ -267,10 +364,10 @@ function drawTag(ctx, x, y, text, accent) {
267
364
  ctx.restore();
268
365
  }
269
366
 
270
- function renderActiveList(items) {
271
- if (!activeList) return;
367
+ function renderLaneList(items, container, emptyLabel) {
368
+ if (!container) return;
272
369
  if (!items.length) {
273
- activeList.innerHTML = "<div class=\"lane-meta\">No active agents.</div>";
370
+ container.innerHTML = `<div class="lane-meta">${emptyLabel}</div>`;
274
371
  return;
275
372
  }
276
373
  const sorted = [...items].sort((a, b) => {
@@ -281,14 +378,17 @@ function renderActiveList(items) {
281
378
  return b.cpu - a.cpu;
282
379
  });
283
380
 
284
- activeList.innerHTML = sorted
381
+ container.innerHTML = sorted
285
382
  .map((agent) => {
286
383
  const doingRaw = agent.summary?.current || agent.doing || agent.cmdShort || "";
287
384
  const doing = escapeHtml(truncate(doingRaw, 80));
288
385
  const selectedClass = selected && selected.id === agent.id ? "is-selected" : "";
386
+ const accent = accentFor(agent);
387
+ const accentGlow = accentSoftFor(agent);
388
+ const cli = cliForAgent(agent);
289
389
  const label = escapeHtml(labelFor(agent));
290
390
  return `
291
- <button class="lane-item ${selectedClass}" type="button" data-id="${agent.id}">
391
+ <button class="lane-item ${selectedClass} cli-${cli}" type="button" data-id="${agent.id}" style="--cli-accent: ${accent}; --cli-accent-glow: ${accentGlow};">
292
392
  <div class="lane-pill ${agent.state}"></div>
293
393
  <div class="lane-copy">
294
394
  <div class="lane-label">${label}</div>
@@ -299,7 +399,7 @@ function renderActiveList(items) {
299
399
  })
300
400
  .join("");
301
401
 
302
- Array.from(activeList.querySelectorAll(".lane-item")).forEach((item) => {
402
+ Array.from(container.querySelectorAll(".lane-item")).forEach((item) => {
303
403
  item.addEventListener("click", () => {
304
404
  const id = item.getAttribute("data-id");
305
405
  selected = sorted.find((agent) => agent.id === id) || null;
@@ -446,10 +546,14 @@ function draw() {
446
546
  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
447
547
 
448
548
  for (const item of drawList) {
449
- const palette = statePalette[item.agent.state] || statePalette.idle;
549
+ const palette = paletteFor(item.agent);
450
550
  const memMB = item.agent.mem / (1024 * 1024);
451
551
  const heightBase = Math.min(120, Math.max(18, memMB * 0.4));
452
552
  const isActive = item.agent.state === "active";
553
+ const isServer = isServerKind(item.agent.kind);
554
+ const accent = accentFor(item.agent);
555
+ const accentStrong = accentStrongFor(item.agent);
556
+ const accentSoft = accentSoftFor(item.agent);
453
557
  const pulse =
454
558
  isActive && !reducedMotion
455
559
  ? 4 + Math.sin(time / 200) * 3
@@ -490,7 +594,7 @@ function draw() {
490
594
  y + tileH * 0.02,
491
595
  tileW * 0.92,
492
596
  tileH * 0.46,
493
- `rgba(87, 242, 198, ${glowAlpha})`,
597
+ accentGlow(item.agent, glowAlpha),
494
598
  null
495
599
  );
496
600
  drawDiamond(
@@ -499,14 +603,14 @@ function draw() {
499
603
  y - height - tileH * 0.18,
500
604
  roofSize * 0.82,
501
605
  roofSize * 0.42,
502
- `rgba(87, 242, 198, ${capAlpha})`,
606
+ accentGlow(item.agent, capAlpha),
503
607
  null
504
608
  );
505
609
  ctx.restore();
506
610
  }
507
611
 
508
612
  if (selected && selected.id === item.agent.id) {
509
- drawDiamond(ctx, x, y, tileW + 10, tileH + 6, "rgba(0,0,0,0)", "#57f2c6");
613
+ drawDiamond(ctx, x, y, tileW + 10, tileH + 6, "rgba(0,0,0,0)", accent);
510
614
  }
511
615
 
512
616
  ctx.fillStyle = "rgba(10, 12, 15, 0.6)";
@@ -519,15 +623,21 @@ function draw() {
519
623
  const showActiveTag = topActiveIds.has(item.agent.id);
520
624
  if (isHovered || isSelected) {
521
625
  const label = truncate(labelFor(item.agent), 20);
522
- drawTag(ctx, x, y - height - tileH * 0.6, label, "rgba(87, 242, 198, 0.6)");
626
+ drawTag(ctx, x, y - height - tileH * 0.6, label, accentStrong);
523
627
  const doing = truncate(item.agent.summary?.current || item.agent.doing || "", 36);
524
- drawTag(ctx, x, y - height - tileH * 0.9, doing, "rgba(87, 242, 198, 0.35)");
628
+ drawTag(ctx, x, y - height - tileH * 0.9, doing, accentSoft);
629
+ if (isServer) {
630
+ drawTag(ctx, x, y + tileH * 0.2, "server", "rgba(79, 107, 122, 0.6)");
631
+ }
525
632
  } else if (showActiveTag) {
526
633
  const doing = truncate(
527
634
  item.agent.summary?.current || item.agent.doing || labelFor(item.agent),
528
635
  32
529
636
  );
530
- drawTag(ctx, x, y - height - tileH * 0.7, doing, "rgba(87, 242, 198, 0.35)");
637
+ drawTag(ctx, x, y - height - tileH * 0.7, doing, accentSoft);
638
+ if (isServer) {
639
+ drawTag(ctx, x, y + tileH * 0.2, "server", "rgba(79, 107, 122, 0.6)");
640
+ }
531
641
  }
532
642
 
533
643
  hitList.push({
@@ -680,7 +790,9 @@ function connect() {
680
790
 
681
791
  function applySnapshot(payload) {
682
792
  agents = payload.agents || [];
683
- setCount(agents.length);
793
+ const serverAgents = agents.filter((agent) => isServerKind(agent.kind));
794
+ const agentNodes = agents.filter((agent) => !isServerKind(agent.kind));
795
+ setCount(agentNodes.length, serverAgents.length);
684
796
  const query = searchQuery.trim().toLowerCase();
685
797
  searchMatches = new Set(
686
798
  query ? agents.filter((agent) => matchesQuery(agent, query)).map((agent) => agent.id) : []
@@ -689,12 +801,19 @@ function applySnapshot(payload) {
689
801
  ? agents.filter((agent) => searchMatches.has(agent.id))
690
802
  : agents;
691
803
  const listAgents = query
692
- ? visibleAgents
693
- : visibleAgents.filter((agent) => agent.state !== "idle");
804
+ ? visibleAgents.filter((agent) => !isServerKind(agent.kind))
805
+ : visibleAgents.filter((agent) => agent.state !== "idle" && !isServerKind(agent.kind));
806
+ const listServers = query
807
+ ? visibleAgents.filter((agent) => isServerKind(agent.kind))
808
+ : visibleAgents.filter((agent) => isServerKind(agent.kind));
694
809
  if (laneTitle) {
695
810
  laneTitle.textContent = query ? "search results" : "active agents";
696
811
  }
697
- renderActiveList(listAgents);
812
+ if (serverTitle) {
813
+ serverTitle.textContent = query ? "server results" : "servers";
814
+ }
815
+ renderLaneList(listAgents, activeList, "No active agents.");
816
+ renderLaneList(listServers, serverList, "No servers detected.");
698
817
  if (selected) {
699
818
  selected = agents.find((agent) => agent.id === selected.id) || selected;
700
819
  renderPanel(selected);
package/public/index.html CHANGED
@@ -41,6 +41,9 @@
41
41
  aria-label="Search metadata"
42
42
  />
43
43
  <div id="active-list"></div>
44
+ <div class="lane-divider" aria-hidden="true"></div>
45
+ <div class="lane-title server-title">servers</div>
46
+ <div id="server-list"></div>
44
47
  </div>
45
48
  <div id="tooltip" class="hidden"></div>
46
49
  <aside id="panel" class="collapsed" aria-label="Agent details">
package/public/style.css CHANGED
@@ -282,13 +282,28 @@ body {
282
282
  gap: 8px;
283
283
  }
284
284
 
285
+ #server-list {
286
+ display: flex;
287
+ flex-direction: column;
288
+ gap: 8px;
289
+ }
290
+
291
+ .lane-divider {
292
+ height: 1px;
293
+ margin: 12px 0;
294
+ background: rgba(62, 78, 89, 0.6);
295
+ }
296
+
285
297
  .lane-item {
298
+ --cli-accent: rgba(87, 242, 198, 0.7);
299
+ --cli-accent-glow: rgba(87, 242, 198, 0.25);
286
300
  display: flex;
287
301
  align-items: flex-start;
288
302
  gap: 10px;
289
303
  padding: 10px;
290
304
  border-radius: 10px;
291
305
  border: 1px solid rgba(62, 78, 89, 0.5);
306
+ border-left: 3px solid var(--cli-accent);
292
307
  background: rgba(13, 17, 22, 0.85);
293
308
  cursor: pointer;
294
309
  text-align: left;
@@ -300,16 +315,18 @@ body {
300
315
  }
301
316
 
302
317
  .lane-item:hover {
303
- border-color: rgba(87, 242, 198, 0.5);
318
+ border-color: var(--cli-accent);
319
+ border-left-color: var(--cli-accent);
304
320
  }
305
321
 
306
322
  .lane-item.is-selected {
307
- border-color: rgba(87, 242, 198, 0.7);
308
- box-shadow: 0 0 12px rgba(87, 242, 198, 0.25);
323
+ border-color: var(--cli-accent);
324
+ border-left-color: var(--cli-accent);
325
+ box-shadow: 0 0 12px var(--cli-accent-glow);
309
326
  }
310
327
 
311
328
  .lane-item:focus-visible {
312
- outline: 2px solid rgba(87, 242, 198, 0.6);
329
+ outline: 2px solid var(--cli-accent);
313
330
  outline-offset: 2px;
314
331
  }
315
332