@cotal-ai/cli 0.7.0 → 0.8.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/commands/down-manifest.d.ts +6 -0
- package/dist/commands/down-manifest.d.ts.map +1 -0
- package/dist/commands/down-manifest.js +282 -0
- package/dist/commands/down-manifest.js.map +1 -0
- package/dist/commands/down.d.ts +5 -3
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +24 -4
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/meshes.js +1 -1
- package/dist/commands/meshes.js.map +1 -1
- package/dist/commands/mint.d.ts.map +1 -1
- package/dist/commands/mint.js +2 -1
- package/dist/commands/mint.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +66 -16
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/spawn-manifest.d.ts +10 -0
- package/dist/commands/spawn-manifest.d.ts.map +1 -0
- package/dist/commands/spawn-manifest.js +197 -0
- package/dist/commands/spawn-manifest.js.map +1 -0
- package/dist/commands/spawn.d.ts.map +1 -1
- package/dist/commands/spawn.js +31 -4
- package/dist/commands/spawn.js.map +1 -1
- package/dist/commands/topology.d.ts +10 -0
- package/dist/commands/topology.d.ts.map +1 -0
- package/dist/commands/topology.js +46 -0
- package/dist/commands/topology.js.map +1 -0
- package/dist/commands/up.d.ts +4 -0
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +112 -2
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/use.d.ts +1 -1
- package/dist/commands/use.d.ts.map +1 -1
- package/dist/commands/use.js +1 -1
- package/dist/commands/use.js.map +1 -1
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +3 -1
- package/dist/commands/web.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/connect.d.ts +18 -16
- package/dist/lib/connect.d.ts.map +1 -1
- package/dist/lib/connect.js +27 -51
- package/dist/lib/connect.js.map +1 -1
- package/dist/lib/delivery-proc.d.ts.map +1 -1
- package/dist/lib/delivery-proc.js +2 -1
- package/dist/lib/delivery-proc.js.map +1 -1
- package/dist/lib/manager-proc.d.ts +4 -0
- package/dist/lib/manager-proc.d.ts.map +1 -1
- package/dist/lib/manager-proc.js +17 -0
- package/dist/lib/manager-proc.js.map +1 -1
- package/dist/lib/manifest/apply.d.ts +29 -0
- package/dist/lib/manifest/apply.d.ts.map +1 -0
- package/dist/lib/manifest/apply.js +138 -0
- package/dist/lib/manifest/apply.js.map +1 -0
- package/dist/lib/manifest/errors.d.ts +21 -0
- package/dist/lib/manifest/errors.d.ts.map +1 -0
- package/dist/lib/manifest/errors.js +19 -0
- package/dist/lib/manifest/errors.js.map +1 -0
- package/dist/lib/manifest/index.d.ts +13 -0
- package/dist/lib/manifest/index.d.ts.map +1 -0
- package/dist/lib/manifest/index.js +21 -0
- package/dist/lib/manifest/index.js.map +1 -0
- package/dist/lib/manifest/ledger.d.ts +81 -0
- package/dist/lib/manifest/ledger.d.ts.map +1 -0
- package/dist/lib/manifest/ledger.js +213 -0
- package/dist/lib/manifest/ledger.js.map +1 -0
- package/dist/lib/manifest/live.d.ts +25 -0
- package/dist/lib/manifest/live.d.ts.map +1 -0
- package/dist/lib/manifest/live.js +61 -0
- package/dist/lib/manifest/live.js.map +1 -0
- package/dist/lib/manifest/model.d.ts +71 -0
- package/dist/lib/manifest/model.d.ts.map +1 -0
- package/dist/lib/manifest/model.js +2 -0
- package/dist/lib/manifest/model.js.map +1 -0
- package/dist/lib/manifest/preflight.d.ts +12 -0
- package/dist/lib/manifest/preflight.d.ts.map +1 -0
- package/dist/lib/manifest/preflight.js +43 -0
- package/dist/lib/manifest/preflight.js.map +1 -0
- package/dist/lib/manifest/prepare.d.ts +57 -0
- package/dist/lib/manifest/prepare.d.ts.map +1 -0
- package/dist/lib/manifest/prepare.js +95 -0
- package/dist/lib/manifest/prepare.js.map +1 -0
- package/dist/lib/manifest/render.d.ts +41 -0
- package/dist/lib/manifest/render.d.ts.map +1 -0
- package/dist/lib/manifest/render.js +177 -0
- package/dist/lib/manifest/render.js.map +1 -0
- package/dist/lib/manifest/resolve.d.ts +5 -0
- package/dist/lib/manifest/resolve.d.ts.map +1 -0
- package/dist/lib/manifest/resolve.js +185 -0
- package/dist/lib/manifest/resolve.js.map +1 -0
- package/dist/lib/manifest/schema.d.ts +103 -0
- package/dist/lib/manifest/schema.d.ts.map +1 -0
- package/dist/lib/manifest/schema.js +77 -0
- package/dist/lib/manifest/schema.js.map +1 -0
- package/dist/lib/manifest/spawn-plan.d.ts +87 -0
- package/dist/lib/manifest/spawn-plan.d.ts.map +1 -0
- package/dist/lib/manifest/spawn-plan.js +75 -0
- package/dist/lib/manifest/spawn-plan.js.map +1 -0
- package/dist/lib/meshes.d.ts +1 -7
- package/dist/lib/meshes.d.ts.map +1 -1
- package/dist/lib/meshes.js +5 -14
- package/dist/lib/meshes.js.map +1 -1
- package/dist/lib/onboard.js +1 -1
- package/dist/lib/onboard.js.map +1 -1
- package/dist/lib/paths.js +1 -1
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/status.d.ts.map +1 -1
- package/dist/lib/status.js +2 -1
- package/dist/lib/status.js.map +1 -1
- package/dist/web/graph.html +3 -0
- package/dist/web/graph.js +76 -21
- package/package.json +6 -2
package/dist/web/graph.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
const recent = [];
|
|
38
38
|
const feed = { asOf: undefined, available: false }; // membership-feed freshness
|
|
39
39
|
const cam = { x: 0, y: 0, scale: 1, ready: false, user: false };
|
|
40
|
-
const filter = { chat: true, unicast: true, anycast: true, window: 30, paused: false };
|
|
40
|
+
const filter = { chat: true, unicast: true, anycast: true, window: 30, paused: false, hideOffline: true, hideEmpty: true };
|
|
41
41
|
let W = 0, H = 0, DPR = 1, hover = null, sel = null, lastT = 0, alpha = 1;
|
|
42
42
|
|
|
43
43
|
// ── utils ──
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
const id = typeof ref === "object" ? ref.id || ref.name : ref;
|
|
89
89
|
if (!id) return null;
|
|
90
90
|
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", 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(); }
|
|
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(); }
|
|
92
92
|
else if (typeof ref === "object" && ref.name) a.name = ref.name;
|
|
93
93
|
return a;
|
|
94
94
|
}
|
|
@@ -109,7 +109,8 @@
|
|
|
109
109
|
function link(a, b, len, k) { let dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy) || 1; const f = (d - len) * k * alpha, fx = (dx / d) * f, fy = (dy / d) * f; a.vx += fx; a.vy += fy; b.vx -= fx; b.vy -= fy; }
|
|
110
110
|
function physics() {
|
|
111
111
|
if (alpha < 0.004 || filter.paused) return;
|
|
112
|
-
|
|
112
|
+
// while hiding, ghosts leave the sim entirely so the visible graph isn't laid out around invisible nodes (reheat on toggle re-settles)
|
|
113
|
+
const ns = [...hubs.values(), ...agents.values()].filter((n) => !isHidden(n));
|
|
113
114
|
for (let i = 0; i < ns.length; i++) {
|
|
114
115
|
const a = ns[i];
|
|
115
116
|
for (let j = i + 1; j < ns.length; j++) {
|
|
@@ -120,10 +121,10 @@
|
|
|
120
121
|
a.vx += (dx / d) * q; a.vy += (dy / d) * q; b.vx -= (dx / d) * q; b.vy -= (dy / d) * q;
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
|
-
for (const e of edges.values()) { const h = hubs.get(e.chan); if (h) link(e.a, h, 105, 0.08); }
|
|
124
|
-
for (const d of dms.values()) link(d.a, d.b, 165, 0.03);
|
|
124
|
+
for (const e of edges.values()) { const h = hubs.get(e.chan); if (h && !isHidden(h) && !isHiddenMember(e)) link(e.a, h, 105, 0.08); }
|
|
125
|
+
for (const d of dms.values()) if (!isHidden(d.a) && !isHidden(d.b)) link(d.a, d.b, 165, 0.03);
|
|
125
126
|
// small graphs: faint tangential nudge so agents form a loose ring around their hub instead of a line (decays with alpha)
|
|
126
|
-
if (hubs.size <= 2) for (const a of agents.values()) { const h = (primaryChan(a) && hubs.get(primaryChan(a))) || [...hubs.values()][0]; if (h) { const dx = a.x - h.x, dy = a.y - h.y, d = Math.hypot(dx, dy) || 1; a.vx += (-dy / d) * 0.4 * alpha; a.vy += (dx / d) * 0.4 * alpha; } }
|
|
127
|
+
if (hubs.size <= 2) for (const a of agents.values()) { if (isHidden(a)) continue; const h = (primaryChan(a) && hubs.get(primaryChan(a))) || [...hubs.values()][0]; if (h) { const dx = a.x - h.x, dy = a.y - h.y, d = Math.hypot(dx, dy) || 1; a.vx += (-dy / d) * 0.4 * alpha; a.vy += (dx / d) * 0.4 * alpha; } }
|
|
127
128
|
// collision: position-based min-distance — prevents the 1/d² charge singularity + node overlap
|
|
128
129
|
const pad = 10;
|
|
129
130
|
for (let i = 0; i < ns.length; i++) {
|
|
@@ -147,7 +148,7 @@
|
|
|
147
148
|
function onMessage({ mode, senderId, msg }) {
|
|
148
149
|
if (!msg) return;
|
|
149
150
|
const from = ensureAgent(senderId ? { id: senderId, name: msg.from?.name, role: msg.from?.role } : msg.from);
|
|
150
|
-
if (from) from.ts = now();
|
|
151
|
+
if (from) { from.ts = now(); from.present = true; } // a live sender is a live presence (roster event may lag)
|
|
151
152
|
const animate = !filter.paused && filter[mode];
|
|
152
153
|
let toName = null;
|
|
153
154
|
if (mode === "chat" && msg.channel) {
|
|
@@ -177,13 +178,14 @@
|
|
|
177
178
|
if (p.card?.kind === "endpoint") continue;
|
|
178
179
|
const a = ensureAgent({ id: p.card.id, name: p.card.name, role: p.card.role });
|
|
179
180
|
a.status = p.status; a.activity = p.activity || ""; a.role = p.card.role; a.harness = p.card.meta?.connector; a.ts = p.ts;
|
|
181
|
+
a.present = true; // in the roster = a live presence (the authority for isOffline)
|
|
180
182
|
seen.add(a.id);
|
|
181
183
|
}
|
|
182
184
|
// Drop an agent as soon as it goes offline OR leaves the roster (main's ghost fix, c9e9000) — EXCEPT
|
|
183
185
|
// keep it if it's still a feed member: a durable member whose presence is offline must persist to
|
|
184
186
|
// render as "member, currently offline" (the feed's durable arm survives offline). Membership is
|
|
185
187
|
// broker-truth, applied separately; presence no longer carries channels.
|
|
186
|
-
for (const [id, a] of agents) if ((!seen.has(id) || a.status === "offline") && !(a.wideReader || (a.memberOf && a.memberOf.size))) { agents.delete(id); for (const k of [...edges.keys()]) if (edges.get(k).a === a) edges.delete(k); for (const k of [...dms.keys()]) { const d = dms.get(k); if (d.a === a || d.b === a) dms.delete(k); } reheat(); if (sel === a) closeDetail(); }
|
|
188
|
+
for (const [id, a] of agents) { if (!seen.has(id)) a.present = false; if ((!seen.has(id) || a.status === "offline") && !(a.wideReader || (a.memberOf && a.memberOf.size))) { agents.delete(id); for (const k of [...edges.keys()]) if (edges.get(k).a === a) edges.delete(k); for (const k of [...dms.keys()]) { const d = dms.get(k); if (d.a === a || d.b === a) dms.delete(k); } reheat(); if (sel === a) closeDetail(); } }
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
// ── membership (authoritative spokes) ──
|
|
@@ -206,7 +208,8 @@
|
|
|
206
208
|
// An agent that dropped out of the feed entirely is no longer a member of anything (incl. a wide reader,
|
|
207
209
|
// which carries the flag but no concrete edges).
|
|
208
210
|
for (const a of agents.values()) if (!present.has(a.id) && (a.wideReader || (a.memberOf && a.memberOf.size))) { a.live = []; a.durable = []; a.wideReader = false; const empty = new Map(); pruneMemberEdges(a, empty); a.memberOf = empty; }
|
|
209
|
-
|
|
211
|
+
recomputeHubEmpty(); // these mutations change hub visibility — refresh before the detail/selection check below
|
|
212
|
+
if (sel) { if (isHidden(sel)) closeDetail(); else renderDetail(); }
|
|
210
213
|
}
|
|
211
214
|
// Drop this agent's membership edges that are no longer in `keep`; a still-warm one stays as a fading
|
|
212
215
|
// traffic-only edge (mem:false) and is pruned later when cold, so a comet in flight isn't orphaned.
|
|
@@ -220,7 +223,8 @@
|
|
|
220
223
|
// Prune cold traffic-only spokes (a non-member's post that has faded). Membership spokes persist by
|
|
221
224
|
// membership, never on a timer — they're the resting skeleton, faint at rest, glowing on traffic.
|
|
222
225
|
for (const [k, e] of edges) if (!e.mem && e.heat <= TRAFFIC_COLD && now() - e.last > 1000) { edges.delete(k); reheat(); }
|
|
223
|
-
|
|
226
|
+
recomputeHubEmpty(); // no VISIBLE members = dormant (hidden offline ghosts don't keep it lit); drives hub hide + dim
|
|
227
|
+
if (sel && isHidden(sel)) closeDetail(); // a selection hidden by a membership/roster change (not just the toggle) closes its card
|
|
224
228
|
physics();
|
|
225
229
|
// re-frame only once the sim has cooled, so the camera doesn't chase the re-settle wobble
|
|
226
230
|
if (!cam.user && alpha < 0.12) { const f = fitTarget(); const e = 1 - Math.pow(0.02, dt); cam.x += (f.x - cam.x) * e; cam.y += (f.y - cam.y) * e; cam.scale += (f.scale - cam.scale) * e; }
|
|
@@ -237,14 +241,54 @@
|
|
|
237
241
|
// Hover/select a hub or agent → highlight its membership fan, dim the rest (dandelion mitigation).
|
|
238
242
|
function fanFocus() { return hover && hover.kind === "hub" ? hover : sel && sel.kind === "hub" ? sel : hover && hover.kind === "agent" ? hover : sel && sel.kind === "agent" ? sel : null; }
|
|
239
243
|
function inFan(e, f) { if (!f) return true; return f.kind === "hub" ? e.chan === f.name : e.a === f; }
|
|
244
|
+
// Core offline-ness of an AGENT (toggle-INDEPENDENT): it is NOT a live presence — either presence reports it
|
|
245
|
+
// explicitly `offline`, or it is absent from the presence roster entirely (`present === false`). Presence is
|
|
246
|
+
// the authority here, NOT the membership feed. This is what fixes the "ghost flashes back for ~1s" report: a
|
|
247
|
+
// ghost that left EARLIER (its roster-absence already observed, `present === false`) stays hidden when a later
|
|
248
|
+
// snapshot empties its memberOf — there is no per-node `memberOf.size` heuristic to flip it back to VISIBLE on
|
|
249
|
+
// a membership change. (Membership and roster are independent SSE events; this covers the reported case, where
|
|
250
|
+
// a peer's channel join drops a long-departed ghost. A near-simultaneous disconnect whose membership-drop is
|
|
251
|
+
// processed before its roster-absence can still show the node for one roster interval — presence stays the
|
|
252
|
+
// authority and it self-resolves on the next roster event; review-ux/critic R2.) It also does NOT false-hide a
|
|
253
|
+
// genuinely-connected agent that just sits in no channel (e.g.
|
|
254
|
+
// `manager`/DM-only: roster-present, but absent from the channel feed, so its `live` is an empty DEFAULT, not
|
|
255
|
+
// proof of disconnection — review-freelance R2). `present` is set from the roster in updateRoster; a live
|
|
256
|
+
// message sender is marked present too.
|
|
257
|
+
const isOffline = (n) => n.kind === "agent" && (n.status === "offline" || !n.present);
|
|
258
|
+
// A membership edge that is offline FOR ITS channel: durable-only here, or the agent itself is offline.
|
|
259
|
+
const isOfflineMember = (e) => e.durableOnly || isOffline(e.a);
|
|
260
|
+
// Render/sim visibility. `hide offline` (on by default) collapses offline AGENTS. `hide empty` (on by
|
|
261
|
+
// default) collapses a HUB with no VISIBLE member under the current filters — it reads the cached `h.empty`
|
|
262
|
+
// (kept current by recomputeHubEmpty). So when `hide offline` is ON this
|
|
263
|
+
// means "no ONLINE member"; when it's OFF a channel that still shows offline members keeps its hub — the two
|
|
264
|
+
// toggles don't fight (review-critic R2). Gated on `feed.available`: without the authoritative feed there
|
|
265
|
+
// are no membership edges, so every hub would read empty — we can't tell a quiet channel from an unknown
|
|
266
|
+
// one, so we don't hide. Reading the cached flag keeps isHidden(hub) O(1), not an all-edges scan per call.
|
|
267
|
+
// Hiding suppresses node/spoke rendering, hit-testing, camera framing, and physics participation; the node
|
|
268
|
+
// stays in the model, so toggling reveals it instantly.
|
|
269
|
+
const isHidden = (n) => n.kind === "hub"
|
|
270
|
+
? filter.hideEmpty && feed.available && n.empty
|
|
271
|
+
: filter.hideOffline && isOffline(n);
|
|
272
|
+
// Per-CHANNEL visibility of a membership spoke: hidden when `hide offline` is on AND this edge is offline
|
|
273
|
+
// for its channel (durable-only, or the agent is offline) — so "hide offline" holds per channel, not just
|
|
274
|
+
// per node (an agent live elsewhere keeps its node, but its dashed offline spokes + roster seats drop).
|
|
275
|
+
const isHiddenMember = (e) => filter.hideOffline && isOfflineMember(e);
|
|
276
|
+
// Recompute every hub's `empty` (no VISIBLE member under the current filters) in ONE O(hubs+edges) pass.
|
|
277
|
+
// isHidden(hub) reads this cached flag, so call this synchronously anywhere a hub's visibility is consulted
|
|
278
|
+
// outside the render loop — toggle cleanup + after applyMembership — so detail/selection logic never reads a
|
|
279
|
+
// stale flag (review-critic/freelance/ux R2). The frame loop calls it too, for roster/traffic-driven changes.
|
|
280
|
+
function recomputeHubEmpty() {
|
|
281
|
+
for (const h of hubs.values()) h.empty = true;
|
|
282
|
+
for (const e of edges.values()) if (e.mem && !isHiddenMember(e)) { const h = hubs.get(e.chan); if (h) h.empty = false; }
|
|
283
|
+
}
|
|
240
284
|
|
|
241
285
|
function drawSpokes() {
|
|
242
286
|
ctx.lineCap = "round";
|
|
243
287
|
const f = fanFocus();
|
|
244
288
|
// structure layer: a constant-faint spoke per membership (solid = live, dashed-dim = member-offline)
|
|
245
289
|
for (const e of edges.values()) {
|
|
246
|
-
if (!e.mem) continue;
|
|
247
|
-
const h = hubs.get(e.chan); if (!h) continue;
|
|
290
|
+
if (!e.mem || isHiddenMember(e)) continue;
|
|
291
|
+
const h = hubs.get(e.chan); if (!h || isHidden(h)) continue;
|
|
248
292
|
const off = e.durableOnly || e.a.status === "offline";
|
|
249
293
|
const lit = inFan(e, f);
|
|
250
294
|
ctx.beginPath(); ctx.moveTo(e.a.x, e.a.y); ctx.lineTo(h.x, h.y);
|
|
@@ -255,12 +299,14 @@
|
|
|
255
299
|
ctx.setLineDash([]);
|
|
256
300
|
// activity layer: traffic glow on top (members + transient non-member posts)
|
|
257
301
|
ctx.globalCompositeOperation = "lighter";
|
|
258
|
-
for (const e of edges.values()) { const h = hubs.get(e.chan); if (!h || e.heat <= 0.02) continue; const lit = inFan(e, f); ctx.beginPath(); ctx.moveTo(e.a.x, e.a.y); ctx.lineTo(h.x, h.y); ctx.strokeStyle = rgba(MODE.chat, Math.min(0.55, e.heat * 0.55) * (lit ? 1 : 0.35)); ctx.lineWidth = 1 + e.heat * 1.6; ctx.stroke(); }
|
|
302
|
+
for (const e of edges.values()) { const h = hubs.get(e.chan); if (!filter.chat || !h || isHidden(h) || e.heat <= 0.02 || isHiddenMember(e)) continue; const lit = inFan(e, f); ctx.beginPath(); ctx.moveTo(e.a.x, e.a.y); ctx.lineTo(h.x, h.y); ctx.strokeStyle = rgba(MODE.chat, Math.min(0.55, e.heat * 0.55) * (lit ? 1 : 0.35)); ctx.lineWidth = 1 + e.heat * 1.6; ctx.stroke(); }
|
|
259
303
|
ctx.globalCompositeOperation = "source-over"; ctx.globalAlpha = 1;
|
|
260
304
|
}
|
|
261
305
|
function drawDmEdges() {
|
|
306
|
+
if (!filter.unicast) return; // the `direct` chip filters DM traffic — including the persistent DM curves
|
|
262
307
|
ctx.setLineDash([3, 4]);
|
|
263
308
|
for (const d of dms.values()) {
|
|
309
|
+
if (isHidden(d.a) || isHidden(d.b)) continue;
|
|
264
310
|
const mx = (d.a.x + d.b.x) / 2, my = (d.a.y + d.b.y) / 2, nx = -(d.b.y - d.a.y), ny = d.b.x - d.a.x, len = Math.hypot(nx, ny) || 1;
|
|
265
311
|
const cx = mx + (nx / len) * 24, cy = my + (ny / len) * 24;
|
|
266
312
|
ctx.beginPath(); ctx.moveTo(d.a.x, d.a.y); ctx.quadraticCurveTo(cx, cy, d.b.x, d.b.y);
|
|
@@ -272,6 +318,7 @@
|
|
|
272
318
|
ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
273
319
|
const t = performance.now() / 1000;
|
|
274
320
|
for (const h of hubs.values()) {
|
|
321
|
+
if (isHidden(h)) continue;
|
|
275
322
|
const focus = h === hover || h === sel, dim = h.empty ? 0.55 : 1; // dormant hubs read quieter, not gone
|
|
276
323
|
ctx.save(); ctx.shadowColor = MODE.chat; ctx.shadowBlur = (focus ? 28 : 16) * dim;
|
|
277
324
|
const g = ctx.createRadialGradient(h.x, h.y, 0, h.x, h.y, h.r); g.addColorStop(0, h.empty ? "#16314f" : "#2b5a8f"); g.addColorStop(1, "#0c1726");
|
|
@@ -280,6 +327,7 @@
|
|
|
280
327
|
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);
|
|
281
328
|
}
|
|
282
329
|
for (const a of agents.values()) {
|
|
330
|
+
if (isHidden(a)) continue;
|
|
283
331
|
const col = STAT[a.status] || STAT.idle, focus = a === hover || a === sel, off = a.status === "offline";
|
|
284
332
|
const r = a.r + Math.sin(t * 0.8 + a.phase) * 0.4;
|
|
285
333
|
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(); } }
|
|
@@ -311,7 +359,10 @@
|
|
|
311
359
|
function drawParticles(dt) {
|
|
312
360
|
ctx.globalCompositeOperation = "lighter"; ctx.lineCap = "round";
|
|
313
361
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
314
|
-
const p = particles[i]; if (!filter.paused) p.t += dt / p.dur;
|
|
362
|
+
const p = particles[i]; if (!filter.paused) p.t += dt / p.dur;
|
|
363
|
+
// a fan-out comet to a hidden offline member would fly to empty space — still tick + retire it, just don't draw
|
|
364
|
+
if (isHidden(p.a) || isHidden(p.b)) { if (p.t >= 1) { if (p.onArrive) p.onArrive(); particles.splice(i, 1); } continue; }
|
|
365
|
+
const t = ease(Math.min(1, p.t)); let x, y;
|
|
315
366
|
if (p.curve) { const mx = (p.a.x + p.b.x) / 2, my = (p.a.y + p.b.y) / 2, nx = -(p.b.y - p.a.y), ny = p.b.x - p.a.x, len = Math.hypot(nx, ny) || 1, cx = mx + (nx / len) * 24, cy = my + (ny / len) * 24, u = 1 - t; x = u * u * p.a.x + 2 * u * t * cx + t * t * p.b.x; y = u * u * p.a.y + 2 * u * t * cy + t * t * p.b.y; }
|
|
316
367
|
else { x = p.a.x + (p.b.x - p.a.x) * t; y = p.a.y + (p.b.y - p.a.y) * t; }
|
|
317
368
|
if (!filter.paused) { p.trail.push(x, y); if (p.trail.length > 12) p.trail.splice(0, p.trail.length - 12); }
|
|
@@ -338,16 +389,16 @@
|
|
|
338
389
|
function fitTarget() {
|
|
339
390
|
const ns = [...hubs.values(), ...agents.values()]; if (!ns.length) return { x: W / 2, y: H / 2, scale: 1 };
|
|
340
391
|
// content nodes drive the frame; empty hubs only nudge the padding so one stray node can't shrink the live graph
|
|
341
|
-
const content = ns.filter((n) => !(n.kind === "hub" && n.empty)), frame = content.length ? content : ns;
|
|
392
|
+
const content = ns.filter((n) => !(n.kind === "hub" && n.empty) && !isHidden(n)), frame = content.length ? content : ns;
|
|
342
393
|
let a = 1e9, b = 1e9, c = -1e9, d = -1e9;
|
|
343
394
|
for (const n of frame) { const r = n.r + 40; a = Math.min(a, n.x - r); c = Math.max(c, n.x + r); b = Math.min(b, n.y - r); d = Math.max(d, n.y + r); }
|
|
344
|
-
for (const h of hubs.values()) if (h.empty) { a = Math.min(a, h.x - 20); c = Math.max(c, h.x + 20); b = Math.min(b, h.y - 20); d = Math.max(d, h.y + 20); }
|
|
395
|
+
for (const h of hubs.values()) if (h.empty && !isHidden(h)) { a = Math.min(a, h.x - 20); c = Math.max(c, h.x + 20); b = Math.min(b, h.y - 20); d = Math.max(d, h.y + 20); }
|
|
345
396
|
const bw = c - a || 1, bh = d - b || 1, pad = 90, maxScale = ns.length <= 6 ? 2.4 : 1.6;
|
|
346
397
|
const scale = Math.max(0.35, Math.min(maxScale, Math.min((W - pad * 2) / bw, (H - pad * 2) / bh)));
|
|
347
398
|
return { x: W / 2 - ((a + c) / 2) * scale, y: H / 2 - ((b + d) / 2) * scale, scale };
|
|
348
399
|
}
|
|
349
400
|
const toWorld = (sx, sy) => ({ x: (sx - cam.x) / cam.scale, y: (sy - cam.y) / cam.scale });
|
|
350
|
-
function pick(sx, sy) { const w = toWorld(sx, sy); let best = null, bd = 1e9; for (const n of [...hubs.values(), ...agents.values()]) { const d = Math.hypot(n.x - w.x, n.y - w.y); if (d < n.r + 8 && d < bd) { bd = d; best = n; } } return best; }
|
|
401
|
+
function pick(sx, sy) { const w = toWorld(sx, sy); let best = null, bd = 1e9; for (const n of [...hubs.values(), ...agents.values()]) { if (isHidden(n)) continue; const d = Math.hypot(n.x - w.x, n.y - w.y); if (d < n.r + 8 && d < bd) { bd = d; best = n; } } return best; }
|
|
351
402
|
|
|
352
403
|
// ── membership freshness pill ──
|
|
353
404
|
function setFeed() {
|
|
@@ -371,17 +422,19 @@
|
|
|
371
422
|
if (sel.kind === "hub") {
|
|
372
423
|
// members from the broker feed (subscribed), split into live vs member-currently-offline; plus a
|
|
373
424
|
// "recently active" subset (who actually posted here) vs just-subscribed.
|
|
374
|
-
const
|
|
375
|
-
|
|
425
|
+
const memEdges = [...edges.values()].filter((e) => e.chan === sel.name && e.mem);
|
|
426
|
+
// hide per MEMBERSHIP edge: an agent live elsewhere but durable-only here is offline FOR THIS channel
|
|
427
|
+
const mem = memEdges.filter((e) => !isHiddenMember(e)).map((e) => e.a).sort((x, y) => x.name.localeCompare(y.name));
|
|
428
|
+
const hiddenOff = memEdges.length - mem.length;
|
|
376
429
|
const activeIds = new Set(recent.filter((m) => m.chan === sel.name && m.fromId).map((m) => m.fromId));
|
|
377
430
|
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>`; };
|
|
378
|
-
const memberList = mem.length ? `<div class="d-tags">${mem.map(memberRow).join("")}</div>` : `<div class="d-block muted"
|
|
431
|
+
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>`;
|
|
379
432
|
el.innerHTML = `<span class="x" id="dx">✕</span>
|
|
380
433
|
<div class="d-kind">channel</div>
|
|
381
434
|
<div class="d-who">#${esc(sel.name)}</div>
|
|
382
435
|
${sel.desc ? `<div class="d-block">${esc(sel.desc)}</div>` : ""}
|
|
383
436
|
<div class="d-rows">
|
|
384
|
-
<div class="d-row"><span class="k">subscribers</span><span class="v">${mem.length} agent${mem.length === 1 ? "" : "s"}</span></div>
|
|
437
|
+
<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>
|
|
385
438
|
<div class="d-row"><span class="k">messages</span><span class="v">${sel.msgs || 0}</span></div>
|
|
386
439
|
</div>
|
|
387
440
|
<div class="d-section"><div class="d-label">members</div>${memberList}</div>
|
|
@@ -416,6 +469,8 @@
|
|
|
416
469
|
window.addEventListener("keydown", (e) => { if (e.key === "Escape") closeDetail(); });
|
|
417
470
|
$("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]); };
|
|
418
471
|
$("pause").onclick = () => { filter.paused = !filter.paused; $("pause").classList.toggle("on", filter.paused); $("pause").textContent = filter.paused ? "▶ resume" : "⏸ pause"; };
|
|
472
|
+
$("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(); };
|
|
473
|
+
$("hideEmpty").onclick = () => { filter.hideEmpty = !filter.hideEmpty; $("hideEmpty").classList.toggle("on", filter.hideEmpty); recomputeHubEmpty(); if (sel && isHidden(sel)) closeDetail(); if (hover && isHidden(hover)) hover = null; reheat(); if (sel) renderDetail(); };
|
|
419
474
|
$("legendToggle").onclick = () => $("legend").classList.toggle("collapsed");
|
|
420
475
|
function setConn(live) { const el = $("conn"); el.classList.toggle("down", !live); el.querySelector(".t").textContent = live ? "live" : "disconnected"; }
|
|
421
476
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cotal-ai/cli",
|
|
3
3
|
"description": "Cotal mesh CLI: up, join, watch, console, web, spawn, mint, channels, history.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
"@clack/prompts": "^1.0.0",
|
|
22
22
|
"ink": "^6.0.0",
|
|
23
23
|
"react": "^19.0.0",
|
|
24
|
-
"
|
|
24
|
+
"yaml": "^2.9.0",
|
|
25
|
+
"zod": "^4.4.3",
|
|
26
|
+
"@cotal-ai/workspace": "0.8.0",
|
|
27
|
+
"@cotal-ai/core": "0.8.0"
|
|
25
28
|
},
|
|
26
29
|
"optionalDependencies": {
|
|
27
30
|
"@eplightning/nats-server-darwin-arm64": "^2.14.0",
|
|
@@ -42,6 +45,7 @@
|
|
|
42
45
|
},
|
|
43
46
|
"scripts": {
|
|
44
47
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
+
"test": "tsx smoke/manifest.smoke.ts",
|
|
45
49
|
"build": "tsc -p tsconfig.json && rm -rf dist/web && cp -R src/web dist/web"
|
|
46
50
|
}
|
|
47
51
|
}
|