consensus-cli 0.1.0 → 0.1.4

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;
@@ -24,6 +26,11 @@ const statePalette = {
24
26
  idle: { top: "#384a57", left: "#2b3943", right: "#25323b", stroke: "#4f6b7a" },
25
27
  error: { top: "#82443c", left: "#6d3530", right: "#5a2c28", stroke: "#d1584b" },
26
28
  };
29
+ const serverPalette = {
30
+ active: { top: "#7d6a2b", left: "#665725", right: "#54481f", stroke: "#f5c453" },
31
+ idle: { top: "#353b42", left: "#272c33", right: "#1f242a", stroke: "#6b7380" },
32
+ error: statePalette.error,
33
+ };
27
34
  const stateOpacity = {
28
35
  active: 1,
29
36
  idle: 0.35,
@@ -49,6 +56,53 @@ let searchMatches = new Set();
49
56
  const layout = new Map();
50
57
  const occupied = new Map();
51
58
 
59
+ function ensureSelectedVisible(agent) {
60
+ if (!agent || !panel.classList.contains("open")) return;
61
+ if (view.dragging) return;
62
+ const panelRect = panel.getBoundingClientRect();
63
+ if (panelRect.width >= window.innerWidth * 0.8) return;
64
+ const key = keyForAgent(agent);
65
+ const coord = layout.get(key);
66
+ if (!coord) return;
67
+
68
+ const screen = isoToScreen(coord.x, coord.y, tileW, tileH);
69
+ const memMB = (agent.mem || 0) / (1024 * 1024);
70
+ const heightBase = Math.min(120, Math.max(18, memMB * 0.4));
71
+ const idleScale = agent.state === "idle" ? 0.6 : 1;
72
+ const height = heightBase * idleScale;
73
+
74
+ const targetX = view.x + screen.x * view.scale;
75
+ const targetY = view.y + screen.y * view.scale;
76
+ const halfW = (tileW / 2) * view.scale;
77
+ const halfH = (tileH / 2) * view.scale;
78
+ const padding = 36;
79
+ const viewportWidth = window.innerWidth - panelRect.width;
80
+ const viewportHeight = window.innerHeight;
81
+
82
+ const left = targetX - halfW;
83
+ const right = targetX + halfW;
84
+ const top = targetY - (height + tileH * 0.6) * view.scale;
85
+ const bottom = targetY + (halfH + tileH * 0.6) * view.scale;
86
+
87
+ let dx = 0;
88
+ let dy = 0;
89
+ if (right > viewportWidth - padding) {
90
+ dx = viewportWidth - padding - right;
91
+ } else if (left < padding) {
92
+ dx = padding - left;
93
+ }
94
+ if (top < padding) {
95
+ dy = padding - top;
96
+ } else if (bottom > viewportHeight - padding) {
97
+ dy = viewportHeight - padding - bottom;
98
+ }
99
+
100
+ if (dx !== 0 || dy !== 0) {
101
+ view.x += dx;
102
+ view.y += dy;
103
+ }
104
+ }
105
+
52
106
  function resize() {
53
107
  deviceScale = window.devicePixelRatio || 1;
54
108
  canvas.width = window.innerWidth * deviceScale;
@@ -72,12 +126,16 @@ function hashString(input) {
72
126
  return Math.abs(hash);
73
127
  }
74
128
 
75
- function keyForAgent(agent) {
129
+ function groupKeyForAgent(agent) {
76
130
  return agent.repo || agent.cwd || agent.cmd || agent.id;
77
131
  }
78
132
 
79
- function assignCoordinate(key) {
80
- const hash = hashString(key);
133
+ function keyForAgent(agent) {
134
+ return `${groupKeyForAgent(agent)}::${agent.id}`;
135
+ }
136
+
137
+ function assignCoordinate(key, baseKey) {
138
+ const hash = hashString(baseKey || key);
81
139
  const baseX = (hash % 16) - 8;
82
140
  const baseY = ((hash >> 4) % 16) - 8;
83
141
  const maxRadius = 20;
@@ -105,9 +163,10 @@ function updateLayout(newAgents) {
105
163
  const activeKeys = new Set();
106
164
  for (const agent of newAgents) {
107
165
  const key = keyForAgent(agent);
166
+ const baseKey = groupKeyForAgent(agent);
108
167
  activeKeys.add(key);
109
168
  if (!layout.has(key)) {
110
- assignCoordinate(key);
169
+ assignCoordinate(key, baseKey);
111
170
  }
112
171
  }
113
172
 
@@ -123,8 +182,14 @@ function setStatus(text) {
123
182
  statusEl.textContent = text;
124
183
  }
125
184
 
126
- function setCount(count) {
127
- countEl.textContent = `${count} agent${count === 1 ? "" : "s"}`;
185
+ function setCount(agentCount, serverCount) {
186
+ const agentLabel = `${agentCount} agent${agentCount === 1 ? "" : "s"}`;
187
+ if (typeof serverCount === "number") {
188
+ const serverLabel = `${serverCount} server${serverCount === 1 ? "" : "s"}`;
189
+ countEl.textContent = `${agentLabel} • ${serverLabel}`;
190
+ return;
191
+ }
192
+ countEl.textContent = agentLabel;
128
193
  }
129
194
 
130
195
  function formatBytes(bytes) {
@@ -153,6 +218,26 @@ function escapeHtml(value) {
153
218
  .replace(/'/g, "&#39;");
154
219
  }
155
220
 
221
+ function isServerKind(kind) {
222
+ return kind === "app-server" || kind === "opencode-server";
223
+ }
224
+
225
+ function paletteFor(agent) {
226
+ if (isServerKind(agent.kind)) {
227
+ return serverPalette[agent.state] || serverPalette.idle;
228
+ }
229
+ return statePalette[agent.state] || statePalette.idle;
230
+ }
231
+
232
+ function accentFor(agent) {
233
+ return isServerKind(agent.kind) ? "#f5c453" : "#57f2c6";
234
+ }
235
+
236
+ function accentGlow(agent, alpha) {
237
+ const tint = isServerKind(agent.kind) ? "245, 196, 83" : "87, 242, 198";
238
+ return `rgba(${tint}, ${alpha})`;
239
+ }
240
+
156
241
  function labelFor(agent) {
157
242
  if (agent.title) return agent.title;
158
243
  if (agent.repo) return agent.repo;
@@ -220,10 +305,10 @@ function drawTag(ctx, x, y, text, accent) {
220
305
  ctx.restore();
221
306
  }
222
307
 
223
- function renderActiveList(items) {
224
- if (!activeList) return;
308
+ function renderLaneList(items, container, emptyLabel) {
309
+ if (!container) return;
225
310
  if (!items.length) {
226
- activeList.innerHTML = "<div class=\"lane-meta\">No active agents.</div>";
311
+ container.innerHTML = `<div class="lane-meta">${emptyLabel}</div>`;
227
312
  return;
228
313
  }
229
314
  const sorted = [...items].sort((a, b) => {
@@ -234,7 +319,7 @@ function renderActiveList(items) {
234
319
  return b.cpu - a.cpu;
235
320
  });
236
321
 
237
- activeList.innerHTML = sorted
322
+ container.innerHTML = sorted
238
323
  .map((agent) => {
239
324
  const doingRaw = agent.summary?.current || agent.doing || agent.cmdShort || "";
240
325
  const doing = escapeHtml(truncate(doingRaw, 80));
@@ -252,7 +337,7 @@ function renderActiveList(items) {
252
337
  })
253
338
  .join("");
254
339
 
255
- Array.from(activeList.querySelectorAll(".lane-item")).forEach((item) => {
340
+ Array.from(container.querySelectorAll(".lane-item")).forEach((item) => {
256
341
  item.addEventListener("click", () => {
257
342
  const id = item.getAttribute("data-id");
258
343
  selected = sorted.find((agent) => agent.id === id) || null;
@@ -369,13 +454,16 @@ function draw() {
369
454
  ctx.fillStyle = "rgba(228, 230, 235, 0.6)";
370
455
  ctx.font = "16px Space Grotesk";
371
456
  ctx.textAlign = "center";
372
- ctx.fillText("No codex processes found", 0, 0);
373
- ctx.restore();
374
- requestAnimationFrame(draw);
375
- return;
457
+ ctx.fillText("No codex processes found", 0, 0);
458
+ ctx.restore();
459
+ requestAnimationFrame(draw);
460
+ return;
376
461
  }
377
462
 
378
463
  updateLayout(agents);
464
+ if (selected) {
465
+ ensureSelectedVisible(selected);
466
+ }
379
467
 
380
468
  const drawList = agents
381
469
  .map((agent) => {
@@ -396,13 +484,22 @@ function draw() {
396
484
  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
397
485
 
398
486
  for (const item of drawList) {
399
- const palette = statePalette[item.agent.state] || statePalette.idle;
487
+ const palette = paletteFor(item.agent);
400
488
  const memMB = item.agent.mem / (1024 * 1024);
401
489
  const heightBase = Math.min(120, Math.max(18, memMB * 0.4));
490
+ const isActive = item.agent.state === "active";
491
+ const isServer = isServerKind(item.agent.kind);
492
+ const accent = accentFor(item.agent);
493
+ const accentStrong = isServer ? "rgba(245, 196, 83, 0.6)" : "rgba(87, 242, 198, 0.6)";
494
+ const accentSoft = isServer ? "rgba(245, 196, 83, 0.35)" : "rgba(87, 242, 198, 0.35)";
402
495
  const pulse =
403
- item.agent.state === "active" && !reducedMotion
496
+ isActive && !reducedMotion
404
497
  ? 4 + Math.sin(time / 200) * 3
405
498
  : 0;
499
+ const pulsePhase =
500
+ isActive && !reducedMotion
501
+ ? (Math.sin(time / 240) + 1) / 2
502
+ : 0;
406
503
  const idleScale = item.agent.state === "idle" ? 0.6 : 1;
407
504
  const height = heightBase * idleScale + pulse;
408
505
 
@@ -425,8 +522,33 @@ function draw() {
425
522
  null
426
523
  );
427
524
 
525
+ if (isActive) {
526
+ const glowAlpha = 0.12 + pulsePhase * 0.22;
527
+ const capAlpha = 0.16 + pulsePhase * 0.28;
528
+ ctx.save();
529
+ drawDiamond(
530
+ ctx,
531
+ x,
532
+ y + tileH * 0.02,
533
+ tileW * 0.92,
534
+ tileH * 0.46,
535
+ accentGlow(item.agent, glowAlpha),
536
+ null
537
+ );
538
+ drawDiamond(
539
+ ctx,
540
+ x,
541
+ y - height - tileH * 0.18,
542
+ roofSize * 0.82,
543
+ roofSize * 0.42,
544
+ accentGlow(item.agent, capAlpha),
545
+ null
546
+ );
547
+ ctx.restore();
548
+ }
549
+
428
550
  if (selected && selected.id === item.agent.id) {
429
- drawDiamond(ctx, x, y, tileW + 10, tileH + 6, "rgba(0,0,0,0)", "#57f2c6");
551
+ drawDiamond(ctx, x, y, tileW + 10, tileH + 6, "rgba(0,0,0,0)", accent);
430
552
  }
431
553
 
432
554
  ctx.fillStyle = "rgba(10, 12, 15, 0.6)";
@@ -439,15 +561,21 @@ function draw() {
439
561
  const showActiveTag = topActiveIds.has(item.agent.id);
440
562
  if (isHovered || isSelected) {
441
563
  const label = truncate(labelFor(item.agent), 20);
442
- drawTag(ctx, x, y - height - tileH * 0.6, label, "rgba(87, 242, 198, 0.6)");
564
+ drawTag(ctx, x, y - height - tileH * 0.6, label, accentStrong);
443
565
  const doing = truncate(item.agent.summary?.current || item.agent.doing || "", 36);
444
- drawTag(ctx, x, y - height - tileH * 0.9, doing, "rgba(87, 242, 198, 0.35)");
566
+ drawTag(ctx, x, y - height - tileH * 0.9, doing, accentSoft);
567
+ if (isServer) {
568
+ drawTag(ctx, x, y + tileH * 0.2, "server", "rgba(79, 107, 122, 0.6)");
569
+ }
445
570
  } else if (showActiveTag) {
446
571
  const doing = truncate(
447
572
  item.agent.summary?.current || item.agent.doing || labelFor(item.agent),
448
573
  32
449
574
  );
450
- drawTag(ctx, x, y - height - tileH * 0.7, doing, "rgba(87, 242, 198, 0.35)");
575
+ drawTag(ctx, x, y - height - tileH * 0.7, doing, accentSoft);
576
+ if (isServer) {
577
+ drawTag(ctx, x, y + tileH * 0.2, "server", "rgba(79, 107, 122, 0.6)");
578
+ }
451
579
  }
452
580
 
453
581
  hitList.push({
@@ -600,7 +728,9 @@ function connect() {
600
728
 
601
729
  function applySnapshot(payload) {
602
730
  agents = payload.agents || [];
603
- setCount(agents.length);
731
+ const serverAgents = agents.filter((agent) => isServerKind(agent.kind));
732
+ const agentNodes = agents.filter((agent) => !isServerKind(agent.kind));
733
+ setCount(agentNodes.length, serverAgents.length);
604
734
  const query = searchQuery.trim().toLowerCase();
605
735
  searchMatches = new Set(
606
736
  query ? agents.filter((agent) => matchesQuery(agent, query)).map((agent) => agent.id) : []
@@ -609,12 +739,19 @@ function applySnapshot(payload) {
609
739
  ? agents.filter((agent) => searchMatches.has(agent.id))
610
740
  : agents;
611
741
  const listAgents = query
612
- ? visibleAgents
613
- : visibleAgents.filter((agent) => agent.state !== "idle");
742
+ ? visibleAgents.filter((agent) => !isServerKind(agent.kind))
743
+ : visibleAgents.filter((agent) => agent.state !== "idle" && !isServerKind(agent.kind));
744
+ const listServers = query
745
+ ? visibleAgents.filter((agent) => isServerKind(agent.kind))
746
+ : visibleAgents.filter((agent) => isServerKind(agent.kind));
614
747
  if (laneTitle) {
615
748
  laneTitle.textContent = query ? "search results" : "active agents";
616
749
  }
617
- renderActiveList(listAgents);
750
+ if (serverTitle) {
751
+ serverTitle.textContent = query ? "server results" : "servers";
752
+ }
753
+ renderLaneList(listAgents, activeList, "No active agents.");
754
+ renderLaneList(listServers, serverList, "No servers detected.");
618
755
  if (selected) {
619
756
  selected = agents.find((agent) => agent.id === selected.id) || selected;
620
757
  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,6 +282,18 @@ 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 {
286
298
  display: flex;
287
299
  align-items: flex-start;
@@ -320,11 +332,13 @@ body {
320
332
  margin-top: 6px;
321
333
  background: var(--idle);
322
334
  box-shadow: 0 0 10px transparent;
335
+ transform-origin: center;
323
336
  }
324
337
 
325
338
  .lane-pill.active {
326
339
  background: var(--active);
327
340
  box-shadow: 0 0 12px rgba(81, 195, 165, 0.5);
341
+ animation: lanePulse 1.3s ease-in-out infinite;
328
342
  }
329
343
 
330
344
  .lane-pill.error {
@@ -332,6 +346,21 @@ body {
332
346
  box-shadow: 0 0 12px rgba(209, 88, 75, 0.5);
333
347
  }
334
348
 
349
+ @keyframes lanePulse {
350
+ 0% {
351
+ transform: scale(1);
352
+ box-shadow: 0 0 10px rgba(81, 195, 165, 0.35);
353
+ }
354
+ 50% {
355
+ transform: scale(1.35);
356
+ box-shadow: 0 0 16px rgba(81, 195, 165, 0.6);
357
+ }
358
+ 100% {
359
+ transform: scale(1);
360
+ box-shadow: 0 0 10px rgba(81, 195, 165, 0.35);
361
+ }
362
+ }
363
+
335
364
  .lane-copy {
336
365
  display: flex;
337
366
  flex-direction: column;