super-dsh 0.1.6 → 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.
Files changed (87) hide show
  1. package/client/index.js +741 -256
  2. package/cordis.patch.yml +16 -0
  3. package/package.json +7 -2
  4. package/superd-hub/dist/a2a-delivery.d.ts +189 -0
  5. package/superd-hub/dist/a2a-delivery.js +240 -0
  6. package/superd-hub/dist/a2a-host.d.ts +76 -0
  7. package/superd-hub/dist/a2a-host.js +145 -0
  8. package/superd-hub/dist/a2a-ingest.d.ts +31 -0
  9. package/superd-hub/dist/a2a-ingest.js +636 -0
  10. package/superd-hub/dist/a2a-log.d.ts +17 -0
  11. package/superd-hub/dist/a2a-log.js +42 -0
  12. package/superd-hub/dist/a2a-row.d.ts +65 -0
  13. package/superd-hub/dist/a2a-row.js +162 -0
  14. package/superd-hub/dist/a2a-service.d.ts +357 -0
  15. package/superd-hub/dist/a2a-service.js +980 -0
  16. package/superd-hub/dist/a2a-wire.d.ts +220 -0
  17. package/superd-hub/dist/a2a-wire.js +305 -0
  18. package/superd-hub/dist/agent-ids.d.ts +10 -0
  19. package/superd-hub/dist/agent-ids.js +68 -0
  20. package/superd-hub/dist/agent-roster.d.ts +29 -18
  21. package/superd-hub/dist/agent-roster.js +54 -15
  22. package/superd-hub/dist/agent-status.d.ts +119 -0
  23. package/superd-hub/dist/agent-status.js +386 -0
  24. package/superd-hub/dist/client/mesh-api.d.ts +134 -0
  25. package/superd-hub/dist/client/mesh-api.js +219 -0
  26. package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
  27. package/superd-hub/dist/client/mesh-groups.js +66 -0
  28. package/superd-hub/dist/client/roster.d.ts +48 -0
  29. package/superd-hub/dist/client/roster.js +96 -0
  30. package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
  31. package/superd-hub/dist/exec-pty-channel.js +162 -0
  32. package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
  33. package/superd-hub/dist/exec-pty-framing.js +81 -0
  34. package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
  35. package/superd-hub/dist/exec-pty-rpc.js +128 -0
  36. package/superd-hub/dist/exec-pty-session.d.ts +76 -0
  37. package/superd-hub/dist/exec-pty-session.js +234 -0
  38. package/superd-hub/dist/exec.d.ts +56 -0
  39. package/superd-hub/dist/exec.js +225 -0
  40. package/superd-hub/dist/gateway.d.ts +39 -2
  41. package/superd-hub/dist/gateway.js +62 -3
  42. package/superd-hub/dist/hub-tool.d.ts +174 -0
  43. package/superd-hub/dist/hub-tool.js +464 -0
  44. package/superd-hub/dist/index.d.ts +15 -1
  45. package/superd-hub/dist/index.js +16 -0
  46. package/superd-hub/dist/machines.d.ts +45 -0
  47. package/superd-hub/dist/machines.js +125 -0
  48. package/superd-hub/dist/mesh-federate.d.ts +138 -0
  49. package/superd-hub/dist/mesh-federate.js +223 -0
  50. package/superd-hub/dist/mesh-gateway.d.ts +67 -0
  51. package/superd-hub/dist/mesh-gateway.js +408 -0
  52. package/superd-hub/dist/mesh-link.d.ts +274 -0
  53. package/superd-hub/dist/mesh-link.js +441 -0
  54. package/superd-hub/dist/mesh-rpc.d.ts +123 -0
  55. package/superd-hub/dist/mesh-rpc.js +250 -0
  56. package/superd-hub/dist/mux-client.d.ts +71 -0
  57. package/superd-hub/dist/mux-client.js +281 -0
  58. package/superd-hub/dist/pack-version.d.ts +2 -0
  59. package/superd-hub/dist/pack-version.js +30 -0
  60. package/superd-hub/dist/roster.d.ts +6 -0
  61. package/superd-hub/dist/rpc.d.ts +18 -2
  62. package/superd-hub/dist/rpc.js +34 -4
  63. package/superd-hub/dist/spawn-world.js +3 -1
  64. package/superd-hub/dist/switch-core.d.ts +62 -25
  65. package/superd-hub/dist/switch-core.js +53 -30
  66. package/superd-hub/dist/system-head.d.ts +22 -12
  67. package/superd-hub/dist/system-head.js +100 -84
  68. package/superd-hub/dist/wire-endpoints.d.ts +264 -0
  69. package/superd-hub/dist/wire-endpoints.js +180 -0
  70. package/superd-hub/lib/client/index.js +741 -256
  71. package/superd-hub/package.json +11 -2
  72. package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
  73. package/agent-agy/dist/agent-preset-projection.js +0 -33
  74. package/agent-claude/dist/agent-preset-projection.js +0 -30
  75. package/agent-codex/dist/agent-preset-projection.js +0 -37
  76. package/agent-codex/dist/app-home.js +0 -65
  77. package/agent-codex/dist/spike.js +0 -46
  78. package/agent-hermes/dist/agent-preset-projection.js +0 -34
  79. package/agent-omp/dist/agent-preset-projection.js +0 -36
  80. package/agent-omp/dist/app-home.js +0 -86
  81. package/agent-pi/dist/agent-preset-projection.js +0 -29
  82. package/superd-hub/dist/client-shim.d.ts +0 -28
  83. package/superd-hub/dist/client-shim.js +0 -251
  84. package/superd-hub/dist/index-pass.d.ts +0 -22
  85. package/superd-hub/dist/index-pass.js +0 -47
  86. package/superd-hub/dist/labels.d.ts +0 -45
  87. package/superd-hub/dist/labels.js +0 -83
