@cotal-ai/web 0.12.0 → 0.13.0
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/dist/web/app.js +388 -124
- package/dist/web/graph.html +13 -1
- package/dist/web/graph.js +181 -22
- package/dist/web/harness.js +33 -0
- package/dist/web/index.html +172 -21
- package/dist/web/md.js +31 -0
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +34 -9
- package/dist/web.js.map +1 -1
- package/package.json +7 -3
package/dist/web/graph.html
CHANGED
|
@@ -98,6 +98,11 @@
|
|
|
98
98
|
#legend .sw-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
99
99
|
#legend .ring { width: 8px; height: 8px; border-radius: 50%; flex: none;
|
|
100
100
|
background: transparent; box-shadow: 0 0 0 1.5px currentColor inset; }
|
|
101
|
+
/* Offline is hollow dashed (shape channel), not a filled/inset-solid ring. */
|
|
102
|
+
#legend .ring.off {
|
|
103
|
+
background: transparent; box-shadow: none;
|
|
104
|
+
border: 1.5px dashed currentColor;
|
|
105
|
+
}
|
|
101
106
|
|
|
102
107
|
/* ── detail panel (right, floating) ── */
|
|
103
108
|
#detail {
|
|
@@ -135,6 +140,10 @@
|
|
|
135
140
|
.d-row { display: flex; gap: 10px; font-size: 11.5px; line-height: 1.4; }
|
|
136
141
|
.d-row .k { color: var(--faint); min-width: 64px; flex: none; }
|
|
137
142
|
.d-row .v { color: var(--fg); word-break: break-word; }
|
|
143
|
+
.d-row .v.muted { color: var(--faint); font-style: italic; }
|
|
144
|
+
.d-row .v .hmark { margin-right: 4px; font-weight: 700; }
|
|
145
|
+
.d-row .v.att-dnd { color: #db6d28; font-weight: 600; }
|
|
146
|
+
.d-row .v.att-focus { color: var(--chat); font-weight: 600; }
|
|
138
147
|
.d-msgs { display: flex; flex-direction: column; gap: 5px; }
|
|
139
148
|
.d-msg { font-size: 11px; line-height: 1.45; padding: 7px 10px 8px; border-radius: 6px;
|
|
140
149
|
background: #ffffff06; border-left: 2px solid var(--line); }
|
|
@@ -195,13 +204,16 @@
|
|
|
195
204
|
<span class="ls">agents</span>
|
|
196
205
|
<span class="row"><span class="ring" style="color:var(--working)"></span> working</span>
|
|
197
206
|
<span class="row"><span class="ring" style="color:var(--waiting)"></span> waiting · needs input</span>
|
|
198
|
-
<span class="row"><span class="ring" style="color:var(--idle)"></span> idle
|
|
207
|
+
<span class="row"><span class="ring" style="color:var(--idle)"></span> idle (filled)</span>
|
|
208
|
+
<span class="row"><span class="ring off" style="color:var(--idle);opacity:.85"></span> offline (hollow dashed)</span>
|
|
209
|
+
<span class="row"><span style="font-size:12px;color:#db6d28;font-weight:700">◼</span> dnd · <span style="font-size:12px;color:var(--chat);font-weight:700">◉</span> focus</span>
|
|
199
210
|
</div>
|
|
200
211
|
</div>
|
|
201
212
|
|
|
202
213
|
<aside id="detail" class="glass"></aside>
|
|
203
214
|
<div class="hint" id="hint">click a node for detail · scroll to zoom · drag to pan</div>
|
|
204
215
|
|
|
216
|
+
<script src="/harness.js"></script>
|
|
205
217
|
<script src="/graph.js"></script>
|
|
206
218
|
</body>
|
|
207
219
|
</html>
|
package/dist/web/graph.js
CHANGED
|
@@ -26,6 +26,33 @@
|
|
|
26
26
|
const MEM_OFF = "#5a6472"; // a durable member whose presence is offline ("member, currently offline")
|
|
27
27
|
const TRAFFIC_COLD = 0.02; // heat below which a NON-member (traffic-only) spoke is pruned
|
|
28
28
|
const FEED_STALE_MS = 45000; // membership feed older than this reads "stale" (daemon polls ~15s)
|
|
29
|
+
// Harness branding from harness.js (one source with the monitor). Canvas uses .glyph; DOM uses .svg.
|
|
30
|
+
const HARNESS = window.COTAL_HARNESS || {};
|
|
31
|
+
const harnessLabel = (k) => (HARNESS[k] ? HARNESS[k].label : k);
|
|
32
|
+
const harnessColor = (k) => (HARNESS[k] ? HARNESS[k].color : "#8b949e");
|
|
33
|
+
const harnessGlyph = (k) => (HARNESS[k] ? HARNESS[k].glyph : "·");
|
|
34
|
+
// Attention: open/absent are identical (receives all) — only dnd/focus surface.
|
|
35
|
+
const attMark = (a) => (a === "dnd" ? "◼" : a === "focus" ? "◉" : "");
|
|
36
|
+
/** Fit `provider/model · variant` into maxChars. Prefer dropping the provider prefix over the
|
|
37
|
+
* variant — "gpt-5.6-sol · xhigh" carries more per pixel than "openai/gpt-5.6-sol ·…". */
|
|
38
|
+
function fitModelLabel(model, variant, maxChars) {
|
|
39
|
+
let core = String(model);
|
|
40
|
+
const withVar = (m) => (variant ? `${m} · ${variant}` : m);
|
|
41
|
+
let s = withVar(core);
|
|
42
|
+
if (s.length <= maxChars) return s;
|
|
43
|
+
if (core.includes("/")) {
|
|
44
|
+
core = core.slice(core.lastIndexOf("/") + 1);
|
|
45
|
+
s = withVar(core);
|
|
46
|
+
if (s.length <= maxChars) return s;
|
|
47
|
+
}
|
|
48
|
+
if (variant) {
|
|
49
|
+
const suf = ` · ${variant}`;
|
|
50
|
+
const room = maxChars - suf.length;
|
|
51
|
+
if (room >= 4) return `${core.slice(0, room - 1)}…${suf}`;
|
|
52
|
+
return String(variant).length <= maxChars ? String(variant) : `${String(variant).slice(0, maxChars - 1)}…`;
|
|
53
|
+
}
|
|
54
|
+
return core.length <= maxChars ? core : `${core.slice(0, maxChars - 1)}…`;
|
|
55
|
+
}
|
|
29
56
|
|
|
30
57
|
// ── state ──
|
|
31
58
|
const hubs = new Map(); // channel -> hub node
|
|
@@ -88,7 +115,7 @@
|
|
|
88
115
|
const id = typeof ref === "object" ? ref.id || ref.name : ref;
|
|
89
116
|
if (!id) return null;
|
|
90
117
|
let a = agents.get(id);
|
|
91
|
-
if (!a) { a = Object.assign({ kind: "agent", id, name: (typeof ref === "object" && ref.name) || shortId(id), role: typeof ref === "object" ? ref.role : undefined, status: "idle", present: false, activity: "", harness: undefined, ts: 0, live: [], durable: [], memberOf: new Map(), r: 6.5, charge: -190, mass: 1, phase: (hash(id) % 1000) / 1000 * 6.283 }, spawn(id, 70)); agents.set(id, a); reheat(); }
|
|
118
|
+
if (!a) { a = Object.assign({ kind: "agent", id, name: (typeof ref === "object" && ref.name) || shortId(id), role: typeof ref === "object" ? ref.role : undefined, status: "idle", present: false, activity: "", harness: undefined, model: undefined, variant: undefined, attention: undefined, ts: 0, live: [], durable: [], memberOf: new Map(), r: 6.5, charge: -190, mass: 1, phase: (hash(id) % 1000) / 1000 * 6.283 }, spawn(id, 70)); agents.set(id, a); reheat(); }
|
|
92
119
|
else if (typeof ref === "object" && ref.name) a.name = ref.name;
|
|
93
120
|
return a;
|
|
94
121
|
}
|
|
@@ -143,30 +170,65 @@
|
|
|
143
170
|
alpha += (0 - alpha) * 0.0228;
|
|
144
171
|
}
|
|
145
172
|
|
|
146
|
-
// ── traffic ──
|
|
173
|
+
// ── traffic (visuals are decoration; graph STATE is not) ──
|
|
174
|
+
// Particle/bloom arrays are bounded. A backgrounded tab stops rAF, so an unbounded queue + the
|
|
175
|
+
// chat onArrive fan-out (one comet per other member) detonates into thousands of particles on
|
|
176
|
+
// return — measured peak 8042 from 120 backlogged chats. Gate enqueue on visibility; hard-cap
|
|
177
|
+
// both arrays (drop oldest; dropped onArrive is skipped — heat already applied on the state path).
|
|
178
|
+
const PARTICLE_CAP = 240;
|
|
179
|
+
const BLOOM_CAP = 80;
|
|
180
|
+
const tabVisible = () => document.visibilityState === "visible";
|
|
147
181
|
const mk = (a, b, color, onArrive, curve) => ({ a, b, t: 0, dur: curve ? 1.4 : 1.1, color, onArrive: onArrive || null, curve: !!curve, trail: [] });
|
|
182
|
+
function pushParticle(p) {
|
|
183
|
+
if (!tabVisible()) return false;
|
|
184
|
+
particles.push(p);
|
|
185
|
+
while (particles.length > PARTICLE_CAP) particles.shift(); // drop oldest; skip its onArrive
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
function pushBloom(b) {
|
|
189
|
+
if (!tabVisible()) return false;
|
|
190
|
+
blooms.push(b);
|
|
191
|
+
while (blooms.length > BLOOM_CAP) blooms.shift();
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
/** Apply fan-out spoke heat without enqueueing comets (used when visuals are gated off). */
|
|
195
|
+
function heatFanOut(channel, from) {
|
|
196
|
+
for (const e of edges.values()) if (e.chan === channel && e.a !== from) e.heat = 1;
|
|
197
|
+
}
|
|
148
198
|
function onMessage({ mode, senderId, msg }) {
|
|
149
199
|
if (!msg) return;
|
|
150
200
|
const from = ensureAgent(senderId ? { id: senderId, name: msg.from?.name, role: msg.from?.role } : msg.from);
|
|
151
201
|
if (from) { from.ts = now(); from.present = true; } // a live sender is a live presence (roster event may lag)
|
|
152
|
-
|
|
202
|
+
// Visual gate: pause chip, mode filter, AND tab visibility. State (heat/recent/roster) always applies.
|
|
203
|
+
const animate = !filter.paused && filter[mode] && tabVisible();
|
|
153
204
|
let toName = null;
|
|
154
205
|
if (mode === "chat" && msg.channel) {
|
|
155
206
|
const h = ensureHub(msg.channel);
|
|
156
207
|
if (from) chatHit(from, msg.channel, now()).heat = 1;
|
|
157
208
|
// inbound: sender → hub, then the hub flashes and fans the post back out to every other member on
|
|
158
209
|
// the channel (their spokes glow as the wave reaches them) — a real broadcast.
|
|
159
|
-
if (animate && from && h)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
210
|
+
if (animate && from && h) {
|
|
211
|
+
pushParticle(mk(from, h, MODE.chat, () => {
|
|
212
|
+
pushBloom({ x: h.x, y: h.y, t: 0, dur: 0.95, color: MODE.chat, r0: h.r });
|
|
213
|
+
for (const e of edges.values()) if (e.chan === msg.channel && e.a !== from) {
|
|
214
|
+
e.heat = 1;
|
|
215
|
+
pushParticle(mk(h, e.a, MODE.chat, null, false));
|
|
216
|
+
}
|
|
217
|
+
}));
|
|
218
|
+
} else if (from && h) {
|
|
219
|
+
// No comet: still heat every member spoke so the skeleton reflects the broadcast on return.
|
|
220
|
+
heatFanOut(msg.channel, from);
|
|
221
|
+
}
|
|
163
222
|
} else if (mode === "unicast") {
|
|
164
223
|
const to = typeof msg.to === "string" ? agents.get(msg.to) : msg.to && agents.get(msg.to.id);
|
|
165
224
|
toName = to?.name || (typeof msg.to === "string" ? shortId(msg.to) : msg.to?.name);
|
|
166
|
-
if (from && to && from !== to) {
|
|
225
|
+
if (from && to && from !== to) {
|
|
226
|
+
dmHit(from, to, now()).heat = 1;
|
|
227
|
+
if (animate) pushParticle(mk(from, to, MODE.unicast, null, true));
|
|
228
|
+
}
|
|
167
229
|
} else if (mode === "anycast") {
|
|
168
230
|
toName = "@" + (msg.toService || "");
|
|
169
|
-
if (animate && from)
|
|
231
|
+
if (animate && from) pushBloom({ x: from.x, y: from.y, t: 0, dur: 1.0, color: MODE.anycast, r0: from.r });
|
|
170
232
|
}
|
|
171
233
|
recent.push({ mode, from: from?.name, fromId: from?.id, to: toName, chan: msg.channel, text: partsText(msg), ts: msg.ts || now() });
|
|
172
234
|
if (recent.length > 80) recent.shift();
|
|
@@ -177,7 +239,12 @@
|
|
|
177
239
|
for (const p of list) {
|
|
178
240
|
if (p.card?.kind === "endpoint") continue;
|
|
179
241
|
const a = ensureAgent({ id: p.card.id, name: p.card.name, role: p.card.role });
|
|
180
|
-
a.status = p.status; a.activity = p.activity || ""; a.role = p.card.role;
|
|
242
|
+
a.status = p.status; a.activity = p.activity || ""; a.role = p.card.role;
|
|
243
|
+
a.harness = p.card.meta?.connector; a.model = p.card.meta?.model; a.variant = p.card.meta?.variant;
|
|
244
|
+
a.attention = p.attention; // open/absent both mean receives-all; only dnd/focus render
|
|
245
|
+
// Card legibility fields the detail panel renders (same source as the Monitor's Agent Detail).
|
|
246
|
+
a.description = p.card.description; a.tags = p.card.tags; a.channelModes = p.channelModes;
|
|
247
|
+
a.ts = p.ts;
|
|
181
248
|
a.present = true; // in the roster = a live presence (the authority for isOffline)
|
|
182
249
|
seen.add(a.id);
|
|
183
250
|
}
|
|
@@ -333,22 +400,61 @@
|
|
|
333
400
|
ctx.lineWidth = 1.5; ctx.strokeStyle = rgba(MODE.chat, 0.95 * dim); ctx.stroke();
|
|
334
401
|
ctx.fillStyle = rgba("#cfe2ff", dim); ctx.font = "600 12.5px var(--font), sans-serif"; ctx.fillText("#" + h.name, h.x, h.y + h.r + 13);
|
|
335
402
|
}
|
|
336
|
-
// Label gate counts VISIBLE agents (what's actually drawn), not agents.size — the Map also holds
|
|
337
|
-
// hidden offline ghosts (durable members kept for "member, currently offline"), which would push the
|
|
338
|
-
// count past the threshold and suppress every label even with only a handful of agents online.
|
|
339
|
-
let shown = 0; for (const a of agents.values()) if (!isHidden(a)) shown++;
|
|
340
403
|
for (const a of agents.values()) {
|
|
341
404
|
if (isHidden(a)) continue;
|
|
342
|
-
const col = STAT[a.status] || STAT.idle, focus = a === hover || a === sel, off = a.status === "offline";
|
|
405
|
+
const col = STAT[a.status] || STAT.idle, focus = a === hover || a === sel, off = a.status === "offline", idle = a.status === "idle";
|
|
343
406
|
const r = a.r + Math.sin(t * 0.8 + a.phase) * 0.4;
|
|
344
407
|
if (a.status === "waiting") { const pulse = 0.5 + 0.5 * Math.sin(t * 1.7); for (const o of [0, 0.5]) { ctx.beginPath(); ctx.arc(a.x, a.y, r + 5 + ((pulse + o) % 1) * 9, 0, 2 * Math.PI); ctx.strokeStyle = rgba(STAT.waiting, (1 - ((pulse + o) % 1)) * 0.45); ctx.lineWidth = 1.6; ctx.stroke(); } }
|
|
345
408
|
// wide reader (subscribes `>`/`*`): a faint dashed halo — "reads all channels" without a spoke per hub
|
|
346
409
|
if (a.wideReader) { ctx.save(); ctx.setLineDash([2, 3]); ctx.beginPath(); ctx.arc(a.x, a.y, r + 4.5, 0, 2 * Math.PI); ctx.strokeStyle = rgba(MEM_LIVE, off ? 0.3 : 0.6); ctx.lineWidth = 1.2; ctx.stroke(); ctx.restore(); }
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
410
|
+
// Shape channel (never colour alone): offline = hollow ring; idle = filled disc; working/waiting = filled + glow.
|
|
411
|
+
ctx.save(); ctx.shadowColor = col; ctx.shadowBlur = focus ? 20 : off ? 0 : idle ? 6 : 13;
|
|
412
|
+
if (off) {
|
|
413
|
+
ctx.beginPath(); ctx.arc(a.x, a.y, r, 0, 2 * Math.PI);
|
|
414
|
+
ctx.fillStyle = "#141b26"; ctx.fill();
|
|
415
|
+
ctx.lineWidth = 2; ctx.strokeStyle = rgba(col, 0.85); ctx.setLineDash([2.5, 2]); ctx.stroke(); ctx.setLineDash([]);
|
|
416
|
+
} else {
|
|
417
|
+
const g = ctx.createRadialGradient(a.x, a.y, 0, a.x, a.y, r); g.addColorStop(0, rgba(col, 1)); g.addColorStop(0.55, rgba(col, 0.55)); g.addColorStop(1, "#141b26");
|
|
418
|
+
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(a.x, a.y, r, 0, 2 * Math.PI); ctx.fill();
|
|
419
|
+
ctx.lineWidth = 2; ctx.strokeStyle = rgba(col, 1); ctx.stroke();
|
|
420
|
+
}
|
|
421
|
+
ctx.restore();
|
|
422
|
+
// Attention mark (dnd/focus only) — glyph at node rim, sized for rest-scale legibility.
|
|
423
|
+
const am = attMark(a.attention);
|
|
424
|
+
if (am && !off) {
|
|
425
|
+
ctx.fillStyle = a.attention === "dnd" ? "#db6d28" : MODE.chat;
|
|
426
|
+
ctx.font = "700 11px var(--font), sans-serif";
|
|
427
|
+
ctx.fillText(am, a.x + r + 2, a.y - r - 1);
|
|
428
|
+
}
|
|
429
|
+
// Labels track SCREEN SPACE, not global node count. A count gate hid every name on a real
|
|
430
|
+
// 19-agent mesh (David's "don't hide model names") and zoom never helped. Rule:
|
|
431
|
+
// • viewport cull — off-screen nodes cost nothing
|
|
432
|
+
// • name when the node's on-screen footprint can hold text (or focus/waiting)
|
|
433
|
+
// • model/harness when zoomed further (or focus) — density follows cam.scale
|
|
434
|
+
const sx = cam.x + a.x * cam.scale, sy = cam.y + a.y * cam.scale;
|
|
435
|
+
const inView = sx >= -40 && sx <= W + 40 && sy >= -40 && sy <= H + 40;
|
|
436
|
+
const foot = 2 * r * cam.scale; // on-screen diameter in CSS px
|
|
437
|
+
const showName = focus || a.status === "waiting" || (inView && foot >= 8);
|
|
438
|
+
const showModel = focus || (inView && foot >= 16);
|
|
439
|
+
if (showName) {
|
|
440
|
+
ctx.fillStyle = focus ? "#ffffff" : "#cdd6e2";
|
|
441
|
+
ctx.font = (focus ? "600 " : "500 ") + "11px var(--font), sans-serif";
|
|
442
|
+
ctx.fillText(a.name, a.x, a.y - r - 8);
|
|
443
|
+
}
|
|
444
|
+
if (showModel) {
|
|
445
|
+
// Budget scales with on-screen footprint (same axis as the label gate). Never eat the
|
|
446
|
+
// variant to save chars — if we must shorten, drop the provider prefix first
|
|
447
|
+
// ("openai/gpt-5.6-sol · xhigh" → "gpt-5.6-sol · xhigh"), then trim the model id.
|
|
448
|
+
const maxChars = focus ? 56 : Math.max(20, Math.min(56, Math.round(foot * 2.2)));
|
|
449
|
+
let sub = "";
|
|
450
|
+
if (a.model) sub = fitModelLabel(a.model, a.variant, maxChars);
|
|
451
|
+
else if (a.harness) sub = harnessLabel(a.harness);
|
|
452
|
+
if (sub) {
|
|
453
|
+
ctx.fillStyle = focus ? "#b6c2d1" : "#8b949e";
|
|
454
|
+
ctx.font = "500 9.5px var(--font), sans-serif";
|
|
455
|
+
ctx.fillText(sub, a.x, a.y - r - (showName ? 19 : 8));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
352
458
|
}
|
|
353
459
|
ctx.globalAlpha = 1;
|
|
354
460
|
}
|
|
@@ -440,6 +546,16 @@
|
|
|
440
546
|
const activeIds = new Set(recent.filter((m) => m.chan === sel.name && m.fromId).map((m) => m.fromId));
|
|
441
547
|
const memberRow = (a) => { const off = a.status === "offline" || (a.memberOf && a.memberOf.get(sel.name) === "durable"); const dotCol = STAT[a.status] || STAT.idle; return `<span class="mtag"><span class="dot" style="background:${off ? MEM_OFF : dotCol}"></span>${esc(a.name)}${activeIds.has(a.id) ? '<span class="act">active</span>' : ""}${off ? '<span class="off">offline</span>' : ""}</span>`; };
|
|
442
548
|
const memberList = mem.length ? `<div class="d-tags">${mem.map(memberRow).join("")}</div>` : `<div class="d-block muted">${hiddenOff ? `${hiddenOff} member${hiddenOff === 1 ? "" : "s"} offline (hidden)` : "no subscribers yet"}</div>`;
|
|
549
|
+
// Effective channel policy (from /api/channels, server-resolved). Delivery class = durability;
|
|
550
|
+
// replay = whether a join backfills. Omit a row the feed didn't carry (never guess a default).
|
|
551
|
+
const deliveryRow = sel.deliveryClass
|
|
552
|
+
? `<div class="d-row"><span class="k">delivery</span><span class="v">${esc(sel.deliveryClass)} · ${sel.deliveryClass === "durable" ? "at-least-once for members" : "at-most-once"}</span></div>`
|
|
553
|
+
: "";
|
|
554
|
+
const replayRow = sel.replay === true
|
|
555
|
+
? `<div class="d-row"><span class="k">replay</span><span class="v">${sel.replayWindow ? "on · " + esc(sel.replayWindow) : "on"}</span></div>`
|
|
556
|
+
: sel.replay === false
|
|
557
|
+
? `<div class="d-row"><span class="k">replay</span><span class="v muted">off</span></div>`
|
|
558
|
+
: "";
|
|
443
559
|
el.innerHTML = `<span class="x" id="dx">✕</span>
|
|
444
560
|
<div class="d-kind">channel</div>
|
|
445
561
|
<div class="d-who">#${esc(sel.name)}</div>
|
|
@@ -447,6 +563,8 @@
|
|
|
447
563
|
<div class="d-rows">
|
|
448
564
|
<div class="d-row"><span class="k">subscribers</span><span class="v">${hiddenOff ? `${mem.length} shown <span style="color:var(--faint)">+${hiddenOff} offline hidden</span>` : `${mem.length} agent${mem.length === 1 ? "" : "s"}`}</span></div>
|
|
449
565
|
<div class="d-row"><span class="k">messages</span><span class="v">${sel.msgs || 0}</span></div>
|
|
566
|
+
${deliveryRow}
|
|
567
|
+
${replayRow}
|
|
450
568
|
</div>
|
|
451
569
|
<div class="d-section"><div class="d-label">members</div>${memberList}</div>
|
|
452
570
|
<div class="d-section"><div class="d-label">recent</div><div class="d-msgs">${recentRows((m) => m.chan === sel.name)}</div></div>`;
|
|
@@ -457,13 +575,41 @@
|
|
|
457
575
|
const liveSet = (sel.live || []).filter((c) => c !== ">" && c !== "*").map((c) => `<span class="ctag">#${esc(c)}</span>`).join("");
|
|
458
576
|
const durOnly = (sel.durable || []).filter((c) => !(sel.live || []).includes(c)).map((c) => `<span class="ctag off">#${esc(c)}</span>`).join("");
|
|
459
577
|
const subs = wideChip || liveSet || durOnly ? `<div class="d-tags">${wideChip}${liveSet}${durOnly}</div>` : `<div class="d-block muted">no channel subscriptions</div>`;
|
|
578
|
+
// Identity rows: branded harness (not raw key); model · variant when known; "not reported" only for harness agents.
|
|
579
|
+
const hLabel = sel.harness ? harnessLabel(sel.harness) : "";
|
|
580
|
+
const hColor = sel.harness ? harnessColor(sel.harness) : "";
|
|
581
|
+
const harnessRow = sel.harness
|
|
582
|
+
? `<div class="d-row"><span class="k">harness</span><span class="v"><span class="hmark" style="color:${hColor}">${esc(harnessGlyph(sel.harness))}</span> ${esc(hLabel)}</span></div>`
|
|
583
|
+
: "";
|
|
584
|
+
const modelRow = sel.harness
|
|
585
|
+
? `<div class="d-row"><span class="k">model</span><span class="v${sel.model ? "" : " muted"}">${sel.model ? esc(sel.model) + (sel.variant ? ` · ${esc(sel.variant)}` : "") : "not reported"}</span></div>`
|
|
586
|
+
: sel.model
|
|
587
|
+
? `<div class="d-row"><span class="k">model</span><span class="v">${esc(sel.model)}${sel.variant ? ` · ${esc(sel.variant)}` : ""}</span></div>`
|
|
588
|
+
: "";
|
|
589
|
+
const att = attMark(sel.attention);
|
|
590
|
+
const attRow = att
|
|
591
|
+
? `<div class="d-row"><span class="k">attention</span><span class="v att-${esc(sel.attention)}">${att} ${esc(sel.attention)}</span></div>`
|
|
592
|
+
: "";
|
|
593
|
+
// The card's own description (AgentCard.description) — the same legibility text the Monitor shows.
|
|
594
|
+
const descBlock = sel.description ? `<div class="d-block">${esc(sel.description)}</div>` : "";
|
|
595
|
+
const tagsSection = (sel.tags || []).length
|
|
596
|
+
? `<div class="d-section"><div class="d-label">tags</div><div class="d-tags">${sel.tags.map((t) => `<span class="ctag">${esc(t)}</span>`).join("")}</div></div>`
|
|
597
|
+
: "";
|
|
598
|
+
// Per-channel attention overrides (quiet / muted) — advisory receive-side, not ACL.
|
|
599
|
+
const modeEntries = Object.entries(sel.channelModes || {}).sort(([a], [b]) => a.localeCompare(b));
|
|
600
|
+
const modesSection = modeEntries.length
|
|
601
|
+
? `<div class="d-section"><div class="d-label">channel modes</div><div class="d-tags">${modeEntries.map(([ch, m]) => `<span class="ctag${m === "muted" ? " off" : ""}">#${esc(ch)} · ${esc(m)}</span>`).join("")}</div></div>`
|
|
602
|
+
: "";
|
|
460
603
|
el.innerHTML = `<span class="x" id="dx">✕</span>
|
|
461
604
|
<div class="d-kind">agent</div>
|
|
462
605
|
<div class="d-who">${esc(sel.name)}${sel.role ? `<span class="role">${esc(sel.role)}</span>` : ""}</div>
|
|
463
606
|
<div class="d-status ${sel.status}"><span class="dot"></span>${esc(sel.status)}</div>
|
|
607
|
+
${descBlock}
|
|
464
608
|
<div class="d-section"><div class="d-label">activity</div><div class="d-block ${sel.activity ? "" : "muted"}">${esc(sel.activity || "no current activity")}</div></div>
|
|
609
|
+
${(harnessRow || modelRow || attRow) ? `<div class="d-rows">${harnessRow}${modelRow}${attRow}</div>` : ""}
|
|
465
610
|
<div class="d-section"><div class="d-label">subscribes</div>${subs}</div>
|
|
466
|
-
${
|
|
611
|
+
${modesSection}
|
|
612
|
+
${tagsSection}
|
|
467
613
|
<div class="d-section"><div class="d-label">recent</div><div class="d-msgs">${recentRows((m) => m.from === sel.name || m.to === sel.name)}</div></div>`;
|
|
468
614
|
}
|
|
469
615
|
el.classList.add("open"); $("dx").onclick = closeDetail;
|
|
@@ -487,6 +633,19 @@
|
|
|
487
633
|
// is normalized to px so a mouse notch and a trackpad swipe both feel right.
|
|
488
634
|
canvas.addEventListener("wheel", (e) => { e.preventDefault(); cam.user = true; const px = e.deltaY * (e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? H : 1), f = Math.exp(-px * 0.0015), ns = Math.max(0.3, Math.min(3, cam.scale * f)), w = toWorld(e.clientX, e.clientY); cam.scale = ns; cam.x = e.clientX - w.x * ns; cam.y = e.clientY - w.y * ns; }, { passive: false });
|
|
489
635
|
window.addEventListener("keydown", (e) => { if (e.key === "Escape") closeDetail(); });
|
|
636
|
+
// Backgrounded tab: drop in-flight decoration (rAF is frozen, so mid-flight comets would otherwise
|
|
637
|
+
// detonate their onArrive fan-out on return). State (edges/roster/recent) is untouched. On return:
|
|
638
|
+
// reset lastT so the first frame doesn't integrate a multi-minute dt, and reheat physics so a node
|
|
639
|
+
// set that changed while hidden can re-settle instead of sitting cold.
|
|
640
|
+
document.addEventListener("visibilitychange", () => {
|
|
641
|
+
if (document.visibilityState === "hidden") {
|
|
642
|
+
particles.length = 0;
|
|
643
|
+
blooms.length = 0;
|
|
644
|
+
} else {
|
|
645
|
+
lastT = performance.now();
|
|
646
|
+
reheat();
|
|
647
|
+
}
|
|
648
|
+
});
|
|
490
649
|
$("modes").onclick = (e) => { const c = e.target.closest(".chip"); if (!c) return; const m = c.dataset.mode; filter[m] = !filter[m]; c.classList.toggle("on", filter[m]); };
|
|
491
650
|
$("pause").onclick = () => { filter.paused = !filter.paused; $("pause").classList.toggle("on", filter.paused); $("pause").textContent = filter.paused ? "▶ resume" : "⏸ pause"; };
|
|
492
651
|
$("hideOffline").onclick = () => { filter.hideOffline = !filter.hideOffline; $("hideOffline").classList.toggle("on", filter.hideOffline); recomputeHubEmpty(); if (sel && isHidden(sel)) closeDetail(); if (hover && isHidden(hover)) hover = null; reheat(); if (sel) renderDetail(); };
|
|
@@ -502,7 +661,7 @@
|
|
|
502
661
|
fetch("/api/activity?limit=400").then((r) => r.json()).catch(() => []), fetch("/api/dms?limit=400").then((r) => r.json()).catch(() => []),
|
|
503
662
|
]);
|
|
504
663
|
$("space").textContent = "· " + meta.space;
|
|
505
|
-
for (const c of chans) { const h = ensureHub(c.channel); h.msgs = c.messages || 0; h.desc = c.description || ""; }
|
|
664
|
+
for (const c of chans) { const h = ensureHub(c.channel); h.msgs = c.messages || 0; h.desc = c.description || ""; h.deliveryClass = c.deliveryClass; h.replay = c.replay; h.replayWindow = c.replayWindow; }
|
|
506
665
|
updateRoster(roster);
|
|
507
666
|
applyMembership(membership); // authoritative spokes BEFORE traffic seeding (no skeleton flicker)
|
|
508
667
|
for (const e of activity) { const m = e.msg; const a = m?.from?.id && agents.get(m.from.id); if (e.mode === "chat" && m?.channel && a) chatHit(a, m.channel, m.ts || now()); }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Shared harness (host connector) branding for the monitor and graph surfaces.
|
|
2
|
+
// One source of truth: label + colour + SVG (DOM badges) + glyph (canvas text — canvas
|
|
3
|
+
// cannot draw the inline SVG). Loaded as a plain script before app.js / graph.js.
|
|
4
|
+
//
|
|
5
|
+
// Claude and OpenCode marks: public-domain SVG data from Simple Icons (CC0).
|
|
6
|
+
// Hermes / Pi: custom glyphs (no clean official mark). Unknown connectors are not listed
|
|
7
|
+
// here — callers degrade to a neutral name with no invented icon.
|
|
8
|
+
window.COTAL_HARNESS = {
|
|
9
|
+
claude: {
|
|
10
|
+
label: "Claude Code",
|
|
11
|
+
color: "#d97757", // official Claude clay
|
|
12
|
+
glyph: "◆",
|
|
13
|
+
svg: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z"/></svg>`,
|
|
14
|
+
},
|
|
15
|
+
opencode: {
|
|
16
|
+
label: "OpenCode",
|
|
17
|
+
color: "#cdd6e0", // monochrome by brand; rendered light on the dark UI
|
|
18
|
+
glyph: "▣",
|
|
19
|
+
svg: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M22 24H2V0h20zM17 4.8H7v14.4h10z"/></svg>`,
|
|
20
|
+
},
|
|
21
|
+
hermes: {
|
|
22
|
+
label: "Hermes",
|
|
23
|
+
color: "#a78bfa", // no official mark — custom messenger glyph, violet
|
|
24
|
+
glyph: "➤",
|
|
25
|
+
svg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 3 3 10.5l7 2.5 2.5 7L21 3Z"/><path d="M21 3 10 13"/></svg>`,
|
|
26
|
+
},
|
|
27
|
+
pi: {
|
|
28
|
+
label: "Pi",
|
|
29
|
+
color: "#7dd3fc", // no official mark — simple π glyph, sky
|
|
30
|
+
glyph: "π",
|
|
31
|
+
svg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16"/><path d="M9 7v12"/><path d="M15 7v12c0 1.5 1.5 2 3 1"/></svg>`,
|
|
32
|
+
},
|
|
33
|
+
};
|