super-dsh 0.1.5 → 0.1.9
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/client/index.js +1010 -345
- package/cordis.patch.yml +22 -5
- package/package.json +8 -3
- package/superd-hub/dist/a2a-delivery.d.ts +189 -0
- package/superd-hub/dist/a2a-delivery.js +240 -0
- package/superd-hub/dist/a2a-host.d.ts +76 -0
- package/superd-hub/dist/a2a-host.js +145 -0
- package/superd-hub/dist/a2a-ingest.d.ts +31 -0
- package/superd-hub/dist/a2a-ingest.js +636 -0
- package/superd-hub/dist/a2a-log.d.ts +17 -0
- package/superd-hub/dist/a2a-log.js +42 -0
- package/superd-hub/dist/a2a-row.d.ts +65 -0
- package/superd-hub/dist/a2a-row.js +162 -0
- package/superd-hub/dist/a2a-service.d.ts +357 -0
- package/superd-hub/dist/a2a-service.js +980 -0
- package/superd-hub/dist/a2a-wire.d.ts +220 -0
- package/superd-hub/dist/a2a-wire.js +305 -0
- package/superd-hub/dist/agent-ids.d.ts +10 -0
- package/superd-hub/dist/agent-ids.js +68 -0
- package/superd-hub/dist/agent-roster.d.ts +32 -17
- package/superd-hub/dist/agent-roster.js +54 -15
- package/superd-hub/dist/agent-status.d.ts +119 -0
- package/superd-hub/dist/agent-status.js +386 -0
- package/superd-hub/dist/client/mesh-api.d.ts +134 -0
- package/superd-hub/dist/client/mesh-api.js +219 -0
- package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
- package/superd-hub/dist/client/mesh-groups.js +66 -0
- package/superd-hub/dist/client/roster.d.ts +48 -0
- package/superd-hub/dist/client/roster.js +96 -0
- package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
- package/superd-hub/dist/exec-pty-channel.js +162 -0
- package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
- package/superd-hub/dist/exec-pty-framing.js +81 -0
- package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
- package/superd-hub/dist/exec-pty-rpc.js +128 -0
- package/superd-hub/dist/exec-pty-session.d.ts +76 -0
- package/superd-hub/dist/exec-pty-session.js +234 -0
- package/superd-hub/dist/exec.d.ts +56 -0
- package/superd-hub/dist/exec.js +225 -0
- package/superd-hub/dist/fusion-transport.d.ts +23 -21
- package/superd-hub/dist/fusion-transport.js +12 -40
- package/superd-hub/dist/gateway.d.ts +39 -2
- package/superd-hub/dist/gateway.js +62 -3
- package/superd-hub/dist/hub-tool.d.ts +174 -0
- package/superd-hub/dist/hub-tool.js +464 -0
- package/superd-hub/dist/index.d.ts +18 -4
- package/superd-hub/dist/index.js +19 -3
- package/superd-hub/dist/machines.d.ts +45 -0
- package/superd-hub/dist/machines.js +125 -0
- package/superd-hub/dist/mesh-federate.d.ts +138 -0
- package/superd-hub/dist/mesh-federate.js +223 -0
- package/superd-hub/dist/mesh-gateway.d.ts +67 -0
- package/superd-hub/dist/mesh-gateway.js +408 -0
- package/superd-hub/dist/mesh-link.d.ts +274 -0
- package/superd-hub/dist/mesh-link.js +441 -0
- package/superd-hub/dist/mesh-rpc.d.ts +123 -0
- package/superd-hub/dist/mesh-rpc.js +250 -0
- package/superd-hub/dist/mux-client.d.ts +71 -0
- package/superd-hub/dist/mux-client.js +281 -0
- package/superd-hub/dist/pack-version.d.ts +2 -0
- package/superd-hub/dist/pack-version.js +30 -0
- package/superd-hub/dist/roster.d.ts +10 -0
- package/superd-hub/dist/roster.js +9 -1
- package/superd-hub/dist/rpc.d.ts +18 -2
- package/superd-hub/dist/rpc.js +34 -4
- package/superd-hub/dist/spawn-world.js +3 -1
- package/superd-hub/dist/switch-core.d.ts +98 -21
- package/superd-hub/dist/switch-core.js +123 -28
- package/superd-hub/dist/system-head.d.ts +22 -12
- package/superd-hub/dist/system-head.js +100 -84
- package/superd-hub/dist/toast-core.d.ts +43 -0
- package/superd-hub/dist/toast-core.js +63 -0
- package/superd-hub/dist/wire-endpoints.d.ts +264 -0
- package/superd-hub/dist/wire-endpoints.js +180 -0
- package/superd-hub/dist/world-join.js +8 -3
- package/superd-hub/dist/world-mount.d.ts +27 -0
- package/superd-hub/dist/world-mount.js +67 -0
- package/superd-hub/lib/client/index.js +1010 -345
- package/superd-hub/package.json +11 -2
package/client/index.js
CHANGED
|
@@ -268,7 +268,9 @@ const NATIVE_LINK = {
|
|
|
268
268
|
key: "native",
|
|
269
269
|
label: "DSH",
|
|
270
270
|
path: "/",
|
|
271
|
-
machine: "local"
|
|
271
|
+
machine: "local",
|
|
272
|
+
ready: true,
|
|
273
|
+
failed: false
|
|
272
274
|
};
|
|
273
275
|
/** Labels for the read-only face's bare keys (it reports keys, not labels). */
|
|
274
276
|
const FALLBACK_LABELS = {
|
|
@@ -292,7 +294,9 @@ function coerceAgents(raw) {
|
|
|
292
294
|
key,
|
|
293
295
|
label: typeof label === "string" ? label : FALLBACK_LABELS[key] ?? key,
|
|
294
296
|
path,
|
|
295
|
-
machine: typeof machine === "string" ? machine :
|
|
297
|
+
machine: typeof machine === "string" && machine !== "" ? machine : void 0,
|
|
298
|
+
ready: entry.ready === true,
|
|
299
|
+
failed: entry.failed === true
|
|
296
300
|
});
|
|
297
301
|
}
|
|
298
302
|
return links.length > 0 ? links : void 0;
|
|
@@ -304,7 +308,8 @@ function rosterFromGlobal() {
|
|
|
304
308
|
}
|
|
305
309
|
/** Normalize the current mount root: `/omp` and `/omp/` are the same page. */
|
|
306
310
|
function currentMountPath() {
|
|
307
|
-
const
|
|
311
|
+
const location$1 = globalThis.location;
|
|
312
|
+
const pathname = location$1 === void 0 ? "/" : location$1.pathname;
|
|
308
313
|
if (pathname === "" || pathname === "/") return "/";
|
|
309
314
|
return pathname.endsWith("/") ? pathname : `${pathname}/`;
|
|
310
315
|
}
|
|
@@ -329,7 +334,8 @@ async function fetchRoster() {
|
|
|
329
334
|
key,
|
|
330
335
|
label: FALLBACK_LABELS[key] ?? key,
|
|
331
336
|
path: `/${key}/`,
|
|
332
|
-
machine: "local"
|
|
337
|
+
machine: "local",
|
|
338
|
+
ready: key === "native"
|
|
333
339
|
});
|
|
334
340
|
}
|
|
335
341
|
if (links.length > 0) return links;
|
|
@@ -338,78 +344,288 @@ async function fetchRoster() {
|
|
|
338
344
|
}
|
|
339
345
|
|
|
340
346
|
//#endregion
|
|
341
|
-
//#region src/client/
|
|
342
|
-
/** Storage slot for the ledger (v1 shape; bump the suffix on breaking edits). */
|
|
343
|
-
const STORAGE_KEY = "superd-hub.machines.v1";
|
|
347
|
+
//#region src/client/mesh-api.ts
|
|
344
348
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
349
|
+
* Machine-mesh data access for the Hub machines panel (mesh P1, Task 5):
|
|
350
|
+
* the client half of `/api/superd/machines` (host side: mesh-rpc.ts) —
|
|
351
|
+
* same-origin fetch behind the standard browser-trust + cookie fence, the
|
|
352
|
+
* roster face's house style (`credentials: 'same-origin'`). The localStorage
|
|
353
|
+
* ledger era (machines-store.ts, retired with this module) ended: the hub's
|
|
354
|
+
* registry is the single source of truth, this file only fetches it and
|
|
355
|
+
* coerces the wire answer into row views.
|
|
356
|
+
*
|
|
357
|
+
* Node-safe by construction — no DOM, no react, no @deepseek-ai imports —
|
|
358
|
+
* so the host tsc emits it to `dist/client/mesh-api.js` and
|
|
359
|
+
* test/client-mesh-view.test.mjs runs the pure halves (coercion, token-URL
|
|
360
|
+
* pre-check) under `node --test` without jsdom: the switch-core/toast-core
|
|
361
|
+
* precedent in reverse (pure logic kept beside the fetch wrappers, all under
|
|
362
|
+
* src/client). No fetch happens at import time.
|
|
363
|
+
*
|
|
364
|
+
* @module superd-hub/client/mesh-api
|
|
348
365
|
*/
|
|
349
|
-
|
|
366
|
+
/**
|
|
367
|
+
* Base path of the mesh machines API — the client-side spelling of the
|
|
368
|
+
* host's `MESH_API_BASE` (mesh-rpc.ts). Duplicated deliberately: importing
|
|
369
|
+
* the host module would drag its node:fs-backed dependencies into the
|
|
370
|
+
* browser bundle; a path literal cannot drift silently because the mesh-rpc
|
|
371
|
+
* route registration and this file are reviewed together.
|
|
372
|
+
*/
|
|
373
|
+
const MESH_API_BASE = "/api/superd/machines";
|
|
374
|
+
/** The statuses the wire may carry; anything else coerces to `'pending'`. */
|
|
375
|
+
const STATUSES = [
|
|
376
|
+
"pending",
|
|
377
|
+
"reachable",
|
|
378
|
+
"unreachable",
|
|
379
|
+
"auth-expired"
|
|
380
|
+
];
|
|
381
|
+
/** Coerce the self block; a shape without a non-empty string id + string label is simply absent. */
|
|
382
|
+
function coerceSelf(raw) {
|
|
383
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
384
|
+
const { id, label } = raw;
|
|
385
|
+
if (typeof id !== "string" || id === "" || typeof label !== "string") return void 0;
|
|
350
386
|
return {
|
|
351
|
-
id
|
|
352
|
-
|
|
353
|
-
label: "This machine",
|
|
354
|
-
url: typeof location === "undefined" ? "" : location.origin,
|
|
355
|
-
self: true,
|
|
356
|
-
addedAt: 0
|
|
387
|
+
id,
|
|
388
|
+
label
|
|
357
389
|
};
|
|
358
390
|
}
|
|
359
391
|
/**
|
|
360
|
-
*
|
|
392
|
+
* Coerce a raw GET body into the panel's view list. Any bad shape degrades
|
|
393
|
+
* to the empty list (rows dropped individually, self dropped as a block) —
|
|
394
|
+
* the UI never crashes on a malformed answer.
|
|
361
395
|
*
|
|
362
|
-
* @
|
|
396
|
+
* @param raw - the parsed JSON body (or anything else the wire produced).
|
|
397
|
+
* @returns self (when usable) + the coerced remote rows.
|
|
363
398
|
*/
|
|
364
|
-
function
|
|
365
|
-
|
|
366
|
-
|
|
399
|
+
function coerceMachineList(raw) {
|
|
400
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { machines: [] };
|
|
401
|
+
const record = raw;
|
|
402
|
+
const self = coerceSelf(record.self);
|
|
403
|
+
const machines = [];
|
|
404
|
+
if (Array.isArray(record.machines)) for (const entry of record.machines) {
|
|
405
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
406
|
+
const row = entry;
|
|
407
|
+
if (typeof row.id !== "string" || row.id === "") continue;
|
|
408
|
+
if (typeof row.name !== "string" || row.name === "") continue;
|
|
409
|
+
if (typeof row.origin !== "string" || row.origin === "") continue;
|
|
410
|
+
const status = typeof row.status === "string" && STATUSES.includes(row.status) ? row.status : "pending";
|
|
411
|
+
const base = {
|
|
412
|
+
id: row.id,
|
|
413
|
+
name: row.name,
|
|
414
|
+
label: typeof row.label === "string" && row.label !== "" ? row.label : row.name,
|
|
415
|
+
origin: row.origin,
|
|
416
|
+
status,
|
|
417
|
+
self: self !== void 0 && row.id === self.id
|
|
418
|
+
};
|
|
419
|
+
const withProbe = typeof row.lastProbeAt === "number" ? {
|
|
420
|
+
...base,
|
|
421
|
+
lastProbeAt: row.lastProbeAt
|
|
422
|
+
} : base;
|
|
423
|
+
machines.push(typeof row.versionWarning === "string" && row.versionWarning !== "" ? {
|
|
424
|
+
...withProbe,
|
|
425
|
+
versionWarning: row.versionWarning
|
|
426
|
+
} : withProbe);
|
|
427
|
+
}
|
|
428
|
+
return self === void 0 ? { machines } : {
|
|
429
|
+
self,
|
|
430
|
+
machines
|
|
431
|
+
};
|
|
367
432
|
}
|
|
368
|
-
/**
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
433
|
+
/**
|
|
434
|
+
* The version-warning chip for one machine row (I2): present EXACTLY when the
|
|
435
|
+
* coerced row carries a `versionWarning` — the Hub machines panel renders
|
|
436
|
+
* the chip iff this returns a value. Kept here beside the coercion so the
|
|
437
|
+
* presence contract stays unit-testable without jsdom (the HubPage seat is
|
|
438
|
+
* a one-line consumer following the existing chip patterns).
|
|
439
|
+
*/
|
|
440
|
+
function machineVersionChip(machine) {
|
|
441
|
+
return machine.versionWarning === void 0 ? void 0 : { text: machine.versionWarning };
|
|
442
|
+
}
|
|
443
|
+
/** Read a non-2xx body's `{error}` message; degrade to the bare status line. */
|
|
444
|
+
async function errorText(response) {
|
|
372
445
|
try {
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
375
|
-
|
|
446
|
+
const body = await response.json();
|
|
447
|
+
if (typeof body?.error === "string" && body.error !== "") return body.error;
|
|
448
|
+
} catch {}
|
|
449
|
+
return `HTTP ${response.status}`;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* GET the machine list (self + remotes).
|
|
453
|
+
*
|
|
454
|
+
* @returns the coerced list — a malformed body degrades to the empty list, never a throw.
|
|
455
|
+
* @throws when the request fails or the face answers non-2xx.
|
|
456
|
+
*/
|
|
457
|
+
async function fetchMachines() {
|
|
458
|
+
const response = await fetch(MESH_API_BASE, { credentials: "same-origin" });
|
|
459
|
+
if (!response.ok) throw new Error(await errorText(response));
|
|
460
|
+
return coerceMachineList(await response.json().catch(() => void 0));
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* POST a pairing `{name, label?, tokenUrl}` (label omitted → the server
|
|
464
|
+
* collapses it to the name).
|
|
465
|
+
*
|
|
466
|
+
* @throws Error(body.error) when the face answers non-2xx — the message is
|
|
467
|
+
* mesh-link's own, already token-redacted.
|
|
468
|
+
*/
|
|
469
|
+
async function addMachine(input) {
|
|
470
|
+
const body = {
|
|
471
|
+
name: input.name,
|
|
472
|
+
tokenUrl: input.tokenUrl
|
|
473
|
+
};
|
|
474
|
+
if (typeof input.label === "string" && input.label !== "") body.label = input.label;
|
|
475
|
+
const response = await fetch(MESH_API_BASE, {
|
|
476
|
+
credentials: "same-origin",
|
|
477
|
+
method: "POST",
|
|
478
|
+
headers: { "content-type": "application/json" },
|
|
479
|
+
body: JSON.stringify(body)
|
|
480
|
+
});
|
|
481
|
+
if (!response.ok) throw new Error(await errorText(response));
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* DELETE one remote row by id (`{removed: boolean}` answers even for
|
|
485
|
+
* unknown ids — a false removal is a reload, not an error).
|
|
486
|
+
*
|
|
487
|
+
* @throws Error(body.error) on a non-2xx answer.
|
|
488
|
+
*/
|
|
489
|
+
async function removeMachine(id) {
|
|
490
|
+
const response = await fetch(`${MESH_API_BASE}?id=${encodeURIComponent(id)}`, {
|
|
491
|
+
credentials: "same-origin",
|
|
492
|
+
method: "DELETE"
|
|
493
|
+
});
|
|
494
|
+
if (!response.ok) throw new Error(await errorText(response));
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* POST `{op: 'reprobe', id}` — a fresh probe of one remote; the outcome
|
|
498
|
+
* (even unreachable/auth-expired) lands in the status table server-side, so
|
|
499
|
+
* the caller reloads rather than interpreting the probe body.
|
|
500
|
+
*
|
|
501
|
+
* @throws Error(body.error) on a non-2xx answer (the op itself failed).
|
|
502
|
+
*/
|
|
503
|
+
async function reprobeMachine(id) {
|
|
504
|
+
const response = await fetch(MESH_API_BASE, {
|
|
505
|
+
credentials: "same-origin",
|
|
506
|
+
method: "POST",
|
|
507
|
+
headers: { "content-type": "application/json" },
|
|
508
|
+
body: JSON.stringify({
|
|
509
|
+
op: "reprobe",
|
|
510
|
+
id
|
|
511
|
+
})
|
|
512
|
+
});
|
|
513
|
+
if (!response.ok) throw new Error(await errorText(response));
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The standing cadence note (D18): machine status refresh rides the hub's
|
|
517
|
+
* 5-minute sweep — the panel renders this verbatim beside its live-fetch
|
|
518
|
+
* control. English copy to match the DSH UI.
|
|
519
|
+
*/
|
|
520
|
+
const AUTO_REFRESH_NOTE = "auto-refreshes every 5 min";
|
|
521
|
+
/**
|
|
522
|
+
* The per-machine last-fetch line, shaped from the row's `lastProbeAt`
|
|
523
|
+
* (the hub's status table stamp — mesh-rpc joins it on every GET).
|
|
524
|
+
* Pure by design (the UI passes its own `now`): headless-testable without
|
|
525
|
+
* jsdom, same presence contract as {@link machineVersionChip} — absent or
|
|
526
|
+
* non-finite stamp means "never probed" and the panel omits the line.
|
|
527
|
+
*
|
|
528
|
+
* @param lastProbeAt - epoch ms of the machine's last recorded probe.
|
|
529
|
+
* @param now - epoch ms the panel renders at.
|
|
530
|
+
* @returns "Updated 2m ago" / "Updated just now" / … , or undefined.
|
|
531
|
+
*/
|
|
532
|
+
function lastFetchLabel(lastProbeAt, now) {
|
|
533
|
+
if (typeof lastProbeAt !== "number" || !Number.isFinite(lastProbeAt)) return void 0;
|
|
534
|
+
const seconds = Math.max(0, Math.floor((now - lastProbeAt) / 1e3));
|
|
535
|
+
if (seconds < 60) return "Updated just now";
|
|
536
|
+
if (seconds < 3600) return `Updated ${Math.floor(seconds / 60)}m ago`;
|
|
537
|
+
if (seconds < 86400) return `Updated ${Math.floor(seconds / 3600)}h ago`;
|
|
538
|
+
return `Updated ${Math.floor(seconds / 86400)}d ago`;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Client-side pre-check before a pairing submit: a parseable absolute
|
|
542
|
+
* http(s) URL carrying a non-empty `token` query param. The server re-parses
|
|
543
|
+
* authoritatively (mesh-link's parseTokenUrl); this only keeps obviously
|
|
544
|
+
* dead drafts off the wire.
|
|
545
|
+
*
|
|
546
|
+
* @param input - the raw token-URL draft (trim before calling).
|
|
547
|
+
* @returns whether the draft is submit-shaped.
|
|
548
|
+
*/
|
|
549
|
+
function validTokenUrl(input) {
|
|
550
|
+
if (typeof input !== "string" || input === "") return false;
|
|
551
|
+
let url;
|
|
552
|
+
try {
|
|
553
|
+
url = new URL(input);
|
|
376
554
|
} catch {
|
|
377
|
-
return
|
|
555
|
+
return false;
|
|
378
556
|
}
|
|
379
|
-
if (
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
return list;
|
|
557
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
558
|
+
const token = url.searchParams.get("token");
|
|
559
|
+
return token !== null && token !== "";
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region src/client/mesh-groups.ts
|
|
564
|
+
/** The machine key every LOCAL roster row carries (older host shapes may omit the field — grouping treats absent as local). */
|
|
565
|
+
const LOCAL_MACHINE_KEY = "local";
|
|
566
|
+
/** The LOCAL section's fixed display label — this machine needs no registry row to be named. */
|
|
567
|
+
const LOCAL_MACHINE_LABEL = "This machine";
|
|
568
|
+
/** Whether a row's machine field parks it in the local section: `'local'` or absent (the coercion drops unknown values). */
|
|
569
|
+
function isLocalMachine(machine) {
|
|
570
|
+
return machine === void 0 || machine === LOCAL_MACHINE_KEY;
|
|
396
571
|
}
|
|
397
572
|
/**
|
|
398
|
-
*
|
|
573
|
+
* Group a roster into machine sections for the switch menu and the Hub
|
|
574
|
+
* roster rows: the LOCAL group first (label {@link LOCAL_MACHINE_LABEL};
|
|
575
|
+
* rows whose `machine` is `'local'` or absent), then one group per remote
|
|
576
|
+
* machine sorted by display label (code-unit order) with the machine key as
|
|
577
|
+
* the tiebreak. Members keep the roster's given order inside their group;
|
|
578
|
+
* groups only exist for machines actually present in the roster (no empty
|
|
579
|
+
* local section when every row is federated). An unknown label map entry —
|
|
580
|
+
* missing, or an empty string — falls back to the raw machine key.
|
|
399
581
|
*
|
|
400
|
-
* @param
|
|
582
|
+
* @param links - the roster rows (any order; the section order is derived, not inherited).
|
|
583
|
+
* @param machineLabels - machine key (m4) → display label; the machines API answer via {@link machineLabelsFrom}.
|
|
584
|
+
* @returns the sections, local first.
|
|
401
585
|
*/
|
|
402
|
-
function
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
586
|
+
function groupByMachine(links, machineLabels) {
|
|
587
|
+
const local = [];
|
|
588
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
589
|
+
for (const link of links) {
|
|
590
|
+
if (isLocalMachine(link.machine)) {
|
|
591
|
+
local.push(link);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const machine = link.machine;
|
|
595
|
+
const bucket = byKey.get(machine);
|
|
596
|
+
if (bucket === void 0) byKey.set(machine, [link]);
|
|
597
|
+
else bucket.push(link);
|
|
598
|
+
}
|
|
599
|
+
const labelOf = (key) => {
|
|
600
|
+
const label = machineLabels[key];
|
|
601
|
+
return typeof label === "string" && label !== "" ? label : key;
|
|
602
|
+
};
|
|
603
|
+
const remote = [...byKey.entries()].map(([key, agents]) => ({
|
|
604
|
+
key,
|
|
605
|
+
label: labelOf(key),
|
|
606
|
+
agents
|
|
607
|
+
})).sort((a, b) => a.label !== b.label ? a.label < b.label ? -1 : 1 : a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
608
|
+
return local.length > 0 ? [{
|
|
609
|
+
key: LOCAL_MACHINE_KEY,
|
|
610
|
+
label: LOCAL_MACHINE_LABEL,
|
|
611
|
+
agents: local
|
|
612
|
+
}, ...remote] : remote;
|
|
407
613
|
}
|
|
408
|
-
/**
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
614
|
+
/**
|
|
615
|
+
* The machines API answer → the grouping label map: every remote row's
|
|
616
|
+
* `id → label` (mesh-api's coercion already collapsed an empty label to the
|
|
617
|
+
* machine name) plus the self block's own `m4 → label`. An absent list is
|
|
618
|
+
* the empty map — the caller's degrade-to-flat trigger, never a throw.
|
|
619
|
+
*
|
|
620
|
+
* @param list - the coerced `fetchMachines()` answer, when it succeeded.
|
|
621
|
+
* @returns machine key → display label (the raw key stays absent, not mapped).
|
|
622
|
+
*/
|
|
623
|
+
function machineLabelsFrom(list) {
|
|
624
|
+
const labels = {};
|
|
625
|
+
if (list === void 0) return labels;
|
|
626
|
+
for (const machine of list.machines) labels[machine.id] = machine.label !== "" ? machine.label : machine.name;
|
|
627
|
+
if (list.self !== void 0) labels[list.self.id] = list.self.label;
|
|
628
|
+
return labels;
|
|
413
629
|
}
|
|
414
630
|
|
|
415
631
|
//#endregion
|
|
@@ -883,37 +1099,6 @@ function activeAgentName(activeAgent, roster) {
|
|
|
883
1099
|
if (activeAgent === void 0 || activeAgent === "") return void 0;
|
|
884
1100
|
return roster.find((row) => row.key === activeAgent)?.label ?? activeAgent;
|
|
885
1101
|
}
|
|
886
|
-
/** Which machine one roster row groups under: the row's own machine (parser-guaranteed). */
|
|
887
|
-
function machineGroup(row) {
|
|
888
|
-
return row.machine;
|
|
889
|
-
}
|
|
890
|
-
/**
|
|
891
|
-
* Group a roster by machine for the HubPanelIcon chevron menu's native
|
|
892
|
-
* workspace-list look: one group per distinct `machineGroup` key, groups in
|
|
893
|
-
* first-appearance order, members keeping the roster's order inside their
|
|
894
|
-
* group. A single-group roster (today's local-only shape) renders exactly
|
|
895
|
-
* like the old flat list minus the card border.
|
|
896
|
-
*
|
|
897
|
-
* Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
|
|
898
|
-
*/
|
|
899
|
-
function machineGroups(rows) {
|
|
900
|
-
const groups = [];
|
|
901
|
-
const byMachine = /* @__PURE__ */ new Map();
|
|
902
|
-
for (const row of rows) {
|
|
903
|
-
const machine = machineGroup(row);
|
|
904
|
-
let group = byMachine.get(machine);
|
|
905
|
-
if (group === void 0) {
|
|
906
|
-
group = {
|
|
907
|
-
machine,
|
|
908
|
-
agents: []
|
|
909
|
-
};
|
|
910
|
-
byMachine.set(machine, group);
|
|
911
|
-
groups.push(group);
|
|
912
|
-
}
|
|
913
|
-
group.agents.push(row);
|
|
914
|
-
}
|
|
915
|
-
return groups;
|
|
916
|
-
}
|
|
917
1102
|
/**
|
|
918
1103
|
* Build the switch machine over an injected {@link SwitchHost}.
|
|
919
1104
|
*
|
|
@@ -931,10 +1116,10 @@ function machineGroups(rows) {
|
|
|
931
1116
|
function createSwitchController(host, options = {}) {
|
|
932
1117
|
const fallbackMs = options.fallbackMs ?? SWITCH_FALLBACK_MS;
|
|
933
1118
|
const quietWindowMs = options.quietWindowMs ?? LANDING_QUIET_MS;
|
|
934
|
-
let current;
|
|
1119
|
+
let current$1;
|
|
935
1120
|
let armedLanding;
|
|
936
1121
|
return { run(label) {
|
|
937
|
-
const prior = current;
|
|
1122
|
+
const prior = current$1;
|
|
938
1123
|
return new Promise((resolve) => {
|
|
939
1124
|
let timer;
|
|
940
1125
|
let watchdog;
|
|
@@ -961,7 +1146,7 @@ function createSwitchController(host, options = {}) {
|
|
|
961
1146
|
attempt.finished = true;
|
|
962
1147
|
host.clearTimeout(timer);
|
|
963
1148
|
disposer();
|
|
964
|
-
if (current === attempt) current = void 0;
|
|
1149
|
+
if (current$1 === attempt) current$1 = void 0;
|
|
965
1150
|
if (landingArmed) watchdog = host.setTimeout(() => {
|
|
966
1151
|
attempt.cancelLanding();
|
|
967
1152
|
}, fallbackMs);
|
|
@@ -974,9 +1159,9 @@ function createSwitchController(host, options = {}) {
|
|
|
974
1159
|
cancelPriorLanding();
|
|
975
1160
|
}
|
|
976
1161
|
if (prior !== void 0 && !prior.finished) prior.finish();
|
|
977
|
-
current = attempt;
|
|
1162
|
+
current$1 = attempt;
|
|
978
1163
|
const revert = (reason) => {
|
|
979
|
-
if (attempt.finished || current !== attempt) return;
|
|
1164
|
+
if (attempt.finished || current$1 !== attempt) return;
|
|
980
1165
|
attempt.cancelLanding();
|
|
981
1166
|
try {
|
|
982
1167
|
host.setAgent(NATIVE_AGENT);
|
|
@@ -999,7 +1184,7 @@ function createSwitchController(host, options = {}) {
|
|
|
999
1184
|
revert("timeout");
|
|
1000
1185
|
}, fallbackMs);
|
|
1001
1186
|
disposer = host.onGenerationEstablished(() => {
|
|
1002
|
-
if (attempt.finished || current !== attempt) return;
|
|
1187
|
+
if (attempt.finished || current$1 !== attempt) return;
|
|
1003
1188
|
attempt.finish();
|
|
1004
1189
|
});
|
|
1005
1190
|
landingArmed = true;
|
|
@@ -1056,6 +1241,169 @@ function createSwitchController(host, options = {}) {
|
|
|
1056
1241
|
});
|
|
1057
1242
|
} };
|
|
1058
1243
|
}
|
|
1244
|
+
/**
|
|
1245
|
+
* The pure pre-flight decision. `native` always runs; an absent row (roster
|
|
1246
|
+
* fetch failed, or the key is not listed) also runs — the machine's roster
|
|
1247
|
+
* probe is the authority on unknown targets, exactly as before C1. A
|
|
1248
|
+
* not-ready FEDERATED row (`machine` set and not `'local'`) decides
|
|
1249
|
+
* `'machine-down'` — an immediate actionable failure, never a wait loop
|
|
1250
|
+
* (the hub's probe already knows the machine is unreachable or its auth
|
|
1251
|
+
* expired); `failed` keeps precedence everywhere (a peer-reported spawn
|
|
1252
|
+
* rejection still reports as failed).
|
|
1253
|
+
*/
|
|
1254
|
+
function decidePreFlight(label, row) {
|
|
1255
|
+
if (label === NATIVE_AGENT) return "run";
|
|
1256
|
+
if (row === void 0) return "run";
|
|
1257
|
+
if (row.failed === true) return "failed";
|
|
1258
|
+
if (row.ready !== true) {
|
|
1259
|
+
if (row.machine !== void 0 && row.machine !== "local") return "machine-down";
|
|
1260
|
+
return "wait";
|
|
1261
|
+
}
|
|
1262
|
+
return "run";
|
|
1263
|
+
}
|
|
1264
|
+
/** Bound on the machine-down branch's enrichment fetch (mesh P3 Task 5a:
|
|
1265
|
+
* the parked P2 minor — an unbounded `fetchMachines` await — folds here; a
|
|
1266
|
+
* hung hub answer must never swallow the toast). */
|
|
1267
|
+
const MACHINE_DOWN_ENRICH_MS = 3e3;
|
|
1268
|
+
/**
|
|
1269
|
+
* The machine-down branch's pure half (I1(c) feedback + I1(b) light probe,
|
|
1270
|
+
* mesh P3 Task 5a): enrich the toast through the machines face — BOUNDED at
|
|
1271
|
+
* {@link MACHINE_DOWN_ENRICH_MS} — then, after toasting, fire ONE reprobe
|
|
1272
|
+
* for the row's machine and forget it: a stale-unreachable machine gets a
|
|
1273
|
+
* fresh attempt per failed click, so the NEXT click can succeed without
|
|
1274
|
+
* waiting for the hub's 5-minute sweep. Never blocks the UI path (the whole
|
|
1275
|
+
* body is detached), never surfaces a reprobe failure (the sweep retries).
|
|
1276
|
+
*/
|
|
1277
|
+
function runMachineDownFeedback(label, row, faces) {
|
|
1278
|
+
(async () => {
|
|
1279
|
+
let machineName = row.machine;
|
|
1280
|
+
let status;
|
|
1281
|
+
const enriched = await new Promise((resolve) => {
|
|
1282
|
+
const bound$1 = faces.setTimeout(() => resolve(void 0), MACHINE_DOWN_ENRICH_MS);
|
|
1283
|
+
faces.fetchMachines().then((list) => {
|
|
1284
|
+
faces.clearTimeout(bound$1);
|
|
1285
|
+
resolve(list);
|
|
1286
|
+
}, () => {
|
|
1287
|
+
faces.clearTimeout(bound$1);
|
|
1288
|
+
resolve(void 0);
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
1291
|
+
if (enriched !== void 0) {
|
|
1292
|
+
const found = enriched.machines.find((entry) => entry.id === row.machine);
|
|
1293
|
+
if (found !== void 0) {
|
|
1294
|
+
machineName = found.label;
|
|
1295
|
+
if (found.status === "unreachable" || found.status === "auth-expired") status = found.status;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const who = row.label ?? label;
|
|
1299
|
+
const machine = machineName ?? "its machine";
|
|
1300
|
+
faces.toast(status === void 0 ? `Super D: ${who} is unavailable — machine ${machine} is not ready. Reprobe or re-add it in the Hub machines panel.` : `Super D: ${who} is unavailable — machine ${machine} reports ${status}. Reprobe or re-add it in the Hub machines panel.`);
|
|
1301
|
+
if (row.machine !== void 0) faces.reprobe(row.machine).catch(() => {});
|
|
1302
|
+
})();
|
|
1303
|
+
}
|
|
1304
|
+
/** Default bound on one readiness wait (the page never locks behind a stuck world). */
|
|
1305
|
+
const WAIT_BOUND_MS = 12e4;
|
|
1306
|
+
/** Default poll interval for one readiness wait. */
|
|
1307
|
+
const WAIT_POLL_MS = 1e3;
|
|
1308
|
+
/**
|
|
1309
|
+
* Build a bounded readiness waiter over injected faces. The poll re-reads
|
|
1310
|
+
* the roster row every {@link ReadinessWaiterOptions.pollMs}; `failed`
|
|
1311
|
+
* short-circuits immediately; the bound always terminates the wait.
|
|
1312
|
+
*/
|
|
1313
|
+
function createReadinessWaiter(faces, options = {}) {
|
|
1314
|
+
const boundMs = options.boundMs ?? WAIT_BOUND_MS;
|
|
1315
|
+
const pollMs = options.pollMs ?? WAIT_POLL_MS;
|
|
1316
|
+
let pending;
|
|
1317
|
+
let pollHandle;
|
|
1318
|
+
let boundHandle;
|
|
1319
|
+
let settled = false;
|
|
1320
|
+
const settle = (outcome) => {
|
|
1321
|
+
if (settled) return;
|
|
1322
|
+
settled = true;
|
|
1323
|
+
if (pollHandle !== void 0) faces.clearTimeout(pollHandle);
|
|
1324
|
+
if (boundHandle !== void 0) faces.clearTimeout(boundHandle);
|
|
1325
|
+
pending?.(outcome);
|
|
1326
|
+
pending = void 0;
|
|
1327
|
+
};
|
|
1328
|
+
return {
|
|
1329
|
+
wait(label) {
|
|
1330
|
+
return new Promise((resolve) => {
|
|
1331
|
+
pending = resolve;
|
|
1332
|
+
boundHandle = faces.setTimeout(() => {
|
|
1333
|
+
settle("bound");
|
|
1334
|
+
}, boundMs);
|
|
1335
|
+
const tick = () => {
|
|
1336
|
+
if (settled) return;
|
|
1337
|
+
faces.fetchRow().then((row) => {
|
|
1338
|
+
if (settled) return;
|
|
1339
|
+
if (row?.failed === true) {
|
|
1340
|
+
settle("failed");
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (row?.ready === true) {
|
|
1344
|
+
settle("ready");
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
pollHandle = faces.setTimeout(tick, pollMs);
|
|
1348
|
+
}, () => {
|
|
1349
|
+
if (!settled) pollHandle = faces.setTimeout(tick, pollMs);
|
|
1350
|
+
});
|
|
1351
|
+
};
|
|
1352
|
+
tick();
|
|
1353
|
+
});
|
|
1354
|
+
},
|
|
1355
|
+
cancel() {
|
|
1356
|
+
settle("cancelled");
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
//#endregion
|
|
1362
|
+
//#region src/toast-core.ts
|
|
1363
|
+
/** Failures that name what broke need a longer read than a one-liner. */
|
|
1364
|
+
const TOAST_DEFAULT_HOLD_MS = 6e3;
|
|
1365
|
+
/** The exact props the official primitive receives (pure — the unit seam). */
|
|
1366
|
+
function toastProps(text, options = {}) {
|
|
1367
|
+
const props = {
|
|
1368
|
+
text,
|
|
1369
|
+
holdMs: options.holdMs ?? TOAST_DEFAULT_HOLD_MS
|
|
1370
|
+
};
|
|
1371
|
+
if (options.tone !== void 0) props.tone = options.tone;
|
|
1372
|
+
return props;
|
|
1373
|
+
}
|
|
1374
|
+
let seq = 0;
|
|
1375
|
+
let current;
|
|
1376
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1377
|
+
function emit() {
|
|
1378
|
+
for (const fn of [...listeners]) try {
|
|
1379
|
+
fn(current);
|
|
1380
|
+
} catch {}
|
|
1381
|
+
}
|
|
1382
|
+
/** Fire one toast (best-effort: without a mounted owner nothing renders). */
|
|
1383
|
+
function showSuperdToast(text, options = {}) {
|
|
1384
|
+
current = {
|
|
1385
|
+
seq: ++seq,
|
|
1386
|
+
text,
|
|
1387
|
+
options
|
|
1388
|
+
};
|
|
1389
|
+
emit();
|
|
1390
|
+
}
|
|
1391
|
+
/** Clear a shown toast (the fade's `onDone`); stale sequences are no-ops. */
|
|
1392
|
+
function clearSuperdToast(done) {
|
|
1393
|
+
if (current === void 0 || current.seq !== done) return;
|
|
1394
|
+
current = void 0;
|
|
1395
|
+
emit();
|
|
1396
|
+
}
|
|
1397
|
+
/** Subscribe to the store (the host's effect face); returns unsubscribe. */
|
|
1398
|
+
function onSuperdToast(fn) {
|
|
1399
|
+
listeners.add(fn);
|
|
1400
|
+
try {
|
|
1401
|
+
fn(current);
|
|
1402
|
+
} catch {}
|
|
1403
|
+
return () => {
|
|
1404
|
+
listeners.delete(fn);
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1059
1407
|
|
|
1060
1408
|
//#endregion
|
|
1061
1409
|
//#region src/client/switch.ts
|
|
@@ -1071,39 +1419,32 @@ function uiState() {
|
|
|
1071
1419
|
if (typeof state.subscribe !== "function" || typeof state.activeAgent !== "string") return void 0;
|
|
1072
1420
|
return state;
|
|
1073
1421
|
}
|
|
1074
|
-
/** How long one revert toast stays on screen. */
|
|
1075
|
-
const TOAST_HOLD_MS = 6e3;
|
|
1076
1422
|
/**
|
|
1077
|
-
* One transient message
|
|
1078
|
-
*
|
|
1423
|
+
* One transient message through the official `Toast` primitive (C1 task 4:
|
|
1424
|
+
* the platform's top-center banner — dark surface in both themes, warn icon
|
|
1425
|
+
* seat — rendered by the host's React tree from the Hub sidebar row's
|
|
1426
|
+
* always-mounted owner; see client/toast.ts).
|
|
1079
1427
|
*/
|
|
1080
1428
|
function showToast(message) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
doc.body.appendChild(el);
|
|
1101
|
-
globalThis.setTimeout(() => {
|
|
1102
|
-
try {
|
|
1103
|
-
el.remove();
|
|
1104
|
-
} catch {}
|
|
1105
|
-
}, TOAST_HOLD_MS);
|
|
1106
|
-
} catch {}
|
|
1429
|
+
showSuperdToast(message);
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* I1(c) feedback + I1(b) light probe (mesh P3 Task 5a): the machine-down
|
|
1433
|
+
* branch delegates to the pure {@link runMachineDownFeedback} — the
|
|
1434
|
+
* machines face (already part of the client half — the Hub machines panel's
|
|
1435
|
+
* own cheap same-origin GET) enriches the toast, bounded at 3s; after
|
|
1436
|
+
* toasting, ONE fire-and-forget reprobe POST per failed click refreshes the
|
|
1437
|
+
* hub's status table so the NEXT click can succeed without waiting for the
|
|
1438
|
+
* 5-minute sweep. Nothing here blocks the UI path.
|
|
1439
|
+
*/
|
|
1440
|
+
function reportMachineDown(label, row) {
|
|
1441
|
+
runMachineDownFeedback(label, row, {
|
|
1442
|
+
fetchMachines,
|
|
1443
|
+
reprobe: reprobeMachine,
|
|
1444
|
+
toast: showToast,
|
|
1445
|
+
setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
|
|
1446
|
+
clearTimeout: (handle) => globalThis.clearTimeout(handle)
|
|
1447
|
+
});
|
|
1107
1448
|
}
|
|
1108
1449
|
/** The bound `switchAgent`, or `undefined` until the entry binds it. */
|
|
1109
1450
|
let bound;
|
|
@@ -1211,10 +1552,70 @@ function bindSwitchAgent(deps) {
|
|
|
1211
1552
|
* `false` means the click is swallowed, never that the anchor navigates
|
|
1212
1553
|
* the fused page away (review round 1).
|
|
1213
1554
|
*/
|
|
1555
|
+
let pendingWait;
|
|
1556
|
+
/**
|
|
1557
|
+
* The Hub switch surfaces' entry point. Attempts the in-page switch for
|
|
1558
|
+
* `label`, gated on the target world's readiness (C1): a not-ready world
|
|
1559
|
+
* holds the UI on native with starting feedback and a bounded poll; a
|
|
1560
|
+
* failed world reports and never switches; a ready/native/unknown target
|
|
1561
|
+
* runs the machine (whose roster probe stays the fallback). A not-ready
|
|
1562
|
+
* FEDERATED row skips all of that — an immediate machine-error toast
|
|
1563
|
+
* (I1(c), `machine-down`), never a wait loop.
|
|
1564
|
+
*
|
|
1565
|
+
* @returns `true` when the fusion switch took the click over; `false` when
|
|
1566
|
+
* this page is not a fusion page or the orchestration is unbound.
|
|
1567
|
+
* Informational only — the interception contract (`seatClickDecision`)
|
|
1568
|
+
* prevents navigation on every fusion-page left click regardless: a
|
|
1569
|
+
* `false` means the click is swallowed, never that the anchor navigates
|
|
1570
|
+
* the fused page away (review round 1).
|
|
1571
|
+
*/
|
|
1214
1572
|
function attemptSwitch(label) {
|
|
1215
1573
|
const run = bound;
|
|
1216
1574
|
if (run === void 0 || !fusionPage()) return false;
|
|
1217
|
-
|
|
1575
|
+
pendingWait?.cancel();
|
|
1576
|
+
pendingWait = void 0;
|
|
1577
|
+
(async () => {
|
|
1578
|
+
let row;
|
|
1579
|
+
try {
|
|
1580
|
+
row = (await fetchRoster()).find((agent) => agent.key === label);
|
|
1581
|
+
} catch {
|
|
1582
|
+
row = void 0;
|
|
1583
|
+
}
|
|
1584
|
+
const decision = decidePreFlight(label, row);
|
|
1585
|
+
if (decision === "failed") {
|
|
1586
|
+
showToast(`Super D: ${label} failed to start — see the service log.`);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (decision === "machine-down") {
|
|
1590
|
+
if (row !== void 0) reportMachineDown(label, row);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (decision === "wait") {
|
|
1594
|
+
showToast(`Super D: ${label} is starting…`);
|
|
1595
|
+
const waiter = createReadinessWaiter({
|
|
1596
|
+
fetchRow: async () => {
|
|
1597
|
+
try {
|
|
1598
|
+
return (await fetchRoster()).find((agent) => agent.key === label);
|
|
1599
|
+
} catch {
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
},
|
|
1603
|
+
setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
|
|
1604
|
+
clearTimeout: (handle) => globalThis.clearTimeout(handle)
|
|
1605
|
+
});
|
|
1606
|
+
pendingWait = waiter;
|
|
1607
|
+
const outcome = await waiter.wait(label);
|
|
1608
|
+
if (pendingWait === waiter) pendingWait = void 0;
|
|
1609
|
+
if (outcome === "ready") {
|
|
1610
|
+
run(label).catch(() => {});
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (outcome === "failed") showToast(`Super D: ${label} failed to start — see the service log.`);
|
|
1614
|
+
else if (outcome === "bound") showToast(`Super D: ${label} is still starting — try again in a moment.`);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
run(label).catch(() => {});
|
|
1618
|
+
})();
|
|
1218
1619
|
return true;
|
|
1219
1620
|
}
|
|
1220
1621
|
|
|
@@ -1222,7 +1623,7 @@ function attemptSwitch(label) {
|
|
|
1222
1623
|
//#region src/client/HubPage.tsx
|
|
1223
1624
|
const EMPTY_DRAFT = {
|
|
1224
1625
|
name: "",
|
|
1225
|
-
|
|
1626
|
+
tokenUrl: ""
|
|
1226
1627
|
};
|
|
1227
1628
|
/** Chip text: drop the trailing slash for concision ('/claude/' → '/claude'). */
|
|
1228
1629
|
function displayPath(path) {
|
|
@@ -1246,6 +1647,99 @@ const chipPending = {
|
|
|
1246
1647
|
...chip,
|
|
1247
1648
|
color: "var(--dsw-alias-label-caption)"
|
|
1248
1649
|
};
|
|
1650
|
+
const chipWarn = {
|
|
1651
|
+
...chip,
|
|
1652
|
+
color: "var(--dsw-alias-state-warn-primary)"
|
|
1653
|
+
};
|
|
1654
|
+
const chipError = {
|
|
1655
|
+
...chip,
|
|
1656
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
1657
|
+
};
|
|
1658
|
+
/** Machine id chip (the m4): the monospace chip look the roster's path tags use. */
|
|
1659
|
+
const chipMonoId = {
|
|
1660
|
+
...chip,
|
|
1661
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
1662
|
+
fontSize: 10.5
|
|
1663
|
+
};
|
|
1664
|
+
/** The status dot's fixed part (the color is decided per row). */
|
|
1665
|
+
const dotStyle = {
|
|
1666
|
+
width: 8,
|
|
1667
|
+
height: 8,
|
|
1668
|
+
borderRadius: "50%",
|
|
1669
|
+
flex: "none"
|
|
1670
|
+
};
|
|
1671
|
+
/** The machine origin column: monospace, eats the leftover row width. */
|
|
1672
|
+
const originStyle = {
|
|
1673
|
+
flex: 1,
|
|
1674
|
+
minWidth: 0,
|
|
1675
|
+
overflow: "hidden",
|
|
1676
|
+
textOverflow: "ellipsis",
|
|
1677
|
+
whiteSpace: "nowrap",
|
|
1678
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
1679
|
+
fontSize: 12,
|
|
1680
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
1681
|
+
};
|
|
1682
|
+
/** The quiet text action (reprobe) — the switch link's look. */
|
|
1683
|
+
const textActionStyle = {
|
|
1684
|
+
border: "none",
|
|
1685
|
+
background: "transparent",
|
|
1686
|
+
padding: 0,
|
|
1687
|
+
font: "inherit",
|
|
1688
|
+
fontSize: 12.5,
|
|
1689
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1690
|
+
cursor: "pointer",
|
|
1691
|
+
whiteSpace: "nowrap"
|
|
1692
|
+
};
|
|
1693
|
+
/** The icon action (remove ✕) seat. */
|
|
1694
|
+
const iconActionStyle = {
|
|
1695
|
+
display: "inline-flex",
|
|
1696
|
+
alignItems: "center",
|
|
1697
|
+
justifyContent: "center",
|
|
1698
|
+
width: 22,
|
|
1699
|
+
height: 22,
|
|
1700
|
+
borderRadius: 6,
|
|
1701
|
+
border: "none",
|
|
1702
|
+
background: "transparent",
|
|
1703
|
+
color: "var(--dsw-alias-label-caption)",
|
|
1704
|
+
font: "inherit",
|
|
1705
|
+
cursor: "pointer"
|
|
1706
|
+
};
|
|
1707
|
+
/** The status dot's color: reachable green, pending grey, unreachable amber, auth-expired red. */
|
|
1708
|
+
function statusDotColor(status) {
|
|
1709
|
+
if (status === "reachable") return "var(--dsw-alias-state-success-primary)";
|
|
1710
|
+
if (status === "auth-expired") return "var(--dsw-alias-state-error-primary)";
|
|
1711
|
+
if (status === "unreachable") return "var(--dsw-alias-state-warn-primary)";
|
|
1712
|
+
return "var(--dsw-alias-label-caption)";
|
|
1713
|
+
}
|
|
1714
|
+
/** The last-fetch caption's style (R7): quiet tertiary text beside the status chip. */
|
|
1715
|
+
const ageStyle = {
|
|
1716
|
+
fontSize: 11.5,
|
|
1717
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
1718
|
+
whiteSpace: "nowrap"
|
|
1719
|
+
};
|
|
1720
|
+
/** The status chip (style + text) for one remote row. */
|
|
1721
|
+
function statusChip(machine) {
|
|
1722
|
+
if (machine.status === "reachable") return {
|
|
1723
|
+
style: chipSelf,
|
|
1724
|
+
text: "reachable"
|
|
1725
|
+
};
|
|
1726
|
+
if (machine.status === "auth-expired") return {
|
|
1727
|
+
style: chipError,
|
|
1728
|
+
text: "auth expired"
|
|
1729
|
+
};
|
|
1730
|
+
if (machine.status === "unreachable") return {
|
|
1731
|
+
style: chipWarn,
|
|
1732
|
+
text: "unreachable"
|
|
1733
|
+
};
|
|
1734
|
+
return {
|
|
1735
|
+
style: chipPending,
|
|
1736
|
+
text: "pending"
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
/** One line of error text for the panel's inline error line. */
|
|
1740
|
+
function errText(err) {
|
|
1741
|
+
return err instanceof Error ? err.message : String(err);
|
|
1742
|
+
}
|
|
1249
1743
|
const inputStyle = {
|
|
1250
1744
|
boxSizing: "border-box",
|
|
1251
1745
|
width: "100%",
|
|
@@ -1272,6 +1766,15 @@ const listStyle = {
|
|
|
1272
1766
|
flexDirection: "column",
|
|
1273
1767
|
gap: 2
|
|
1274
1768
|
};
|
|
1769
|
+
/** The machine section head inside the roster list (the grouped shape's caption row). */
|
|
1770
|
+
const sectionHeadStyle = {
|
|
1771
|
+
padding: "8px 8px 2px",
|
|
1772
|
+
fontSize: 11,
|
|
1773
|
+
lineHeight: "16px",
|
|
1774
|
+
color: "var(--dsw-alias-label-caption)",
|
|
1775
|
+
whiteSpace: "nowrap",
|
|
1776
|
+
listStyle: "none"
|
|
1777
|
+
};
|
|
1275
1778
|
/** A row that lights its surface on hover (inline styles have no :hover). */
|
|
1276
1779
|
function HoverRow({ children, onClick, clickable }) {
|
|
1277
1780
|
const [hover, setHover] = react.useState(false);
|
|
@@ -1329,7 +1832,11 @@ function GroupHead({ title, count, action }) {
|
|
|
1329
1832
|
});
|
|
1330
1833
|
}
|
|
1331
1834
|
function HubPage() {
|
|
1332
|
-
const [
|
|
1835
|
+
const [mesh, setMesh] = react.useState({ machines: [] });
|
|
1836
|
+
const [meshLoaded, setMeshLoaded] = react.useState(false);
|
|
1837
|
+
const [meshError, setMeshError] = react.useState(null);
|
|
1838
|
+
const [meshBusy, setMeshBusy] = react.useState(false);
|
|
1839
|
+
const [meshReloads, setMeshReloads] = react.useState(0);
|
|
1333
1840
|
const [showAdd, setShowAdd] = react.useState(false);
|
|
1334
1841
|
const [draft, setDraft] = react.useState(EMPTY_DRAFT);
|
|
1335
1842
|
const [agents, setAgents] = react.useState(() => rosterFromGlobal() ?? null);
|
|
@@ -1355,6 +1862,24 @@ function HubPage() {
|
|
|
1355
1862
|
cancelled = true;
|
|
1356
1863
|
};
|
|
1357
1864
|
}, []);
|
|
1865
|
+
react.useEffect(() => {
|
|
1866
|
+
let cancelled = false;
|
|
1867
|
+
fetchMachines().then((list) => {
|
|
1868
|
+
if (!cancelled) {
|
|
1869
|
+
setMesh(list);
|
|
1870
|
+
setMeshError(null);
|
|
1871
|
+
setMeshLoaded(true);
|
|
1872
|
+
}
|
|
1873
|
+
}).catch((err) => {
|
|
1874
|
+
if (!cancelled) {
|
|
1875
|
+
setMeshError(errText(err));
|
|
1876
|
+
setMeshLoaded(true);
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
return () => {
|
|
1880
|
+
cancelled = true;
|
|
1881
|
+
};
|
|
1882
|
+
}, [meshReloads]);
|
|
1358
1883
|
react.useEffect(() => {
|
|
1359
1884
|
if (!fusion) return;
|
|
1360
1885
|
const state = uiState();
|
|
@@ -1364,6 +1889,7 @@ function HubPage() {
|
|
|
1364
1889
|
});
|
|
1365
1890
|
}, [fusion]);
|
|
1366
1891
|
const here = currentMountPath();
|
|
1892
|
+
const hereOrigin = typeof location === "undefined" ? "" : location.origin;
|
|
1367
1893
|
const isActive = (agent) => hubRowActive({
|
|
1368
1894
|
fusion,
|
|
1369
1895
|
agentKey: agent.key,
|
|
@@ -1386,30 +1912,111 @@ function HubPage() {
|
|
|
1386
1912
|
attempt: () => attemptSwitch(agent.key)
|
|
1387
1913
|
}).preventDefault) event.preventDefault();
|
|
1388
1914
|
};
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1915
|
+
const machineLabels = meshLoaded && meshError === null ? machineLabelsFrom(mesh) : void 0;
|
|
1916
|
+
const groups = agents !== null && machineLabels !== void 0 ? groupByMachine(agents, machineLabels) : void 0;
|
|
1917
|
+
/**
|
|
1918
|
+
* One Agent Runtimes row — the SAME row in both shapes; `showMachine`
|
|
1919
|
+
* keeps the flat fallback's per-row machine text (the grouped shape moves
|
|
1920
|
+
* it onto the section head). The click contract is untouched: every row
|
|
1921
|
+
* still runs the `seatClickDecision` table.
|
|
1922
|
+
*/
|
|
1923
|
+
const renderRow = (agent, showMachine) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverRow, {
|
|
1924
|
+
clickable: switchUi.available,
|
|
1925
|
+
onClick: switchUi.available ? (event) => {
|
|
1926
|
+
rowDecision(agent, event);
|
|
1927
|
+
} : void 0,
|
|
1928
|
+
children: [
|
|
1929
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeMark, {
|
|
1930
|
+
agentKey: agent.key,
|
|
1931
|
+
size: 20
|
|
1932
|
+
}),
|
|
1933
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1934
|
+
style: {
|
|
1935
|
+
fontWeight: 500,
|
|
1936
|
+
whiteSpace: "nowrap"
|
|
1937
|
+
},
|
|
1938
|
+
children: agent.label
|
|
1939
|
+
}),
|
|
1940
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1941
|
+
style: {
|
|
1942
|
+
flex: 1,
|
|
1943
|
+
minWidth: 0,
|
|
1944
|
+
overflow: "hidden",
|
|
1945
|
+
textOverflow: "ellipsis",
|
|
1946
|
+
whiteSpace: "nowrap",
|
|
1947
|
+
fontSize: 12.5,
|
|
1948
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
1949
|
+
},
|
|
1950
|
+
children: showMachine ? agent.machine : null
|
|
1951
|
+
}),
|
|
1952
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1953
|
+
style: {
|
|
1954
|
+
...chip,
|
|
1955
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
1956
|
+
fontSize: 10.5
|
|
1957
|
+
},
|
|
1958
|
+
children: displayPath(agent.path)
|
|
1959
|
+
}),
|
|
1960
|
+
isActive(agent) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1961
|
+
style: chipSelf,
|
|
1962
|
+
children: "current"
|
|
1963
|
+
}),
|
|
1964
|
+
switchUi.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1965
|
+
"aria-current": isActive(agent) ? "page" : void 0,
|
|
1966
|
+
style: {
|
|
1967
|
+
fontSize: 12.5,
|
|
1968
|
+
color: "var(--dsh-alias-label-secondary)",
|
|
1969
|
+
textDecoration: "none",
|
|
1970
|
+
cursor: "pointer"
|
|
1971
|
+
},
|
|
1972
|
+
onClick: (event) => {
|
|
1973
|
+
event.stopPropagation();
|
|
1974
|
+
rowDecision(agent, event);
|
|
1975
|
+
},
|
|
1976
|
+
children: "switch"
|
|
1977
|
+
})
|
|
1978
|
+
]
|
|
1979
|
+
}, agent.key);
|
|
1980
|
+
/** Bump the reload sequence — every mutating op refreshes through the API. */
|
|
1981
|
+
const reloadMachines = () => {
|
|
1982
|
+
setMeshReloads((n) => n + 1);
|
|
1393
1983
|
};
|
|
1394
|
-
const
|
|
1984
|
+
const addRemote = () => {
|
|
1395
1985
|
const name = draft.name.trim();
|
|
1396
|
-
const
|
|
1397
|
-
if (name === "" ||
|
|
1398
|
-
|
|
1399
|
-
|
|
1986
|
+
const tokenUrl = draft.tokenUrl.trim();
|
|
1987
|
+
if (name === "" || !validTokenUrl(tokenUrl)) return;
|
|
1988
|
+
setMeshBusy(true);
|
|
1989
|
+
addMachine({
|
|
1400
1990
|
name,
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1991
|
+
tokenUrl
|
|
1992
|
+
}).then(() => {
|
|
1993
|
+
setDraft(EMPTY_DRAFT);
|
|
1994
|
+
setShowAdd(false);
|
|
1995
|
+
setMeshError(null);
|
|
1996
|
+
reloadMachines();
|
|
1997
|
+
}).catch((err) => {
|
|
1998
|
+
setMeshError(errText(err));
|
|
1999
|
+
}).finally(() => {
|
|
2000
|
+
setMeshBusy(false);
|
|
2001
|
+
});
|
|
1408
2002
|
};
|
|
1409
|
-
const
|
|
1410
|
-
|
|
2003
|
+
const removeRemote = (id) => {
|
|
2004
|
+
removeMachine(id).then(() => {
|
|
2005
|
+
setMeshError(null);
|
|
2006
|
+
reloadMachines();
|
|
2007
|
+
}).catch((err) => {
|
|
2008
|
+
setMeshError(errText(err));
|
|
2009
|
+
});
|
|
1411
2010
|
};
|
|
1412
|
-
const
|
|
2011
|
+
const reprobeRemote = (id) => {
|
|
2012
|
+
reprobeMachine(id).then(() => {
|
|
2013
|
+
setMeshError(null);
|
|
2014
|
+
reloadMachines();
|
|
2015
|
+
}).catch((err) => {
|
|
2016
|
+
setMeshError(errText(err));
|
|
2017
|
+
});
|
|
2018
|
+
};
|
|
2019
|
+
const draftValid = draft.name.trim() !== "" && validTokenUrl(draft.tokenUrl.trim());
|
|
1413
2020
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1414
2021
|
style: {
|
|
1415
2022
|
display: "flex",
|
|
@@ -1537,7 +2144,7 @@ function HubPage() {
|
|
|
1537
2144
|
children: [
|
|
1538
2145
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupHead, {
|
|
1539
2146
|
title: "Machines",
|
|
1540
|
-
count: machines.length,
|
|
2147
|
+
count: (mesh.self !== void 0 ? 1 : 0) + mesh.machines.length,
|
|
1541
2148
|
action: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1542
2149
|
type: "button",
|
|
1543
2150
|
onClick: () => {
|
|
@@ -1571,90 +2178,152 @@ function HubPage() {
|
|
|
1571
2178
|
}), "Add machine"]
|
|
1572
2179
|
})
|
|
1573
2180
|
}),
|
|
1574
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
2181
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1575
2182
|
style: {
|
|
1576
2183
|
margin: 0,
|
|
1577
2184
|
fontSize: 13,
|
|
1578
2185
|
lineHeight: "20px",
|
|
1579
2186
|
color: "var(--dsw-alias-label-tertiary)"
|
|
1580
2187
|
},
|
|
1581
|
-
children:
|
|
2188
|
+
children: [
|
|
2189
|
+
"Add another DSH from remote machines, provide the token URL. Status ",
|
|
2190
|
+
AUTO_REFRESH_NOTE,
|
|
2191
|
+
"."
|
|
2192
|
+
]
|
|
1582
2193
|
}),
|
|
1583
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
2194
|
+
meshError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2195
|
+
style: {
|
|
2196
|
+
margin: 0,
|
|
2197
|
+
fontSize: 12.5,
|
|
2198
|
+
lineHeight: "20px",
|
|
2199
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
2200
|
+
},
|
|
2201
|
+
children: meshError
|
|
2202
|
+
}),
|
|
2203
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
|
|
1584
2204
|
style: listStyle,
|
|
1585
|
-
children:
|
|
1586
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
1587
|
-
"aria-hidden": true,
|
|
1588
|
-
style: {
|
|
1589
|
-
width: 8,
|
|
1590
|
-
height: 8,
|
|
1591
|
-
borderRadius: "50%",
|
|
1592
|
-
flex: "none",
|
|
1593
|
-
background: machine.self ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-label-caption)"
|
|
1594
|
-
}
|
|
1595
|
-
}),
|
|
1596
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2205
|
+
children: [
|
|
2206
|
+
!meshLoaded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
1597
2207
|
style: {
|
|
1598
|
-
|
|
1599
|
-
|
|
2208
|
+
padding: "9px 8px",
|
|
2209
|
+
fontSize: 13,
|
|
2210
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
2211
|
+
listStyle: "none"
|
|
1600
2212
|
},
|
|
1601
|
-
children:
|
|
2213
|
+
children: "Loading machines…"
|
|
1602
2214
|
}),
|
|
1603
|
-
|
|
1604
|
-
style: chip,
|
|
1605
|
-
children: machine.label
|
|
1606
|
-
}),
|
|
1607
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2215
|
+
meshLoaded && meshError === null && mesh.machines.length === 0 && mesh.self === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
1608
2216
|
style: {
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
whiteSpace: "nowrap",
|
|
1614
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
1615
|
-
fontSize: 12,
|
|
1616
|
-
color: "var(--dsw-alias-label-secondary)"
|
|
2217
|
+
padding: "9px 8px",
|
|
2218
|
+
fontSize: 13,
|
|
2219
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
2220
|
+
listStyle: "none"
|
|
1617
2221
|
},
|
|
1618
|
-
children:
|
|
1619
|
-
}),
|
|
1620
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1621
|
-
style: machine.self ? chipSelf : chipPending,
|
|
1622
|
-
children: machine.self ? "self" : "link pending"
|
|
2222
|
+
children: "No machines yet — add the first one with its token URL."
|
|
1623
2223
|
}),
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
},
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
children:
|
|
1650
|
-
d: "M2.5 2.5L8.5 8.5M8.5 2.5L2.5 8.5",
|
|
1651
|
-
stroke: "currentColor",
|
|
1652
|
-
strokeWidth: "1.4",
|
|
1653
|
-
strokeLinecap: "round"
|
|
1654
|
-
})
|
|
2224
|
+
mesh.self !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverRow, { children: [
|
|
2225
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2226
|
+
"aria-hidden": true,
|
|
2227
|
+
style: {
|
|
2228
|
+
...dotStyle,
|
|
2229
|
+
background: "var(--dsw-alias-state-success-primary)"
|
|
2230
|
+
}
|
|
2231
|
+
}),
|
|
2232
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2233
|
+
style: {
|
|
2234
|
+
fontWeight: 500,
|
|
2235
|
+
whiteSpace: "nowrap"
|
|
2236
|
+
},
|
|
2237
|
+
children: mesh.self.label
|
|
2238
|
+
}),
|
|
2239
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2240
|
+
style: originStyle,
|
|
2241
|
+
children: hereOrigin
|
|
2242
|
+
}),
|
|
2243
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2244
|
+
style: chipMonoId,
|
|
2245
|
+
children: mesh.self.id
|
|
2246
|
+
}),
|
|
2247
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2248
|
+
style: chipSelf,
|
|
2249
|
+
children: "self"
|
|
1655
2250
|
})
|
|
2251
|
+
] }),
|
|
2252
|
+
mesh.machines.map((machine) => {
|
|
2253
|
+
const status = statusChip(machine);
|
|
2254
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverRow, { children: [
|
|
2255
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2256
|
+
"aria-hidden": true,
|
|
2257
|
+
style: {
|
|
2258
|
+
...dotStyle,
|
|
2259
|
+
background: statusDotColor(machine.status)
|
|
2260
|
+
}
|
|
2261
|
+
}),
|
|
2262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2263
|
+
style: {
|
|
2264
|
+
fontWeight: 500,
|
|
2265
|
+
whiteSpace: "nowrap"
|
|
2266
|
+
},
|
|
2267
|
+
children: machine.name
|
|
2268
|
+
}),
|
|
2269
|
+
machine.label !== machine.name && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2270
|
+
style: chip,
|
|
2271
|
+
children: machine.label
|
|
2272
|
+
}),
|
|
2273
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2274
|
+
style: originStyle,
|
|
2275
|
+
children: machine.origin
|
|
2276
|
+
}),
|
|
2277
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2278
|
+
style: chipMonoId,
|
|
2279
|
+
children: machine.id
|
|
2280
|
+
}),
|
|
2281
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2282
|
+
style: status.style,
|
|
2283
|
+
children: status.text
|
|
2284
|
+
}),
|
|
2285
|
+
lastFetchLabel(machine.lastProbeAt, Date.now()) !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2286
|
+
style: ageStyle,
|
|
2287
|
+
children: lastFetchLabel(machine.lastProbeAt, Date.now())
|
|
2288
|
+
}),
|
|
2289
|
+
machineVersionChip(machine) !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2290
|
+
style: chipWarn,
|
|
2291
|
+
title: `machine ${machine.id}`,
|
|
2292
|
+
children: machine.versionWarning
|
|
2293
|
+
}),
|
|
2294
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2295
|
+
type: "button",
|
|
2296
|
+
"aria-label": `Refresh ${machine.name}`,
|
|
2297
|
+
onClick: () => {
|
|
2298
|
+
reprobeRemote(machine.id);
|
|
2299
|
+
},
|
|
2300
|
+
style: textActionStyle,
|
|
2301
|
+
children: "refresh"
|
|
2302
|
+
}),
|
|
2303
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2304
|
+
type: "button",
|
|
2305
|
+
"aria-label": `Remove ${machine.name}`,
|
|
2306
|
+
onClick: () => {
|
|
2307
|
+
removeRemote(machine.id);
|
|
2308
|
+
},
|
|
2309
|
+
style: iconActionStyle,
|
|
2310
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2311
|
+
width: "11",
|
|
2312
|
+
height: "11",
|
|
2313
|
+
viewBox: "0 0 11 11",
|
|
2314
|
+
fill: "none",
|
|
2315
|
+
"aria-hidden": "true",
|
|
2316
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
2317
|
+
d: "M2.5 2.5L8.5 8.5M8.5 2.5L2.5 8.5",
|
|
2318
|
+
stroke: "currentColor",
|
|
2319
|
+
strokeWidth: "1.4",
|
|
2320
|
+
strokeLinecap: "round"
|
|
2321
|
+
})
|
|
2322
|
+
})
|
|
2323
|
+
})
|
|
2324
|
+
] }, machine.id);
|
|
1656
2325
|
})
|
|
1657
|
-
]
|
|
2326
|
+
]
|
|
1658
2327
|
}),
|
|
1659
2328
|
showAdd && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1660
2329
|
style: {
|
|
@@ -1683,15 +2352,15 @@ function HubPage() {
|
|
|
1683
2352
|
})] }),
|
|
1684
2353
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1685
2354
|
style: fieldLabel,
|
|
1686
|
-
children: "URL"
|
|
2355
|
+
children: "Token URL"
|
|
1687
2356
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1688
2357
|
style: inputStyle,
|
|
1689
|
-
value: draft.
|
|
1690
|
-
placeholder: "
|
|
2358
|
+
value: draft.tokenUrl,
|
|
2359
|
+
placeholder: "https://192.168.31.130:4999/?token=…",
|
|
1691
2360
|
onChange: (event) => {
|
|
1692
2361
|
setDraft({
|
|
1693
2362
|
...draft,
|
|
1694
|
-
|
|
2363
|
+
tokenUrl: event.target.value
|
|
1695
2364
|
});
|
|
1696
2365
|
}
|
|
1697
2366
|
})] }),
|
|
@@ -1721,9 +2390,9 @@ function HubPage() {
|
|
|
1721
2390
|
children: "Cancel"
|
|
1722
2391
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1723
2392
|
type: "button",
|
|
1724
|
-
disabled: !draftValid,
|
|
2393
|
+
disabled: !draftValid || meshBusy,
|
|
1725
2394
|
onClick: () => {
|
|
1726
|
-
|
|
2395
|
+
addRemote();
|
|
1727
2396
|
},
|
|
1728
2397
|
style: {
|
|
1729
2398
|
padding: "7px 14px",
|
|
@@ -1774,71 +2443,23 @@ function HubPage() {
|
|
|
1774
2443
|
}),
|
|
1775
2444
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
|
|
1776
2445
|
style: listStyle,
|
|
1777
|
-
children: [
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
}),
|
|
1795
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1796
|
-
style: {
|
|
1797
|
-
fontWeight: 500,
|
|
1798
|
-
whiteSpace: "nowrap"
|
|
1799
|
-
},
|
|
1800
|
-
children: agent.label
|
|
1801
|
-
}),
|
|
1802
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1803
|
-
style: {
|
|
1804
|
-
flex: 1,
|
|
1805
|
-
minWidth: 0,
|
|
1806
|
-
overflow: "hidden",
|
|
1807
|
-
textOverflow: "ellipsis",
|
|
1808
|
-
whiteSpace: "nowrap",
|
|
1809
|
-
fontSize: 12.5,
|
|
1810
|
-
color: "var(--dsw-alias-label-tertiary)"
|
|
1811
|
-
},
|
|
1812
|
-
children: agent.machine
|
|
1813
|
-
}),
|
|
1814
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1815
|
-
style: {
|
|
1816
|
-
...chip,
|
|
1817
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
1818
|
-
fontSize: 10.5
|
|
1819
|
-
},
|
|
1820
|
-
children: displayPath(agent.path)
|
|
1821
|
-
}),
|
|
1822
|
-
isActive(agent) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1823
|
-
style: chipSelf,
|
|
1824
|
-
children: "current"
|
|
1825
|
-
}),
|
|
1826
|
-
switchUi.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1827
|
-
"aria-current": isActive(agent) ? "page" : void 0,
|
|
1828
|
-
style: {
|
|
1829
|
-
fontSize: 12.5,
|
|
1830
|
-
color: "var(--dsw-alias-label-secondary)",
|
|
1831
|
-
textDecoration: "none",
|
|
1832
|
-
cursor: "pointer"
|
|
1833
|
-
},
|
|
1834
|
-
onClick: (event) => {
|
|
1835
|
-
event.stopPropagation();
|
|
1836
|
-
rowDecision(agent, event);
|
|
1837
|
-
},
|
|
1838
|
-
children: "switch"
|
|
1839
|
-
})
|
|
1840
|
-
]
|
|
1841
|
-
}, agent.key))]
|
|
2446
|
+
children: [
|
|
2447
|
+
agents === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
2448
|
+
style: {
|
|
2449
|
+
padding: "9px 8px",
|
|
2450
|
+
fontSize: 13,
|
|
2451
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
2452
|
+
listStyle: "none"
|
|
2453
|
+
},
|
|
2454
|
+
children: "Loading roster…"
|
|
2455
|
+
}),
|
|
2456
|
+
groups !== void 0 && groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
2457
|
+
"aria-hidden": true,
|
|
2458
|
+
style: sectionHeadStyle,
|
|
2459
|
+
children: group.label
|
|
2460
|
+
}), group.agents.map((agent) => renderRow(agent, false))] }, group.key)),
|
|
2461
|
+
groups === void 0 && agents?.map((agent) => renderRow(agent, true))
|
|
2462
|
+
]
|
|
1842
2463
|
})
|
|
1843
2464
|
]
|
|
1844
2465
|
})] })
|
|
@@ -1847,6 +2468,34 @@ function HubPage() {
|
|
|
1847
2468
|
});
|
|
1848
2469
|
}
|
|
1849
2470
|
|
|
2471
|
+
//#endregion
|
|
2472
|
+
//#region src/client/toast.ts
|
|
2473
|
+
/**
|
|
2474
|
+
* The element for one pending toast: keyed by sequence so a newer toast
|
|
2475
|
+
* remounts the primitive and restarts its slide-in cycle (the primitive's
|
|
2476
|
+
* own re-show contract).
|
|
2477
|
+
*/
|
|
2478
|
+
function superdToastElement(toast) {
|
|
2479
|
+
return react.createElement(__deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
2480
|
+
key: toast.seq,
|
|
2481
|
+
...toastProps(toast.text, toast.options),
|
|
2482
|
+
onDone: () => {
|
|
2483
|
+
clearSuperdToast(toast.seq);
|
|
2484
|
+
}
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* The always-mounted owner: subscribes to the store and renders the current
|
|
2489
|
+
* toast (nothing while the store is empty). Mounted inside the Hub sidebar
|
|
2490
|
+
* row — the primitive's body portal places the banner top-center regardless
|
|
2491
|
+
* of where the owner sits.
|
|
2492
|
+
*/
|
|
2493
|
+
function SuperdToastHost() {
|
|
2494
|
+
const [toast, setToast] = react.useState(void 0);
|
|
2495
|
+
react.useEffect(() => onSuperdToast(setToast), []);
|
|
2496
|
+
return toast === void 0 ? null : superdToastElement(toast);
|
|
2497
|
+
}
|
|
2498
|
+
|
|
1850
2499
|
//#endregion
|
|
1851
2500
|
//#region src/client/HubPanelIcon.tsx
|
|
1852
2501
|
/** Runtime-mark edge inside the selector, and the chevron's own default edge. */
|
|
@@ -1870,6 +2519,7 @@ function HubPanelIcon({ size }) {
|
|
|
1870
2519
|
const [hover, setHover] = react.useState(false);
|
|
1871
2520
|
const [box, setBox] = react.useState(null);
|
|
1872
2521
|
const [agents, setAgents] = react.useState(() => rosterFromGlobal() ?? [NATIVE_LINK]);
|
|
2522
|
+
const [machineLabels, setMachineLabels] = react.useState(void 0);
|
|
1873
2523
|
const fusion = fusionPage();
|
|
1874
2524
|
const switchUi = switchAffordance(fusion);
|
|
1875
2525
|
const [activeKey, setActiveKey] = react.useState(() => fusion ? uiState()?.activeAgent : void 0);
|
|
@@ -1921,6 +2571,9 @@ function HubPanelIcon({ size }) {
|
|
|
1921
2571
|
fetchRoster().then((list) => {
|
|
1922
2572
|
if (!cancelled) setAgents(list);
|
|
1923
2573
|
}).catch(() => {});
|
|
2574
|
+
fetchMachines().then((list) => {
|
|
2575
|
+
if (!cancelled) setMachineLabels(machineLabelsFrom(list));
|
|
2576
|
+
}).catch(() => {});
|
|
1924
2577
|
return () => {
|
|
1925
2578
|
cancelled = true;
|
|
1926
2579
|
};
|
|
@@ -1943,15 +2596,15 @@ function HubPanelIcon({ size }) {
|
|
|
1943
2596
|
setOpen((value) => !value);
|
|
1944
2597
|
};
|
|
1945
2598
|
const items = [];
|
|
1946
|
-
|
|
2599
|
+
if (machineLabels !== void 0) groupByMachine(agents, machineLabels).forEach((group, groupIndex) => {
|
|
1947
2600
|
if (groupIndex > 0) items.push({
|
|
1948
2601
|
type: "separator",
|
|
1949
|
-
id: `machine-divider:${group.
|
|
2602
|
+
id: `machine-divider:${group.key}`
|
|
1950
2603
|
});
|
|
1951
2604
|
items.push({
|
|
1952
2605
|
type: "label",
|
|
1953
|
-
id: `machine:${group.
|
|
1954
|
-
text: group.
|
|
2606
|
+
id: `machine:${group.key}`,
|
|
2607
|
+
text: group.label
|
|
1955
2608
|
});
|
|
1956
2609
|
for (const agent of group.agents) items.push({
|
|
1957
2610
|
id: agent.key,
|
|
@@ -1962,6 +2615,14 @@ function HubPanelIcon({ size }) {
|
|
|
1962
2615
|
})
|
|
1963
2616
|
});
|
|
1964
2617
|
});
|
|
2618
|
+
else for (const agent of agents) items.push({
|
|
2619
|
+
id: agent.key,
|
|
2620
|
+
label: agent.label,
|
|
2621
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeMark, {
|
|
2622
|
+
agentKey: agent.key,
|
|
2623
|
+
size: 14
|
|
2624
|
+
})
|
|
2625
|
+
});
|
|
1965
2626
|
/**
|
|
1966
2627
|
* A menu pick. The row is a `menuitem` button — there is no anchor default
|
|
1967
2628
|
* to cancel — but the shared table still owns whether the attempt runs
|
|
@@ -1986,61 +2647,65 @@ function HubPanelIcon({ size }) {
|
|
|
1986
2647
|
alignItems: "center",
|
|
1987
2648
|
justifyContent: "center"
|
|
1988
2649
|
},
|
|
1989
|
-
children: [
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
onSelect: select,
|
|
1997
|
-
dense: true,
|
|
1998
|
-
portal: true,
|
|
1999
|
-
align: "end",
|
|
2000
|
-
getAnchorRect: anchorRect,
|
|
2001
|
-
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2002
|
-
ref: selectorRef,
|
|
2003
|
-
role: "button",
|
|
2004
|
-
tabIndex: 0,
|
|
2005
|
-
"aria-label": "Agent selector",
|
|
2006
|
-
"aria-haspopup": "menu",
|
|
2007
|
-
"aria-expanded": open,
|
|
2008
|
-
title: activeLabel === void 0 ? "Agent selector" : `Agent selector: ${activeLabel}`,
|
|
2009
|
-
style: {
|
|
2010
|
-
position: "fixed",
|
|
2011
|
-
left: box.right - ROW_INSET - selectorWidth,
|
|
2012
|
-
top: box.top + (box.bottom - box.top) / 2,
|
|
2013
|
-
transform: "translateY(-50%)",
|
|
2014
|
-
display: "inline-flex",
|
|
2015
|
-
alignItems: "center",
|
|
2016
|
-
justifyContent: "center",
|
|
2017
|
-
gap: SELECTOR_GAP,
|
|
2018
|
-
boxSizing: "border-box",
|
|
2019
|
-
width: selectorWidth,
|
|
2020
|
-
height: SELECTOR_HEIGHT,
|
|
2021
|
-
padding: `0 ${SELECTOR_PAD}px`,
|
|
2022
|
-
borderRadius: 16,
|
|
2023
|
-
background: hover || open ? "var(--dsw-alias-interactive-bg-hover)" : "transparent",
|
|
2024
|
-
color: "var(--dsw-alias-label-caption)",
|
|
2025
|
-
cursor: "pointer",
|
|
2026
|
-
zIndex: 50
|
|
2027
|
-
},
|
|
2028
|
-
onMouseEnter: () => {
|
|
2029
|
-
setHover(true);
|
|
2030
|
-
},
|
|
2031
|
-
onMouseLeave: () => {
|
|
2032
|
-
setHover(false);
|
|
2033
|
-
},
|
|
2034
|
-
onClick: toggle,
|
|
2035
|
-
onKeyDown: (event) => {
|
|
2036
|
-
if (event.key === "Enter" || event.key === " ") toggle(event);
|
|
2650
|
+
children: [
|
|
2651
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(HubGlyph, { size }),
|
|
2652
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SuperdToastHost, {}),
|
|
2653
|
+
wide && box !== null && switchUi.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
2654
|
+
open,
|
|
2655
|
+
onClose: () => {
|
|
2656
|
+
setOpen(false);
|
|
2037
2657
|
},
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2658
|
+
items,
|
|
2659
|
+
selectedIds: activeKey === void 0 ? [] : [activeKey],
|
|
2660
|
+
onSelect: select,
|
|
2661
|
+
dense: true,
|
|
2662
|
+
portal: true,
|
|
2663
|
+
align: "end",
|
|
2664
|
+
getAnchorRect: anchorRect,
|
|
2665
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2666
|
+
ref: selectorRef,
|
|
2667
|
+
role: "button",
|
|
2668
|
+
tabIndex: 0,
|
|
2669
|
+
"aria-label": "Agent selector",
|
|
2670
|
+
"aria-haspopup": "menu",
|
|
2671
|
+
"aria-expanded": open,
|
|
2672
|
+
title: activeLabel === void 0 ? "Agent selector" : `Agent selector: ${activeLabel}`,
|
|
2673
|
+
style: {
|
|
2674
|
+
position: "fixed",
|
|
2675
|
+
left: box.right - ROW_INSET - selectorWidth,
|
|
2676
|
+
top: box.top + (box.bottom - box.top) / 2,
|
|
2677
|
+
transform: "translateY(-50%)",
|
|
2678
|
+
display: "inline-flex",
|
|
2679
|
+
alignItems: "center",
|
|
2680
|
+
justifyContent: "center",
|
|
2681
|
+
gap: SELECTOR_GAP,
|
|
2682
|
+
boxSizing: "border-box",
|
|
2683
|
+
width: selectorWidth,
|
|
2684
|
+
height: SELECTOR_HEIGHT,
|
|
2685
|
+
padding: `0 ${SELECTOR_PAD}px`,
|
|
2686
|
+
borderRadius: 16,
|
|
2687
|
+
background: hover || open ? "var(--dsw-alias-interactive-bg-hover)" : "transparent",
|
|
2688
|
+
color: "var(--dsw-alias-label-caption)",
|
|
2689
|
+
cursor: "pointer",
|
|
2690
|
+
zIndex: 50
|
|
2691
|
+
},
|
|
2692
|
+
onMouseEnter: () => {
|
|
2693
|
+
setHover(true);
|
|
2694
|
+
},
|
|
2695
|
+
onMouseLeave: () => {
|
|
2696
|
+
setHover(false);
|
|
2697
|
+
},
|
|
2698
|
+
onClick: toggle,
|
|
2699
|
+
onKeyDown: (event) => {
|
|
2700
|
+
if (event.key === "Enter" || event.key === " ") toggle(event);
|
|
2701
|
+
},
|
|
2702
|
+
children: [activeKey !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeMark, {
|
|
2703
|
+
agentKey: activeKey,
|
|
2704
|
+
size: MARK_SIZE
|
|
2705
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { size: CHEVRON_SIZE })]
|
|
2706
|
+
})
|
|
2042
2707
|
})
|
|
2043
|
-
|
|
2708
|
+
]
|
|
2044
2709
|
});
|
|
2045
2710
|
}
|
|
2046
2711
|
|