package/client/index.js CHANGED
@@ -294,7 +294,7 @@ function coerceAgents(raw) {
294
294
  key,
295
295
  label: typeof label === "string" ? label : FALLBACK_LABELS[key] ?? key,
296
296
  path,
297
- machine: typeof machine === "string" ? machine : "local",
297
+ machine: typeof machine === "string" && machine !== "" ? machine : void 0,
298
298
  ready: entry.ready === true,
299
299
  failed: entry.failed === true
300
300
  });
@@ -308,7 +308,8 @@ function rosterFromGlobal() {
308
308
  }
309
309
  /** Normalize the current mount root: `/omp` and `/omp/` are the same page. */
310
310
  function currentMountPath() {
311
- const pathname = typeof location === "undefined" ? "/" : location.pathname;
311
+ const location$1 = globalThis.location;
312
+ const pathname = location$1 === void 0 ? "/" : location$1.pathname;
312
313
  if (pathname === "" || pathname === "/") return "/";
313
314
  return pathname.endsWith("/") ? pathname : `${pathname}/`;
314
315
  }
@@ -343,78 +344,288 @@ async function fetchRoster() {
343
344
  }
344
345
 
345
346
  //#endregion
346
- //#region src/client/machines-store.ts
347
- /** Storage slot for the ledger (v1 shape; bump the suffix on breaking edits). */
348
- const STORAGE_KEY = "superd-hub.machines.v1";
347
+ //#region src/client/mesh-api.ts
349
348
  /**
350
- * The self row is DERIVED, never stored: this machine's label tracks the
351
- * product copy and its URL tracks the serving origin on every load. Only
352
- * remote machines persist.
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
365
+ */
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.
353
372
  */
354
- function selfMachine() {
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;
355
386
  return {
356
- id: "self",
357
- name: "local",
358
- label: "This machine",
359
- url: typeof location === "undefined" ? "" : location.origin,
360
- self: true,
361
- addedAt: 0
387
+ id,
388
+ label
362
389
  };
363
390
  }
364
391
  /**
365
- * Read the ledger; a missing or unreadable store resets to the self row.
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.
366
395
  *
367
- * @returns the machines: the derived self row first, then the stored remotes.
396
+ * @param raw - the parsed JSON body (or anything else the wire produced).
397
+ * @returns self (when usable) + the coerced remote rows.
368
398
  */
369
- function loadMachines() {
370
- const remotes = readStoredRemotes();
371
- return [selfMachine(), ...remotes];
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
+ };
372
432
  }
373
- /** Parse the stored remote machines; storage failures degrade to none. */
374
- function readStoredRemotes() {
375
- if (typeof localStorage === "undefined") return [];
376
- let parsed;
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) {
377
445
  try {
378
- const raw = localStorage.getItem(STORAGE_KEY);
379
- if (raw === null) return [];
380
- parsed = JSON.parse(raw);
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);
381
554
  } catch {
382
- return [];
555
+ return false;
383
556
  }
384
- if (!Array.isArray(parsed)) return [];
385
- const list = [];
386
- for (const entry of parsed) {
387
- if (typeof entry !== "object" || entry === null) continue;
388
- const { id, name, label, url, self, addedAt } = entry;
389
- if (self === true) continue;
390
- if (typeof id !== "string" || typeof name !== "string" || typeof url !== "string") continue;
391
- list.push({
392
- id,
393
- name,
394
- label: typeof label === "string" ? label : name,
395
- url,
396
- self: false,
397
- addedAt: typeof addedAt === "number" ? addedAt : 0
398
- });
399
- }
400
- 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;
401
571
  }
402
572
  /**
403
- * Persist the ledger; storage failures degrade silently to in-memory state.
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.
404
581
  *
405
- * @param list - the machines to store (self row included).
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.
406
585
  */
407
- function saveMachines(list) {
408
- if (typeof localStorage === "undefined") return;
409
- try {
410
- localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
411
- } catch {}
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;
412
613
  }
413
- /** A fresh row id (UUID when available, time+random otherwise). */
414
- function freshMachineId() {
415
- const cryptoRef = globalThis.crypto;
416
- if (typeof cryptoRef?.randomUUID === "function") return cryptoRef.randomUUID();
417
- return `m-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
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;
418
629
  }
419
630
 
420
631
  //#endregion
@@ -888,37 +1099,6 @@ function activeAgentName(activeAgent, roster) {
888
1099
  if (activeAgent === void 0 || activeAgent === "") return void 0;
889
1100
  return roster.find((row) => row.key === activeAgent)?.label ?? activeAgent;
890
1101
  }
891
- /** Which machine one roster row groups under: the row's own machine (parser-guaranteed). */
892
- function machineGroup(row) {
893
- return row.machine;
894
- }
895
- /**
896
- * Group a roster by machine for the HubPanelIcon chevron menu's native
897
- * workspace-list look: one group per distinct `machineGroup` key, groups in
898
- * first-appearance order, members keeping the roster's order inside their
899
- * group. A single-group roster (today's local-only shape) renders exactly
900
- * like the old flat list minus the card border.
901
- *
902
- * Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
903
- */
904
- function machineGroups(rows) {
905
- const groups = [];
906
- const byMachine = /* @__PURE__ */ new Map();
907
- for (const row of rows) {
908
- const machine = machineGroup(row);
909
- let group = byMachine.get(machine);
910
- if (group === void 0) {
911
- group = {
912
- machine,
913
- agents: []
914
- };
915
- byMachine.set(machine, group);
916
- groups.push(group);
917
- }
918
- group.agents.push(row);
919
- }
920
- return groups;
921
- }
922
1102
  /**
923
1103
  * Build the switch machine over an injected {@link SwitchHost}.
924
1104
  *
@@ -1064,15 +1244,63 @@ function createSwitchController(host, options = {}) {
1064
1244
  /**
1065
1245
  * The pure pre-flight decision. `native` always runs; an absent row (roster
1066
1246
  * fetch failed, or the key is not listed) also runs — the machine's roster
1067
- * probe is the authority on unknown targets, exactly as before C1.
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).
1068
1253
  */
1069
1254
  function decidePreFlight(label, row) {
1070
1255
  if (label === NATIVE_AGENT) return "run";
1071
1256
  if (row === void 0) return "run";
1072
1257
  if (row.failed === true) return "failed";
1073
- if (row.ready !== true) return "wait";
1258
+ if (row.ready !== true) {
1259
+ if (row.machine !== void 0 && row.machine !== "local") return "machine-down";
1260
+ return "wait";
1261
+ }
1074
1262
  return "run";
1075
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
+ }
1076
1304
  /** Default bound on one readiness wait (the page never locks behind a stuck world). */
1077
1305
  const WAIT_BOUND_MS = 12e4;
1078
1306
  /** Default poll interval for one readiness wait. */
@@ -1200,6 +1428,24 @@ function uiState() {
1200
1428
  function showToast(message) {
1201
1429
  showSuperdToast(message);
1202
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
+ });
1448
+ }
1203
1449
  /** The bound `switchAgent`, or `undefined` until the entry binds it. */
1204
1450
  let bound;
1205
1451
  /**
@@ -1312,7 +1558,10 @@ let pendingWait;
1312
1558
  * `label`, gated on the target world's readiness (C1): a not-ready world
1313
1559
  * holds the UI on native with starting feedback and a bounded poll; a
1314
1560
  * failed world reports and never switches; a ready/native/unknown target
1315
- * runs the machine (whose roster probe stays the fallback).
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
+ *
1316
1565
  * @returns `true` when the fusion switch took the click over; `false` when
1317
1566
  * this page is not a fusion page or the orchestration is unbound.
1318
1567
  * Informational only — the interception contract (`seatClickDecision`)
@@ -1337,6 +1586,10 @@ function attemptSwitch(label) {
1337
1586
  showToast(`Super D: ${label} failed to start — see the service log.`);
1338
1587
  return;
1339
1588
  }
1589
+ if (decision === "machine-down") {
1590
+ if (row !== void 0) reportMachineDown(label, row);
1591
+ return;
1592
+ }
1340
1593
  if (decision === "wait") {
1341
1594
  showToast(`Super D: ${label} is starting…`);
1342
1595
  const waiter = createReadinessWaiter({
@@ -1370,7 +1623,7 @@ function attemptSwitch(label) {
1370
1623
  //#region src/client/HubPage.tsx
1371
1624
  const EMPTY_DRAFT = {
1372
1625
  name: "",
1373
- url: ""
1626
+ tokenUrl: ""
1374
1627
  };
1375
1628
  /** Chip text: drop the trailing slash for concision ('/claude/' → '/claude'). */
1376
1629
  function displayPath(path) {
@@ -1394,6 +1647,99 @@ const chipPending = {
1394
1647
  ...chip,
1395
1648
  color: "var(--dsw-alias-label-caption)"
1396
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
+ }
1397
1743
  const inputStyle = {
1398
1744
  boxSizing: "border-box",
1399
1745
  width: "100%",
@@ -1420,6 +1766,15 @@ const listStyle = {
1420
1766
  flexDirection: "column",
1421
1767
  gap: 2
1422
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
+ };
1423
1778
  /** A row that lights its surface on hover (inline styles have no :hover). */
1424
1779
  function HoverRow({ children, onClick, clickable }) {
1425
1780
  const [hover, setHover] = react.useState(false);
@@ -1477,7 +1832,11 @@ function GroupHead({ title, count, action }) {
1477
1832
  });
1478
1833
  }
1479
1834
  function HubPage() {
1480
- const [machines, setMachines] = react.useState(() => loadMachines());
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);
1481
1840
  const [showAdd, setShowAdd] = react.useState(false);
1482
1841
  const [draft, setDraft] = react.useState(EMPTY_DRAFT);
1483
1842
  const [agents, setAgents] = react.useState(() => rosterFromGlobal() ?? null);
@@ -1503,6 +1862,24 @@ function HubPage() {
1503
1862
  cancelled = true;
1504
1863
  };
1505
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]);
1506
1883
  react.useEffect(() => {
1507
1884
  if (!fusion) return;
1508
1885
  const state = uiState();
@@ -1512,6 +1889,7 @@ function HubPage() {
1512
1889
  });
1513
1890
  }, [fusion]);
1514
1891
  const here = currentMountPath();
1892
+ const hereOrigin = typeof location === "undefined" ? "" : location.origin;
1515
1893
  const isActive = (agent) => hubRowActive({
1516
1894
  fusion,
1517
1895
  agentKey: agent.key,
@@ -1534,30 +1912,111 @@ function HubPage() {
1534
1912
  attempt: () => attemptSwitch(agent.key)
1535
1913
  }).preventDefault) event.preventDefault();
1536
1914
  };
1537
- const persist = (next) => {
1538
- const list = [...next];
1539
- setMachines(list);
1540
- saveMachines(list);
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);
1541
1983
  };
1542
- const addMachine = () => {
1984
+ const addRemote = () => {
1543
1985
  const name = draft.name.trim();
1544
- const url = draft.url.trim();
1545
- if (name === "" || url === "") return;
1546
- persist([...machines, {
1547
- id: freshMachineId(),
1986
+ const tokenUrl = draft.tokenUrl.trim();
1987
+ if (name === "" || !validTokenUrl(tokenUrl)) return;
1988
+ setMeshBusy(true);
1989
+ addMachine({
1548
1990
  name,
1549
- label: name,
1550
- url,
1551
- self: false,
1552
- addedAt: Date.now()
1553
- }]);
1554
- setDraft(EMPTY_DRAFT);
1555
- setShowAdd(false);
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
+ });
1556
2002
  };
1557
- const removeMachine = (id) => {
1558
- persist(machines.filter((machine) => machine.id !== id));
2003
+ const removeRemote = (id) => {
2004
+ removeMachine(id).then(() => {
2005
+ setMeshError(null);
2006
+ reloadMachines();
2007
+ }).catch((err) => {
2008
+ setMeshError(errText(err));
2009
+ });
1559
2010
  };
1560
- const draftValid = draft.name.trim() !== "" && draft.url.trim() !== "";
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());
1561
2020
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1562
2021
  style: {
1563
2022
  display: "flex",
@@ -1685,7 +2144,7 @@ function HubPage() {
1685
2144
  children: [
1686
2145
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupHead, {
1687
2146
  title: "Machines",
1688
- count: machines.length,
2147
+ count: (mesh.self !== void 0 ? 1 : 0) + mesh.machines.length,
1689
2148
  action: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1690
2149
  type: "button",
1691
2150
  onClick: () => {
@@ -1719,90 +2178,152 @@ function HubPage() {
1719
2178
  }), "Add machine"]
1720
2179
  })
1721
2180
  }),
1722
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1723
2182
  style: {
1724
2183
  margin: 0,
1725
2184
  fontSize: 13,
1726
2185
  lineHeight: "20px",
1727
2186
  color: "var(--dsw-alias-label-tertiary)"
1728
2187
  },
1729
- children: "Add another DSH from remote machines, provide the token URL."
2188
+ children: [
2189
+ "Add another DSH from remote machines, provide the token URL. Status ",
2190
+ AUTO_REFRESH_NOTE,
2191
+ "."
2192
+ ]
2193
+ }),
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
1730
2202
  }),
1731
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2203
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
1732
2204
  style: listStyle,
1733
- children: machines.map((machine) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverRow, { children: [
1734
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1735
- "aria-hidden": true,
1736
- style: {
1737
- width: 8,
1738
- height: 8,
1739
- borderRadius: "50%",
1740
- flex: "none",
1741
- background: machine.self ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-label-caption)"
1742
- }
1743
- }),
1744
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2205
+ children: [
2206
+ !meshLoaded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
1745
2207
  style: {
1746
- fontWeight: 500,
1747
- whiteSpace: "nowrap"
2208
+ padding: "9px 8px",
2209
+ fontSize: 13,
2210
+ color: "var(--dsw-alias-label-tertiary)",
2211
+ listStyle: "none"
1748
2212
  },
1749
- children: machine.name
1750
- }),
1751
- machine.label !== machine.name && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1752
- style: chip,
1753
- children: machine.label
2213
+ children: "Loading machines…"
1754
2214
  }),
1755
- /* @__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", {
1756
2216
  style: {
1757
- flex: 1,
1758
- minWidth: 0,
1759
- overflow: "hidden",
1760
- textOverflow: "ellipsis",
1761
- whiteSpace: "nowrap",
1762
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1763
- fontSize: 12,
1764
- color: "var(--dsw-alias-label-secondary)"
2217
+ padding: "9px 8px",
2218
+ fontSize: 13,
2219
+ color: "var(--dsw-alias-label-tertiary)",
2220
+ listStyle: "none"
1765
2221
  },
1766
- children: machine.url
2222
+ children: "No machines yet — add the first one with its token URL."
1767
2223
  }),
1768
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1769
- style: machine.self ? chipSelf : chipPending,
1770
- children: machine.self ? "self" : "link pending"
1771
- }),
1772
- !machine.self && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1773
- type: "button",
1774
- "aria-label": `Remove ${machine.name}`,
1775
- onClick: () => {
1776
- removeMachine(machine.id);
1777
- },
1778
- style: {
1779
- display: "inline-flex",
1780
- alignItems: "center",
1781
- justifyContent: "center",
1782
- width: 22,
1783
- height: 22,
1784
- borderRadius: 6,
1785
- border: "none",
1786
- background: "transparent",
1787
- color: "var(--dsw-alias-label-caption)",
1788
- font: "inherit",
1789
- cursor: "pointer"
1790
- },
1791
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1792
- width: "11",
1793
- height: "11",
1794
- viewBox: "0 0 11 11",
1795
- fill: "none",
1796
- "aria-hidden": "true",
1797
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1798
- d: "M2.5 2.5L8.5 8.5M8.5 2.5L2.5 8.5",
1799
- stroke: "currentColor",
1800
- strokeWidth: "1.4",
1801
- strokeLinecap: "round"
1802
- })
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"
1803
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);
1804
2325
  })
1805
- ] }, machine.id))
2326
+ ]
1806
2327
  }),
1807
2328
  showAdd && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1808
2329
  style: {
@@ -1831,15 +2352,15 @@ function HubPage() {
1831
2352
  })] }),
1832
2353
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1833
2354
  style: fieldLabel,
1834
- children: "URL"
2355
+ children: "Token URL"
1835
2356
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1836
2357
  style: inputStyle,
1837
- value: draft.url,
1838
- placeholder: "http://192.168.31.130:4999",
2358
+ value: draft.tokenUrl,
2359
+ placeholder: "https://192.168.31.130:4999/?token=…",
1839
2360
  onChange: (event) => {
1840
2361
  setDraft({
1841
2362
  ...draft,
1842
- url: event.target.value
2363
+ tokenUrl: event.target.value
1843
2364
  });
1844
2365
  }
1845
2366
  })] }),
@@ -1869,9 +2390,9 @@ function HubPage() {
1869
2390
  children: "Cancel"
1870
2391
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1871
2392
  type: "button",
1872
- disabled: !draftValid,
2393
+ disabled: !draftValid || meshBusy,
1873
2394
  onClick: () => {
1874
- addMachine();
2395
+ addRemote();
1875
2396
  },
1876
2397
  style: {
1877
2398
  padding: "7px 14px",
@@ -1922,71 +2443,23 @@ function HubPage() {
1922
2443
  }),
1923
2444
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
1924
2445
  style: listStyle,
1925
- children: [agents === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
1926
- style: {
1927
- padding: "9px 8px",
1928
- fontSize: 13,
1929
- color: "var(--dsw-alias-label-tertiary)",
1930
- listStyle: "none"
1931
- },
1932
- children: "Loading roster…"
1933
- }), agents?.map((agent) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverRow, {
1934
- clickable: switchUi.available,
1935
- onClick: switchUi.available ? (event) => {
1936
- rowDecision(agent, event);
1937
- } : void 0,
1938
- children: [
1939
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeMark, {
1940
- agentKey: agent.key,
1941
- size: 20
1942
- }),
1943
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1944
- style: {
1945
- fontWeight: 500,
1946
- whiteSpace: "nowrap"
1947
- },
1948
- children: agent.label
1949
- }),
1950
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1951
- style: {
1952
- flex: 1,
1953
- minWidth: 0,
1954
- overflow: "hidden",
1955
- textOverflow: "ellipsis",
1956
- whiteSpace: "nowrap",
1957
- fontSize: 12.5,
1958
- color: "var(--dsw-alias-label-tertiary)"
1959
- },
1960
- children: agent.machine
1961
- }),
1962
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1963
- style: {
1964
- ...chip,
1965
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1966
- fontSize: 10.5
1967
- },
1968
- children: displayPath(agent.path)
1969
- }),
1970
- isActive(agent) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1971
- style: chipSelf,
1972
- children: "current"
1973
- }),
1974
- switchUi.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1975
- "aria-current": isActive(agent) ? "page" : void 0,
1976
- style: {
1977
- fontSize: 12.5,
1978
- color: "var(--dsw-alias-label-secondary)",
1979
- textDecoration: "none",
1980
- cursor: "pointer"
1981
- },
1982
- onClick: (event) => {
1983
- event.stopPropagation();
1984
- rowDecision(agent, event);
1985
- },
1986
- children: "switch"
1987
- })
1988
- ]
1989
- }, 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
+ ]
1990
2463
  })
1991
2464
  ]
1992
2465
  })] })
@@ -2046,6 +2519,7 @@ function HubPanelIcon({ size }) {
2046
2519
  const [hover, setHover] = react.useState(false);
2047
2520
  const [box, setBox] = react.useState(null);
2048
2521
  const [agents, setAgents] = react.useState(() => rosterFromGlobal() ?? [NATIVE_LINK]);
2522
+ const [machineLabels, setMachineLabels] = react.useState(void 0);
2049
2523
  const fusion = fusionPage();
2050
2524
  const switchUi = switchAffordance(fusion);
2051
2525
  const [activeKey, setActiveKey] = react.useState(() => fusion ? uiState()?.activeAgent : void 0);
@@ -2097,6 +2571,9 @@ function HubPanelIcon({ size }) {
2097
2571
  fetchRoster().then((list) => {
2098
2572
  if (!cancelled) setAgents(list);
2099
2573
  }).catch(() => {});
2574
+ fetchMachines().then((list) => {
2575
+ if (!cancelled) setMachineLabels(machineLabelsFrom(list));
2576
+ }).catch(() => {});
2100
2577
  return () => {
2101
2578
  cancelled = true;
2102
2579
  };
@@ -2119,15 +2596,15 @@ function HubPanelIcon({ size }) {
2119
2596
  setOpen((value) => !value);
2120
2597
  };
2121
2598
  const items = [];
2122
- machineGroups(agents).forEach((group, groupIndex) => {
2599
+ if (machineLabels !== void 0) groupByMachine(agents, machineLabels).forEach((group, groupIndex) => {
2123
2600
  if (groupIndex > 0) items.push({
2124
2601
  type: "separator",
2125
- id: `machine-divider:${group.machine}`
2602
+ id: `machine-divider:${group.key}`
2126
2603
  });
2127
2604
  items.push({
2128
2605
  type: "label",
2129
- id: `machine:${group.machine}`,
2130
- text: group.machine
2606
+ id: `machine:${group.key}`,
2607
+ text: group.label
2131
2608
  });
2132
2609
  for (const agent of group.agents) items.push({
2133
2610
  id: agent.key,
@@ -2138,6 +2615,14 @@ function HubPanelIcon({ size }) {
2138
2615
  })
2139
2616
  });
2140
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
+ });
2141
2626
  /**
2142
2627
  * A menu pick. The row is a `menuitem` button — there is no anchor default
2143
2628
  * to cancel — but the shared table still owns whether the attempt runs