super-dsh 0.1.3 → 0.1.5-a

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 (132) hide show
  1. package/README.md +3 -3
  2. package/agent-agy/cordis.patch.yml +8 -4
  3. package/agent-agy/dist/agent-preset-agy.d.ts +23 -43
  4. package/agent-agy/dist/agent-preset-agy.js +29 -166
  5. package/agent-agy/dist/agent.d.ts +2 -1
  6. package/agent-agy/dist/agent.js +3 -2
  7. package/agent-agy/dist/agy-sessions.d.ts +2 -1
  8. package/agent-agy/dist/agy-sessions.js +2 -11
  9. package/agent-agy/dist/index.js +13 -12
  10. package/agent-agy/dist/world-plugin.d.ts +2 -1
  11. package/agent-agy/dist/world-plugin.js +31 -51
  12. package/agent-agy/package.json +6 -6
  13. package/agent-claude/cordis.patch.yml +8 -4
  14. package/agent-claude/dist/agent-preset-claude.js +29 -166
  15. package/agent-claude/dist/agent.js +10 -10
  16. package/agent-claude/dist/claude-client.js +22 -3
  17. package/agent-claude/dist/claude-home.js +0 -8
  18. package/agent-claude/dist/index.js +12 -15
  19. package/agent-claude/dist/session-map.js +2 -5
  20. package/agent-claude/dist/world-plugin.js +3 -3
  21. package/agent-claude/package.json +9 -9
  22. package/agent-codex/README.md +8 -8
  23. package/agent-codex/cordis.patch.yml +9 -5
  24. package/agent-codex/dist/agent-preset-codex.js +29 -168
  25. package/agent-codex/dist/agent.js +17 -12
  26. package/agent-codex/dist/codex-store.js +0 -9
  27. package/agent-codex/dist/index.js +13 -16
  28. package/agent-codex/dist/session-map.js +2 -5
  29. package/agent-codex/dist/world-plugin.js +31 -51
  30. package/agent-codex/package.json +14 -11
  31. package/agent-hermes/README.md +9 -8
  32. package/agent-hermes/cordis.patch.yml +8 -4
  33. package/agent-hermes/dist/agent-preset-hermes.js +29 -168
  34. package/agent-hermes/dist/agent.js +33 -19
  35. package/agent-hermes/dist/hermes-client.js +5 -5
  36. package/agent-hermes/dist/hermes-store.js +2 -11
  37. package/agent-hermes/dist/index.js +13 -12
  38. package/agent-hermes/dist/world-plugin.js +31 -52
  39. package/agent-hermes/package.json +14 -11
  40. package/agent-omp/README.md +2 -2
  41. package/agent-omp/cordis.patch.yml +12 -7
  42. package/agent-omp/dist/agent-preset-omp.js +30 -169
  43. package/agent-omp/dist/agent.js +13 -5
  44. package/agent-omp/dist/index.js +12 -22
  45. package/agent-omp/dist/mobile/gesture.js +2 -2
  46. package/agent-omp/dist/mobile/zoom-guard.js +4 -4
  47. package/agent-omp/dist/mobile-boot.js +2 -2
  48. package/agent-omp/dist/omp-cli.js +1 -1
  49. package/agent-omp/dist/omp-disk-discovery.js +1 -1
  50. package/agent-omp/dist/protocol.js +1 -1
  51. package/agent-omp/dist/sidecar-client.js +1 -1
  52. package/agent-omp/dist/world-plugin.js +37 -50
  53. package/agent-omp/package.json +18 -14
  54. package/agent-omp/sidecar/main.ts +2 -2
  55. package/agent-pi/README.md +5 -5
  56. package/agent-pi/cordis.patch.yml +8 -4
  57. package/agent-pi/dist/agent-preset-pi.js +29 -166
  58. package/agent-pi/dist/agent.js +18 -12
  59. package/agent-pi/dist/index.js +14 -16
  60. package/agent-pi/dist/pi-home.js +2 -12
  61. package/agent-pi/dist/world-plugin.js +31 -51
  62. package/agent-pi/package.json +14 -11
  63. package/client/index.js +1994 -150
  64. package/cordis.patch.yml +39 -15
  65. package/package.json +32 -30
  66. package/{agent-hub → superd-hub}/cordis.patch.yml +3 -3
  67. package/{agent-hub → superd-hub}/dist/agent-roster.d.ts +3 -1
  68. package/{agent-hub → superd-hub}/dist/agent-roster.js +4 -2
  69. package/superd-hub/dist/carrier.d.ts +62 -0
  70. package/superd-hub/dist/carrier.js +213 -0
  71. package/superd-hub/dist/fusion-transport.d.ts +150 -0
  72. package/superd-hub/dist/fusion-transport.js +369 -0
  73. package/{agent-hub → superd-hub}/dist/gateway.d.ts +9 -4
  74. package/superd-hub/dist/gateway.js +44 -0
  75. package/superd-hub/dist/hub-route.d.ts +74 -0
  76. package/superd-hub/dist/hub-route.js +188 -0
  77. package/{agent-hub → superd-hub}/dist/index.d.ts +14 -6
  78. package/{agent-hub → superd-hub}/dist/index.js +14 -6
  79. package/{agent-hub → superd-hub}/dist/roster.d.ts +2 -2
  80. package/{agent-hub → superd-hub}/dist/rpc.js +1 -1
  81. package/{agent-hub → superd-hub}/dist/spawn-world.d.ts +27 -6
  82. package/superd-hub/dist/spawn-world.js +279 -0
  83. package/superd-hub/dist/switch-core.d.ts +630 -0
  84. package/superd-hub/dist/switch-core.js +734 -0
  85. package/superd-hub/dist/system-head.d.ts +69 -0
  86. package/superd-hub/dist/system-head.js +194 -0
  87. package/superd-hub/dist/web-trust.d.ts +112 -0
  88. package/superd-hub/dist/web-trust.js +126 -0
  89. package/superd-hub/dist/world-entry.d.ts +38 -0
  90. package/superd-hub/dist/world-entry.js +47 -0
  91. package/{agent-hub → superd-hub}/dist/world-host.d.ts +15 -20
  92. package/superd-hub/dist/world-host.js +65 -0
  93. package/{agent-hub → superd-hub}/dist/world-join.d.ts +2 -1
  94. package/{agent-hub → superd-hub}/dist/world-join.js +30 -50
  95. package/superd-hub/dist/world-mount.d.ts +82 -0
  96. package/superd-hub/dist/world-mount.js +105 -0
  97. package/{agent-hub → superd-hub}/dist/world-mux.d.ts +21 -6
  98. package/superd-hub/dist/world-mux.js +392 -0
  99. package/{agent-hub → superd-hub}/dist/world-provision.d.ts +20 -4
  100. package/superd-hub/dist/world-provision.js +203 -0
  101. package/superd-hub/dist/world-web-server.d.ts +113 -0
  102. package/superd-hub/dist/world-web-server.js +99 -0
  103. package/superd-hub/lib/client/index.js +2086 -0
  104. package/{agent-hub → superd-hub}/package.json +12 -7
  105. package/agent-hub/dist/carrier.d.ts +0 -43
  106. package/agent-hub/dist/carrier.js +0 -241
  107. package/agent-hub/dist/gateway.js +0 -18
  108. package/agent-hub/dist/spawn-world.js +0 -72
  109. package/agent-hub/dist/world-entry.d.ts +0 -31
  110. package/agent-hub/dist/world-entry.js +0 -51
  111. package/agent-hub/dist/world-host.js +0 -44
  112. package/agent-hub/dist/world-mount.d.ts +0 -52
  113. package/agent-hub/dist/world-mount.js +0 -52
  114. package/agent-hub/dist/world-mux.js +0 -224
  115. package/agent-hub/dist/world-provision.js +0 -122
  116. package/agent-hub/dist/world-web-server.d.ts +0 -162
  117. package/agent-hub/dist/world-web-server.js +0 -248
  118. package/agent-hub/lib/client/index.js +0 -242
  119. /package/{agent-hub → superd-hub}/dist/client-shim.d.ts +0 -0
  120. /package/{agent-hub → superd-hub}/dist/client-shim.js +0 -0
  121. /package/{agent-hub → superd-hub}/dist/envelope.d.ts +0 -0
  122. /package/{agent-hub → superd-hub}/dist/envelope.js +0 -0
  123. /package/{agent-hub → superd-hub}/dist/index-pass.d.ts +0 -0
  124. /package/{agent-hub → superd-hub}/dist/index-pass.js +0 -0
  125. /package/{agent-hub → superd-hub}/dist/labels.d.ts +0 -0
  126. /package/{agent-hub → superd-hub}/dist/labels.js +0 -0
  127. /package/{agent-hub → superd-hub}/dist/ownership.d.ts +0 -0
  128. /package/{agent-hub → superd-hub}/dist/ownership.js +0 -0
  129. /package/{agent-hub → superd-hub}/dist/roster.js +0 -0
  130. /package/{agent-hub → superd-hub}/dist/rpc.d.ts +0 -0
  131. /package/{agent-hub → superd-hub}/dist/targets.d.ts +0 -0
  132. /package/{agent-hub → superd-hub}/dist/targets.js +0 -0
package/client/index.js CHANGED
@@ -27,190 +27,2019 @@ let react = require("react");
27
27
  react = __toESM(react);
28
28
  let react_jsx_runtime = require("react/jsx-runtime");
29
29
  react_jsx_runtime = __toESM(react_jsx_runtime);
30
+ let __deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
31
+ __deepseek_ai_dsh_client_ui_primitives = __toESM(__deepseek_ai_dsh_client_ui_primitives);
30
32
 
31
- //#region src/client/RuntimeSeat.tsx
32
- const NATIVE = {
33
+ //#region src/client/glyph.tsx
34
+ /**
35
+ * The Hub mark: the Agent Teams icon — four quadrant blocks meeting at the
36
+ * center (one per agent, converging into a team), matching the artwork the
37
+ * native Plugins page shows for the Agent Teams official bundle. Redrawn
38
+ * in-bundle because the client bundle's purity gate forbids value imports
39
+ * outside CLIENT_EXTERNALS. The gradient id is unique per instance so repeated
40
+ * marks in one document keep distinct `url(#…)` references.
41
+ *
42
+ * @module superd-hub/client/glyph
43
+ */
44
+ /** Sanitized useId: raw React ids contain colons, kept out of url(#…) refs. */
45
+ function useGlyphId$1() {
46
+ return `superd_hub_glyph_${react.useId().replaceAll(":", "")}`;
47
+ }
48
+ /**
49
+ * Render the Hub mark.
50
+ *
51
+ * @param props - `size`: square edge in pixels.
52
+ * @returns the inline SVG mark.
53
+ */
54
+ function HubGlyph({ size }) {
55
+ const uid = useGlyphId$1();
56
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
57
+ width: size,
58
+ height: size,
59
+ viewBox: "8.63 8.17 18.84 18.84",
60
+ fill: "none",
61
+ xmlns: "http://www.w3.org/2000/svg",
62
+ "aria-hidden": "true",
63
+ style: {
64
+ display: "block",
65
+ flex: "none"
66
+ },
67
+ children: [
68
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
69
+ d: "M13.3027 17.1077H9.03125V12.8372H13.3027V17.1077ZM17.5742 12.8372C17.574 15.1958 15.6623 17.1075 13.3037 17.1077V8.56567H17.5742V12.8372Z",
70
+ fill: `url(#${uid}a)`
71
+ }),
72
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
73
+ d: "M18.5229 12.8372V8.56567H22.7944V12.8372H27.0659V17.1077H22.7944C20.4357 17.1077 18.5232 15.1959 18.5229 12.8372Z",
74
+ fill: "#F2AF63"
75
+ }),
76
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
77
+ d: "M17.5742 26.6072H13.3037V18.0642C15.6625 18.0644 17.5742 19.9769 17.5742 22.3357V26.6072ZM13.3027 22.3357H9.03125V18.0642H13.3027V22.3357Z",
78
+ fill: "#7CB7FF"
79
+ }),
80
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
81
+ d: "M22.7944 26.6072H18.5229V22.3357C18.5229 19.9768 20.4355 18.0642 22.7944 18.0642H27.0659V22.3357H22.7944V26.6072Z",
82
+ fill: "#45D9E7"
83
+ }),
84
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("linearGradient", {
85
+ id: `${uid}a`,
86
+ x1: "16.1247",
87
+ y1: "6.6613",
88
+ x2: "9.03125",
89
+ y2: "19.2206",
90
+ gradientUnits: "userSpaceOnUse",
91
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", { stopColor: "#7CB7FF" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
92
+ offset: "1",
93
+ stopColor: "#145AF3"
94
+ })]
95
+ }) })
96
+ ]
97
+ });
98
+ }
99
+
100
+ //#endregion
101
+ //#region src/client/runtime-icons.tsx
102
+ /**
103
+ * Per-runtime marks for the Agent Runtimes roster. Native carries the DSH
104
+ * blue fish (the official website mark — the FishLogo geometry at the site's
105
+ * #4D6BFE, redrawn in-bundle so no scheme detection is needed); the foreign
106
+ * runtimes carry the curated agent-icon pack; any runtime whose key matches
107
+ * no asset falls back to the subagent artwork (two stacked rounded squares —
108
+ * the native Plugins page's stand-in for subagents) as the default
109
+ * foreign-agent mark.
110
+ *
111
+ * @module superd-hub/client/runtime-icons
112
+ */
113
+ /** Curated per-runtime assets, base64 data URIs keyed by roster key. */
114
+ const RUNTIME_ICON_URIS = {
115
+ claude: `data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjY0IiB3aWR0aD0iNjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNC43MDkgMTUuOTU1bDQuNzItMi42NDcuMDgtLjIzLS4wOC0uMTI4SDkuMmwtLjc5LS4wNDgtMi42OTgtLjA3My0yLjMzOS0uMDk3LTIuMjY2LS4xMjItLjU3MS0uMTIxTDAgMTEuNzg0bC4wNTUtLjM1Mi40OC0uMzIxLjY4Ni4wNiAxLjUyLjEwMyAyLjI3OC4xNTggMS42NTIuMDk3IDIuNDQ5LjI1NWguMzg5bC4wNTUtLjE1Ny0uMTM0LS4wOTgtLjEwMy0uMDk3LTIuMzU4LTEuNTk2LTIuNTUyLTEuNjg4LTEuMzM2LS45NzItLjcyNC0uNDkxLS4zNjQtLjQ2Mi0uMTU4LTEuMDA4LjY1Ni0uNzIyLjg4MS4wNi4yMjUuMDYxLjg5My42ODYgMS45MDggMS40NzYgMi40OTEgMS44MzMuMzY1LjMwNC4xNDUtLjEwMy4wMTktLjA3My0uMTY0LS4yNzQtMS4zNTUtMi40NDYtMS40NDYtMi40OS0uNjQ0LTEuMDMyLS4xNy0uNjE5YTIuOTcgMi45NyAwIDAxLS4xMDQtLjcyOUw2LjI4My4xMzQgNi42OTYgMGwuOTk2LjEzNC40Mi4zNjQuNjIgMS40MTQgMS4wMDIgMi4yMjkgMS41NTUgMy4wMy40NTYuODk4LjI0My44MzIuMDkxLjI1NWguMTU4VjkuMDFsLjEyOC0xLjcwNi4yMzctMi4wOTUuMjMtMi42OTUuMDgtLjc2LjM3Ni0uOTEuNzQ3LS40OTIuNTg0LjI4LjQ4LjY4NS0uMDY3LjQ0NC0uMjg2IDEuODUxLS41NTkgMi45MDMtLjM2NCAxLjk0MmguMjEybC4yNDMtLjI0Mi45ODUtMS4zMDYgMS42NTItMi4wNjQuNzMtLjgyLjg1LS45MDQuNTQ3LS40MzFoMS4wMzNsLjc2IDEuMTI5LS4zNCAxLjE2Ni0xLjA2NCAxLjM0Ny0uODgxIDEuMTQyLTEuMjY0IDEuNy0uNzkgMS4zNi4wNzMuMTEuMTg4LS4wMiAyLjg1Ni0uNjA2IDEuNTQzLS4yOCAxLjg0MS0uMzE1LjgzMy4zODguMDkxLjM5NS0uMzI4LjgwNy0xLjk2OS40ODYtMi4zMDkuNDYyLTMuNDM5LjgxMy0uMDQyLjAzLjA0OS4wNjEgMS41NDkuMTQ2LjY2Mi4wMzZoMS42MjJsMy4wMi4yMjUuNzkuNTIyLjQ3NC42MzgtLjA3OS40ODUtMS4yMTUuNjItMS42NC0uMzg5LTMuODI5LS45MS0xLjMxMi0uMzI5aC0uMTgydi4xMWwxLjA5MyAxLjA2OCAyLjAwNiAxLjgxIDIuNTA5IDIuMzMuMTI3LjU3OC0uMzIyLjQ1NS0uMzQtLjA0OS0yLjIwNS0xLjY1Ny0uODUxLS43NDctMS45MjYtMS42MmgtLjEyOHYuMTdsLjQ0NC42NDkgMi4zNDUgMy41MjEuMTIyIDEuMDgtLjE3LjM1My0uNjA4LjIxMy0uNjY4LS4xMjItMS4zNzQtMS45MjUtMS40MTUtMi4xNjctMS4xNDMtMS45NDMtLjE0LjA4LS42NzQgNy4yNTQtLjMxNi4zNy0uNzI5LjI4LS42MDctLjQ2MS0uMzIyLS43NDcuMzIyLTEuNDc2LjM4OS0xLjkyNC4zMTUtMS41My4yODYtMS45LjE3LS42MzItLjAxMi0uMDQyLS4xNC4wMTgtMS40MzQgMS45NjctMi4xOCAyLjk0NS0xLjcyNiAxLjg0NS0uNDE0LjE2NC0uNzE3LS4zNy4wNjctLjY2Mi40MDEtLjU4OSAyLjM4OC0zLjAzNiAxLjQ0LTEuODgyLjkzLTEuMDg2LS4wMDYtLjE1OGgtLjA1NUw0LjEzMiAxOC41NmwtMS4xMy4xNDYtLjQ4Ny0uNDU2LjA2MS0uNzQ2LjIzMS0uMjQzIDEuOTA4LTEuMzEyLS4wMDYuMDA2eiIgZmlsbD0iI0Q5Nzc1NyIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+Cg==`,
116
+ codex: `data:image/svg+xml;base64,PHN2ZyBmaWxsPSJjdXJyZW50Q29sb3IiIGZpbGwtcnVsZT0iZXZlbm9kZCIgaGVpZ2h0PSI2NCIgd2lkdGg9IjY0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkuMjA1IDguNjU4di0yLjI2YzAtLjE5LjA3Mi0uMzMzLjIzOC0uNDI4bDQuNTQzLTIuNjE2Yy42MTktLjM1NyAxLjM1Ni0uNTIzIDIuMTE3LS41MjMgMi44NTQgMCA0LjY2MiAyLjIxMiA0LjY2MiA0LjU2NiAwIC4xNjcgMCAuMzU3LS4wMjQuNTQ3bC00LjcxLTIuNzU5YS43OTcuNzk3IDAgMDAtLjg1NiAwbC01Ljk3IDMuNDczem0xMC42MDkgOC44VjEyLjA2YzAtLjMzMy0uMTQzLS41Ny0uNDI5LS43MzdsLTUuOTctMy40NzMgMS45NS0xLjExOGEuNDMzLjQzMyAwIDAxLjQ3NiAwbDQuNTQzIDIuNjE3YzEuMzA5Ljc2IDIuMTg5IDIuMzc4IDIuMTg5IDMuOTQ4IDAgMS44MDgtMS4wNyAzLjQ3My0yLjc2IDQuMTYzek03LjgwMiAxMi43MDNsLTEuOTUtMS4xNDJjLS4xNjctLjA5NS0uMjM5LS4yMzgtLjIzOS0uNDI4VjUuODk5YzAtMi41NDUgMS45NS00LjQ3MiA0LjU5MS00LjQ3MiAxIDAgMS45MjcuMzMzIDIuNzEyLjkyOEw4LjIzIDUuMDY3Yy0uMjg1LjE2Ni0uNDI4LjQwNC0uNDI4LjczN3Y2Ljg5OHpNMTIgMTUuMTI4bC0yLjc5NS0xLjU3di0zLjMzTDEyIDguNjU4bDIuNzk1IDEuNTd2My4zM0wxMiAxNS4xMjh6bTEuNzk2IDcuMjNjLTEgMC0xLjkyNy0uMzMyLTIuNzEyLS45MjdsNC42ODYtMi43MTJjLjI4NS0uMTY2LjQyOC0uNDA0LjQyOC0uNzM3di02Ljg5OGwxLjk3NCAxLjE0MmMuMTY3LjA5NS4yMzguMjM4LjIzOC40Mjh2NS4yMzNjMCAyLjU0NS0xLjk3NCA0LjQ3Mi00LjYxNCA0LjQ3MnptLTUuNjM3LTUuMzAzbC00LjU0NC0yLjYxN2MtMS4zMDgtLjc2MS0yLjE4OC0yLjM3OC0yLjE4OC0zLjk0OEE0LjQ4MiA0LjQ4MiAwIDAxNC4yMSA2LjMyN3Y1LjQyM2MwIC4zMzMuMTQzLjU3MS40MjguNzM4bDUuOTQ3IDMuNDQ5LTEuOTUgMS4xMThhLjQzMi40MzIgMCAwMS0uNDc2IDB6bS0uMjYyIDMuOWMtMi42ODggMC00LjY2Mi0yLjAyMS00LjY2Mi00LjUxOSAwLS4xOS4wMjQtLjM4LjA0Ny0uNTdsNC42ODYgMi43MWMuMjg2LjE2Ny41NzEuMTY3Ljg1NiAwbDUuOTctMy40NDh2Mi4yNmMwIC4xOS0uMDcuMzMzLS4yMzcuNDI4bC00LjU0MyAyLjYxNmMtLjYxOS4zNTctMS4zNTYuNTIzLTIuMTE3LjUyM3ptNS44OTkgMi44M2E1Ljk0NyA1Ljk0NyAwIDAwNS44MjctNC43NTZDMjIuMjg3IDE4LjMzOSAyNCAxNS44NCAyNCAxMy4yOTZjMC0xLjY2NS0uNzEzLTMuMjgyLTEuOTk4LTQuNDQ4LjExOS0uNS4xOS0uOTk5LjE5LTEuNDk4IDAtMy40MDEtMi43NTktNS45NDctNS45NDYtNS45NDctLjY0MiAwLTEuMjYuMDk1LTEuODguMzFBNS45NjIgNS45NjIgMCAwMDEwLjIwNSAwYTUuOTQ3IDUuOTQ3IDAgMDAtNS44MjcgNC43NTdDMS43MTMgNS40NDcgMCA3Ljk0NSAwIDEwLjQ5YzAgMS42NjYuNzEzIDMuMjgzIDEuOTk4IDQuNDQ4LS4xMTkuNS0uMTkgMS0uMTkgMS40OTkgMCAzLjQwMSAyLjc1OSA1Ljk0NiA1Ljk0NiA1Ljk0Ni42NDIgMCAxLjI2LS4wOTUgMS44OC0uMzA5YTUuOTYgNS45NiAwIDAwNC4xNjIgMS43MTN6Ii8+PC9zdmc+Cg==`,
117
+ omp: `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnaWQtb21wLWdyYWRpZW50IiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSJva2xjaCgwLjcgMC4yNCAzNDApIi8+PHN0b3Agb2Zmc2V0PSIuNSIgc3RvcC1jb2xvcj0ib2tsY2goMC42MiAwLjIxIDI5NSkiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9Im9rbGNoKDAuODEgMC4xNCAyMDApIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZmlsbD0idXJsKCNnaWQtb21wLWdyYWRpZW50KSIgZD0iTTEwIDE0aDQ0djlINDN2MzNoLTlWMjNoLTl2MjJoLTlWMjNIMTB6Ii8+PC9zdmc+Cg==`,
118
+ pi: `data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjY0IiB3aWR0aD0iNjQiIHZpZXdCb3g9IjAgMCA4MDAgODAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGZpbGw9IiNGRkZGRkYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTE2NS4yOSAxNjUuMjkgSDUxNy4zNiBWNDAwIEg0MDAgVjUxNy4zNiBIMjgyLjY1IFY2MzQuNzIgSDE2NS4yOSBaIE0yODIuNjUgMjgyLjY1IFY0MDAgSDQwMCBWMjgyLjY1IFoiLz48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNNTE3LjM2IDQwMCBINjM0LjcyIFY2MzQuNzIgSDUxNy4zNiBaIi8+PC9zdmc+Cg==`,
119
+ hermes: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAPFBMVEVzc3Pn5+ccHBszMzMBAQH///8AAAAYGBgLCwscHBxLS0vGxsbX19dfX19+fn7z8/OgoKCysrKGhoaSkpIgp1CAAAAACnRSTlP9////ff//gICA//OAVQAABGdJREFUWIWll4uWnCAMQANUuiXh/f//2gRQAZ1te8rZs6MOXPNOBn7Af60f8Cse/7HUF/xUn76MmIJ3zodM/w6IRDml4uxYgO+CfgBQShnba1U6ESa9IV4BebxXB/lK+QuBfwXI2t6rypOy3n4PIGeX5eTbcN36PwGS3ZcWzS8tLHwLiP5xnDVnQtTnrU7fAJTZT2vtq5eX5v5Ae4v4EaBMe89N8c5mMaoccSKMNiGpED8BjNO6ndKDYUhsZsHwf7SWTVlEm/QBUCoTjE38KvBOGOnIKpJxSkKqpphTjCyQia8AQiJvTMrVGeRHThIgskLGuiL+0Zg0uMTo/ArI/KFqbW+jopTsihGaI8SVirXjS8cAH18AlChGRVB9CRokgkz1tQ5rdDNa/muGohcAeO0L6OGD0+1mXJRDEQe0k7BgQHkBaDf5z27xbD00J8hzPWJrA9Dlu+7AjaCT9m6A9VBpBdRL7H4grAC2CHuAwabvCw8AbAeSXm61JAmf1q4D3ANwZdE4CHUFdN3ZGONefQaMK1pFuFXpn/hZhVFHfbeCeRCGii9GPBd5KLaqI0EpIe+EISDsADo3tNJbsTifh7NXXYYO5hFIvlmqUE+U3EpRV3TIMOLktJHaAapJB1jbKRLRi4V4AQIi+UkH3AFcj0wSo41UKxyvQJMEdrjYd5XqA8AZn9Em0+I8YoYS6MUGLRjtHUprVVY2lO4gzmw1jLg7cujwCkBrpkztK34A0Bsg28f528GbI9MbILFt9hacd8AQwb8BqlUYt2Hi6qx6Beg3ANgY9xa+F6crHegF4Cwbf7Ph5sVWFScjrAAu+Rj6ZHLaYrehVIYugnsC+G2eG5yBSQ1c9Ze6IlkhUsQHgPPBqoQHTmr0PL8tKScF4Ec6LAARN6h1lmqVxuAsQasyeVTWBdC2TePG5YSibgmGQqh7TRiAPgS2+WadYboT6JpQWmltgNCN0AHU63yY/HsCesyco9M16VjM3QgdUHoKzO6Z7Gr9FUy3N5D1TQoHwIdrs+wnTuZcId9hkGEHEErL9QPgzGWCeeUB4CKVt/M2ZjFXyQ3A013oe/UauNRtQE+4kSrsj+SjABScRbfs25pnHW7P+eVChqOaLkEYhbOWtUlbsO8dzqIYJRyg1c9mg96Asmixpi88ClJbukV4OkyXgDVE5BxoVlgaPSxNbwK0/+wJPyLRW5e6q+rcDOvh1TuhUThw8gBcU/aUNrbyZJm5bOCjRfeV2JPxSibsfSvOJohovKYRDM61YdmFU0dNYoUpG8lRXGcldhN3Kw4iFwqbHZCUwlJ6j7TJSWWe09k6EGEnKyJVUSCTwrudkpFMlARxcQV09dMUNO5g/7KYMgVODVAn1038KGkVCh2zybR1dCiltJ2LteKR3obR1B+/2u5O5NhLKJ360GuRYLXc2QMfgLsPmNrV9px4e7cBNw0Y64/v4UbIPKy2ccenAx4tV10/hBnwBeFe47zj4V3rwg+8haJ92Fc5L+DrN9ijZSiX90rqAAAAAElFTkSuQmCC`,
120
+ agy: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAABYlBMVEVHcEw5iPw2i/IziPztaDo3ifeJwGA6iPhkhug3ifjtVEg6ivgujO00h/8wivRztHQ0iftrgdLrhy41ifA0iPs+mMJNrp7rWEgwifjgryrcVmJ4wXDiUlmPeMCGxWK3w0FhprPZVmJato0pktxVjflCqKqjbqeLxWKQeL/opSZBp6z1Uj3Xuy01ifwwiPg1h/87if8wh/wvivRCiv4vi+8zktxNjPsvjek1ltEwj+Q5nMTwV0BDpK9Rg+hZifJ5e8tMq6C7ZHk+h/k2h/nOW2lXsJBAn7mGdrx1vG9dgd1Jh/SebZ3lU07meDlitoKuZo1kfszdWlOgvlBWk69wgttBhe/Ia14/iOTaZ01Cj87gpSxzfbOHdqeVcaxMh9O7uz90loxwiKFah77GelWfrVdToKGSe4uud2+kcIXlky6Sm22+qENyp37Mh0iHq2pfpo6tmVeuh2LSszGJhonJlkKWi3VeO12PAAAALXRSTlMARBro/o/8f/1lxVMt8q79vAf6/cv6i23YVBo4QcePmg6TzuC5S3QaM6re4bufpM1dAAADxklEQVRYhZ2X+T9iURTAXz2VVIQYxowxY4wxM7wShWwVSrIvlchStopR+P/n3O0t3tqcH933/d5zzj339sFxBjE40dOz3dbxwegbI/z09AEE221tsf9STJyC4IEIYh2t83+aTVEAht+t8l+aRPC4TVJoNYfBi4tms3b6+voIhjw2fG1JcAFRq9WwoJxv2wdB7GMrBcgE5e18Pr8fy7ZSxPgBRKVSe35+fmyUy2VsiMU+WU8A85UKCBpIcAKCbDZrOYURkkDlqfpcbSDDyUl+HxmspjB84HQ6nyCq1epbo3F3d0INVlNwyniIO2RYW0OGbkv8kNOZTnd1ddXr9cvLt5e3WzDEicHanRjGfB3zl5cvL7e39/fxeBwMFmvA/E79LwQIrmSGtX0rwzSUTqd3dtYBv7k5PLyCOEOGXWywUEP3MObXAUcCMBSvzs62qMFKDV0if4ji+rpYLIJhaxcbzPlxzK+uLi5ubBz2AS8z7MbXzGepX+L7+gqFAjLsIcM5TsL8Un9T8oVSqbS3t7dSTFGDqYDxCwtzc8DnmCGVOscGs4MckvjC3OzsbC6XWyqVNsFAFLs/zFog7Y/w+RwyLG1Khs9mLZDx8/PRaDSHFZugoAZjvlvOR6Mzvb29x8fHS0s4CWRInf80FIwp+RlqOMaGoxWkMG5Cv5Kfnp5KJpMJ0QCKlHETfsl5wKeQIJkARYYYQGHEj0j8DOGxAAyZDBja25HAqAlj8v0RHgqFiAAZlrHhyGXUAjUPIYiG5XZQGDWhT1F/iPKCMCk3tOs/rXbF/qGQz+9wu12dvDAZDCbCCSRACpuuwE3nj/C8h/3dFhCCwWA4HIlEkMGhK+iU56/4zI0F4Qwx6AogAbw/4t3KJRsxkBzsOrxXxqvOSm5wa9EQDjpAwHeqV11YgAyR7zqCUfEAfFrLAZZCJKJ9kHapgZoHZReLiHi01jmX2AC/doYu0aBdwyi7QSG9LvvENmiterUnQB50GsLaNTjEG6A/6zwzaBXJTlA9AlLYBGYYUa15WAJJfZ7jBphBvcsAK0BvzHB4BWRACtUKK4A34jnOzwzv9/GzBPQvOw67QA3vNvIyPmDMw2ERQTCsPMkA8NPoDdObISl8NAdFCh46gkZHyMImCEQh/7aX8gPmPLxbzCBl20l+RKwUwIpACrFfbsZr31JVeAVqoJfGy34E9F/bd+GhBgEPg53xGs+YXriYAU0NT/PXeUW0w8EM3Zyr9f3lOTg4fqq1+lm4icDHEd7kBmiFF58mz9n40IDL2r8yqiQCvN/+D+aPcPZ+RgT3AAAAAElFTkSuQmCC`
121
+ };
122
+ /**
123
+ * The DSH fish silhouette (FishLogo geometry, tight 23.16:17.04 viewBox) at
124
+ * the official site's brand blue.
125
+ *
126
+ * @param props - `size`: width in pixels (height keeps the native ratio).
127
+ * @returns the blue fish mark.
128
+ */
129
+ function DshFish({ size }) {
130
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
131
+ width: size,
132
+ height: size * 17.04 / 23.16,
133
+ viewBox: "0 0 23.16 17.04",
134
+ fill: "none",
135
+ xmlns: "http://www.w3.org/2000/svg",
136
+ "aria-hidden": "true",
137
+ style: {
138
+ display: "block",
139
+ flex: "none"
140
+ },
141
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
142
+ fill: "#4D6BFE",
143
+ d: "M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018ZM11.1749 14.4736C9.15936 12.889 8.18184 12.3675 7.77832 12.39C7.40081 12.4125 7.46881 12.8445 7.55182 13.126C7.63882 13.404 7.75182 13.5955 7.91033 13.8396C8.01983 14.0011 8.09533 14.2411 7.80083 14.4216C7.15181 14.8231 6.02327 14.2866 5.97027 14.2601C4.65673 13.4865 3.5587 12.4655 2.78467 11.069C2.03715 9.72493 1.60314 8.28289 1.53164 6.74384C1.51264 6.37233 1.62214 6.24082 1.99215 6.17332C2.47916 6.08332 2.98118 6.06432 3.46769 6.13582C5.52476 6.43633 7.27581 7.35586 8.74385 8.8129C9.58188 9.64243 10.2159 10.634 10.8689 11.6025C11.5634 12.631 12.3105 13.611 13.262 14.4146C13.598 14.6961 13.866 14.9101 14.1225 15.0681C13.349 15.1546 12.058 15.1731 11.1749 14.4746L11.1749 14.4736ZM12.141 8.25988C12.141 8.09488 12.273 7.96338 12.439 7.96338C12.4765 7.96338 12.5105 7.97088 12.541 7.98188C12.5825 7.99688 12.6205 8.01938 12.6505 8.05338C12.7035 8.10588 12.7335 8.18088 12.7335 8.25988C12.7335 8.42489 12.6015 8.55639 12.4355 8.55639C12.2695 8.55639 12.141 8.42489 12.141 8.25988ZM15.1415 9.79893C14.949 9.87793 14.7565 9.94544 14.5715 9.95294C14.2845 9.96794 13.9715 9.85143 13.8015 9.70893C13.5375 9.48742 13.3485 9.36342 13.2695 8.97691C13.2355 8.8119 13.2545 8.55639 13.2845 8.40989C13.3525 8.09438 13.277 7.89187 13.0545 7.70787C12.8735 7.55786 12.6435 7.51636 12.39 7.51636C12.2955 7.51636 12.2095 7.47486 12.1445 7.44136C12.0395 7.38886 11.9519 7.25735 12.035 7.09585C12.0615 7.04335 12.19 6.91584 12.22 6.89334C12.5635 6.69784 12.9595 6.76184 13.326 6.90834C13.6655 7.04735 13.9225 7.30236 14.292 7.66287C14.6695 8.09838 14.7375 8.21838 14.9525 8.54539C15.1225 8.8009 15.2775 9.06341 15.3831 9.36392C15.4471 9.55142 15.3641 9.70493 15.1415 9.79893Z"
144
+ })
145
+ });
146
+ }
147
+ /** Opaque tile backgrounds for marks that would vanish on a dark frame. */
148
+ const RUNTIME_TILE_BG = {
149
+ codex: "#FFFFFF",
150
+ pi: "#000000"
151
+ };
152
+ /** Sanitized useId: raw React ids contain colons, kept out of url(#…) refs. */
153
+ function useGlyphId() {
154
+ return `superd_subagent_${react.useId().replaceAll(":", "")}`;
155
+ }
156
+ /**
157
+ * The subagent mark: two stacked rounded squares (the Plugins page's
158
+ * subagent artwork, redrawn in-bundle) — the default foreign-agent icon.
159
+ *
160
+ * @param props - `size`: square edge in pixels.
161
+ * @returns the subagent mark.
162
+ */
163
+ function SubagentGlyph({ size }) {
164
+ const uid = useGlyphId();
165
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
166
+ width: size,
167
+ height: size,
168
+ viewBox: "8.47 8.67 19.16 19.16",
169
+ fill: "none",
170
+ xmlns: "http://www.w3.org/2000/svg",
171
+ "aria-hidden": "true",
172
+ style: {
173
+ display: "block",
174
+ flex: "none"
175
+ },
176
+ children: [
177
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
178
+ x: "14.99",
179
+ y: "15.19",
180
+ width: "12.24",
181
+ height: "12.24",
182
+ rx: "2.4",
183
+ fill: `url(#${uid}a)`,
184
+ fillOpacity: "0.85"
185
+ }),
186
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
187
+ x: "8.87",
188
+ y: "9.07",
189
+ width: "12.24",
190
+ height: "12.24",
191
+ rx: "2.4",
192
+ fill: `url(#${uid}b)`,
193
+ fillOpacity: "0.85"
194
+ }),
195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("defs", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("linearGradient", {
196
+ id: `${uid}a`,
197
+ x1: "21.11",
198
+ y1: "15.19",
199
+ x2: "21.11",
200
+ y2: "27.42",
201
+ gradientUnits: "userSpaceOnUse",
202
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", { stopColor: "#45E7A4" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
203
+ offset: "1",
204
+ stopColor: "#05909D"
205
+ })]
206
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("linearGradient", {
207
+ id: `${uid}b`,
208
+ x1: "14.99",
209
+ y1: "9.07",
210
+ x2: "14.99",
211
+ y2: "21.31",
212
+ gradientUnits: "userSpaceOnUse",
213
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", { stopColor: "#69B9FF" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
214
+ offset: "1",
215
+ stopColor: "#324DE2"
216
+ })]
217
+ })] })
218
+ ]
219
+ });
220
+ }
221
+ /** One embedded square image mark at the requested edge. */
222
+ function imgMark(uri, size) {
223
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
224
+ src: uri,
225
+ alt: "",
226
+ width: size,
227
+ height: size,
228
+ style: {
229
+ display: "block",
230
+ flex: "none",
231
+ objectFit: "contain"
232
+ }
233
+ });
234
+ }
235
+ /**
236
+ * Render one runtime's mark: the DSH app icon for native (scheme-aware), the
237
+ * curated asset for a matching roster key, the subagent mark otherwise.
238
+ *
239
+ * @param props - `agentKey`: the roster key; `size`: square edge in pixels.
240
+ * @returns the runtime mark.
241
+ */
242
+ function RuntimeMark({ agentKey, size }) {
243
+ if (agentKey === "native") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DshFish, { size });
244
+ const uri = RUNTIME_ICON_URIS[agentKey];
245
+ if (uri === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubagentGlyph, { size });
246
+ const tile = RUNTIME_TILE_BG[agentKey];
247
+ if (tile === void 0) return imgMark(uri, size);
248
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
249
+ "aria-hidden": "true",
250
+ style: {
251
+ width: size,
252
+ height: size,
253
+ flex: "none",
254
+ borderRadius: Math.max(3, Math.round(size * .2)),
255
+ background: tile,
256
+ display: "inline-flex",
257
+ alignItems: "center",
258
+ justifyContent: "center"
259
+ },
260
+ children: imgMark(uri, Math.round(size * .74))
261
+ });
262
+ }
263
+
264
+ //#endregion
265
+ //#region src/client/roster.ts
266
+ /** The native runtime every composition can fall back to. */
267
+ const NATIVE_LINK = {
33
268
  key: "native",
34
269
  label: "DSH",
35
- path: "/"
270
+ path: "/",
271
+ machine: "local"
36
272
  };
37
- /** Fallback labels for the read-only face (which only reports keys). */
38
- const RUNTIME_LABELS = {
273
+ /** Labels for the read-only face's bare keys (it reports keys, not labels). */
274
+ const FALLBACK_LABELS = {
39
275
  native: "DSH",
40
276
  omp: "OMP",
41
- codex: "Codex"
277
+ codex: "Codex",
278
+ claude: "Claude",
279
+ pi: "Pi",
280
+ hermes: "Hermes",
281
+ agy: "Antigravity"
42
282
  };
43
- function fromGlobal() {
44
- const agents = globalThis.__DSH_AGENT_ROSTER__?.agents;
45
- if (!Array.isArray(agents)) return void 0;
283
+ /** Coerce a wire `agents` array into links; undefined when unusable/empty. */
284
+ function coerceAgents(raw) {
285
+ if (!Array.isArray(raw)) return void 0;
46
286
  const links = [];
47
- for (const entry of agents) {
287
+ for (const entry of raw) {
48
288
  if (typeof entry !== "object" || entry === null) continue;
49
- const { key, label, path } = entry;
289
+ const { key, label, path, machine } = entry;
50
290
  if (typeof key !== "string" || typeof path !== "string") continue;
51
291
  links.push({
52
292
  key,
53
- label: typeof label === "string" ? label : RUNTIME_LABELS[key] ?? key,
54
- path
293
+ label: typeof label === "string" ? label : FALLBACK_LABELS[key] ?? key,
294
+ path,
295
+ machine: typeof machine === "string" ? machine : "local"
55
296
  });
56
297
  }
57
298
  return links.length > 0 ? links : void 0;
58
299
  }
300
+ /** Seed from the index-baked roster global, when the page carries one. */
301
+ function rosterFromGlobal() {
302
+ const raw = globalThis.__DSH_AGENT_ROSTER__;
303
+ return coerceAgents(raw?.agents);
304
+ }
59
305
  /** Normalize the current mount root: `/omp` and `/omp/` are the same page. */
60
- function currentPath() {
306
+ function currentMountPath() {
61
307
  const pathname = typeof location === "undefined" ? "/" : location.pathname;
62
308
  if (pathname === "" || pathname === "/") return "/";
63
309
  return pathname.endsWith("/") ? pathname : `${pathname}/`;
64
310
  }
65
- function RuntimeSeat({ wide }) {
66
- const [agents, setAgents] = react.useState(() => fromGlobal() ?? [NATIVE]);
67
- const [open, setOpen] = react.useState(false);
68
- const rootRef = react.useRef(null);
69
- react.useEffect(() => {
70
- let cancelled = false;
71
- fetch("/api/agent-runtime", { credentials: "same-origin" }).then((r) => r.ok ? r.json() : Promise.reject(/* @__PURE__ */ new Error(`HTTP ${r.status}`))).then((j) => {
72
- if (cancelled) return;
73
- if (Array.isArray(j?.agents)) {
74
- const listed = [];
75
- for (const entry of j.agents) {
76
- if (typeof entry !== "object" || entry === null) continue;
77
- const { key, label, path } = entry;
78
- if (typeof key !== "string" || typeof path !== "string") continue;
79
- listed.push({
80
- key,
81
- label: typeof label === "string" ? label : RUNTIME_LABELS[key] ?? key,
82
- path
83
- });
311
+ /**
312
+ * Ask the hub for the live roster (preferred `agents` shape with paths; the
313
+ * bare `available` key list falls back to `/<key>/` paths).
314
+ *
315
+ * @returns the runtime links, keyed by runtime.
316
+ * @throws when the face is unreachable or returns no usable roster.
317
+ */
318
+ async function fetchRoster() {
319
+ const response = await fetch("/api/agent-runtime", { credentials: "same-origin" });
320
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
321
+ const json = await response.json();
322
+ const agents = coerceAgents(json.agents);
323
+ if (agents !== void 0) return agents;
324
+ if (Array.isArray(json.available)) {
325
+ const links = [];
326
+ for (const key of json.available) {
327
+ if (typeof key !== "string") continue;
328
+ links.push(key === "native" ? NATIVE_LINK : {
329
+ key,
330
+ label: FALLBACK_LABELS[key] ?? key,
331
+ path: `/${key}/`,
332
+ machine: "local"
333
+ });
334
+ }
335
+ if (links.length > 0) return links;
336
+ }
337
+ throw new Error("agent-runtime face returned no roster");
338
+ }
339
+
340
+ //#endregion
341
+ //#region src/client/machines-store.ts
342
+ /** Storage slot for the ledger (v1 shape; bump the suffix on breaking edits). */
343
+ const STORAGE_KEY = "superd-hub.machines.v1";
344
+ /**
345
+ * The self row is DERIVED, never stored: this machine's label tracks the
346
+ * product copy and its URL tracks the serving origin on every load. Only
347
+ * remote machines persist.
348
+ */
349
+ function selfMachine() {
350
+ return {
351
+ id: "self",
352
+ name: "local",
353
+ label: "This machine",
354
+ url: typeof location === "undefined" ? "" : location.origin,
355
+ self: true,
356
+ addedAt: 0
357
+ };
358
+ }
359
+ /**
360
+ * Read the ledger; a missing or unreadable store resets to the self row.
361
+ *
362
+ * @returns the machines: the derived self row first, then the stored remotes.
363
+ */
364
+ function loadMachines() {
365
+ const remotes = readStoredRemotes();
366
+ return [selfMachine(), ...remotes];
367
+ }
368
+ /** Parse the stored remote machines; storage failures degrade to none. */
369
+ function readStoredRemotes() {
370
+ if (typeof localStorage === "undefined") return [];
371
+ let parsed;
372
+ try {
373
+ const raw = localStorage.getItem(STORAGE_KEY);
374
+ if (raw === null) return [];
375
+ parsed = JSON.parse(raw);
376
+ } catch {
377
+ return [];
378
+ }
379
+ if (!Array.isArray(parsed)) return [];
380
+ const list = [];
381
+ for (const entry of parsed) {
382
+ if (typeof entry !== "object" || entry === null) continue;
383
+ const { id, name, label, url, self, addedAt } = entry;
384
+ if (self === true) continue;
385
+ if (typeof id !== "string" || typeof name !== "string" || typeof url !== "string") continue;
386
+ list.push({
387
+ id,
388
+ name,
389
+ label: typeof label === "string" ? label : name,
390
+ url,
391
+ self: false,
392
+ addedAt: typeof addedAt === "number" ? addedAt : 0
393
+ });
394
+ }
395
+ return list;
396
+ }
397
+ /**
398
+ * Persist the ledger; storage failures degrade silently to in-memory state.
399
+ *
400
+ * @param list - the machines to store (self row included).
401
+ */
402
+ function saveMachines(list) {
403
+ if (typeof localStorage === "undefined") return;
404
+ try {
405
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
406
+ } catch {}
407
+ }
408
+ /** A fresh row id (UUID when available, time+random otherwise). */
409
+ function freshMachineId() {
410
+ const cryptoRef = globalThis.crypto;
411
+ if (typeof cryptoRef?.randomUUID === "function") return cryptoRef.randomUUID();
412
+ return `m-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
413
+ }
414
+
415
+ //#endregion
416
+ //#region src/switch-core.ts
417
+ /**
418
+ * Switch orchestration core (native UI fusion, plan Task 4) — the pure logic
419
+ * behind the in-page agent switch: storage-namespace key computation, the
420
+ * session landing decision over the persisted `dsh.sessions.current`
421
+ * selection, the agent-selector click-interception decision (a fusion page
422
+ * never navigates on a left click — spec: 融合页 = state 切换,拦截 click,无导航;
423
+ * T-B: consumed by the Hub UI surfaces, the sole switch surface since the
424
+ * RuntimeSeat retirement), and the fallback state machine (timeout or
425
+ * roster-probe failure → revert to native + a visible toast, never a page
426
+ * lock).
427
+ *
428
+ * Host-half on purpose: plain TS with every effect injected through
429
+ * {@link SwitchHost} — no DOM, no client imports — so `test/client-switch.test.mjs`
430
+ * imports it from `dist/` like every other host module. The client half
431
+ * (`src/client/switch.ts`) builds the real host face from the client context:
432
+ * the `__SUPERD_UI_STATE__` atom (Task 3), `ctx.connection`,
433
+ * `ctx.uiWorkspace`, the Task-3 namespaced `Storage`, the control-plane
434
+ * roster fetch, and a minimal self-owned toast overlay.
435
+ *
436
+ * Orchestration sequence (spec §3.5 as amended 2026-09-26, T-A + T-F
437
+ * errata; user rulings 7/8 and the T-F ruling "按原生的语义去理解…把这个
438
+ * last visit session 也做一下"):
439
+ * 1. `setAgent(label)` — state + NS swap immediately; subsequent requests
440
+ * demux to the target world (Task 3's call-time patches read the atom).
441
+ * 2. `reconnect()` — the generation re-opens against the retargeted data
442
+ * plane; the gateway emits `connection/reset` on establishment, the
443
+ * designed "wire-derived caches must repull" signal, so sessions /
444
+ * config / theme re-derive on their own. Nothing navigates before the
445
+ * repull: the chat area is never cleared and no loading skeleton is
446
+ * inserted — old content stays in place awaiting the landing.
447
+ * 3. Session landing (T-F: NATIVE restore semantics) — upstream's
448
+ * fresh-load restore is `watchNavigation`/`restoreSelection`
449
+ * (navigation.ts:284-347): it fires only when BOTH `workspaces.list`
450
+ * and `sessions.list` reached phase `'ready'`, and then walks
451
+ * `saved.subagentAddress` → `saved.sessionId` looked up in the FRESH
452
+ * `sessions.byId` (stale-proof) → recent-workspace → the fresh-boot
453
+ * default workspace. The landing replicates exactly that: it fires once
454
+ * when BOTH wire-derived caches have installed fresh post-establishment
455
+ * baselines ({@link observeCachesRefreshed} — the T-A gate watched
456
+ * `workspaces.list` alone, so a plane whose sessions list landed later
457
+ * served stale rows to the decision and broke native's own
458
+ * last-visited restore; T-G additionally holds the gate while a
459
+ * persisted pointer has not yet joined the freshly minted INCREMENTAL
460
+ * sessions list — membership via `byId` — firing on membership or on
461
+ * the quiet-window ladder, {@link LANDING_QUIET_MS}, so partial-list
462
+ * first edges stop laddering every switch into a brand-new session),
463
+ * then reads the target NS's persisted
464
+ * `dsh.sessions.current` (the NS swap has long since happened) and
465
+ * executes {@link planRestoreLanding}'s precedence through the public
466
+ * faces. The landing is BOUNDED: if the baselines never install within
467
+ * one further fallback window after the settle, a staleness watchdog
468
+ * disarms it — an armed landing never outlives its switch by more than
469
+ * that window (the safe direction: no landing at all beats a late
470
+ * surprise navigation).
471
+ * 4. Fallback: no generation within {@link SWITCH_FALLBACK_MS}, or a failed
472
+ * roster probe → `setAgent('native')` + `reconnect()` back to the native
473
+ * plane + toast. A revert also cancels a pending landing — a
474
+ * reverted switch never lands.
475
+ *
476
+ * Upstream anchors (dsh-v0.1.7-rc.1):
477
+ * - `ctx.connection.reconnect()` — `packages/client/connection/src/client/index.ts`
478
+ * `ConnectionHandle.reconnect` (declared :148, wired :264-266); the
479
+ * controller resets retry progression and replaces the current attempt
480
+ * immediately (`connection.ts:113-121`).
481
+ * - `connection/reset` — emitted by the gateway client on every generation
482
+ * establishment (`packages/api/gateway/src/client/index.ts:169`),
483
+ * declared as the repull signal
484
+ * (`packages/client/connection/src/client/index.ts:9-17`).
485
+ * - `uiWorkspace.openSession(target)` / `startSession(workspaceId?)` /
486
+ * `openWorkspace(workspaceId)` — the navigation face
487
+ * (`packages/client/ui-workspace/src/client/navigation.ts:34-92`);
488
+ * `SessionTarget = SessionId | SubagentAddress`
489
+ * (`packages/api/session-controller/src/client/contract/sessions.ts:22`).
490
+ * - `watchNavigation` / `restoreSelection` — the fresh-load restore this
491
+ * landing replicates (`navigation.ts:284-347`): both-lists ready gate
492
+ * (:292), `subagentAddress` first (:320-323), the fresh-`byId` session
493
+ * lookup with the blank-reuse nuance (:324-335), `recentWorkspace`
494
+ * fallback (:337, the tie-break at :434-453), and
495
+ * `initializeDefaultWorkspace` on a fresh boot (:338-342, :349-361).
496
+ * - `startSession()`'s workspace resolution — `workspaceId ??
497
+ * currentWorkspaceId ?? recent` over the wire-derived `workspaces.list`
498
+ * / `sessions.list` snapshots (`navigation.ts:223-238`); `connectWorkspace`
499
+ * → `reuseOrCreateBlank` reuses the pointer's own blank whenever the
500
+ * private restore path would (:179-198).
501
+ * - The wire-derived stores: `workspaces.list` installs fresh data only
502
+ * through its follow-stream baseline (`packages/api/workspace-controller/src/client/model.ts`
503
+ * `replaceBaseline` → `installViews`, a new items array every install);
504
+ * `sessions.list` is projected with a fresh `ids` array on every manager
505
+ * change (`packages/api/session-controller/src/client/sessions/service.ts:607-677`)
506
+ * — array identity is the freshness token for both.
507
+ * - `dsh.sessions.current` — the per-tab last-selection store
508
+ * (`navigation.ts:130-131`); its restore reads `subagentAddress` first,
509
+ * then `sessionId` (`navigation.ts:319-330`) — {@link parseLandingTarget}
510
+ * keeps that precedence. The store hydrates once at page boot and
511
+ * persists on write (`packages/client/store/src/index.ts:155-174`), so
512
+ * the landing read must go through the namespaced `Storage` (Task 3
513
+ * patch 5), which prefixes `'<label>:'` at call time.
514
+ *
515
+ * @module @pgmi-builds/superd-hub/switch-core
516
+ */
517
+ /** The active-agent label meaning the native (ctx0) data plane. */
518
+ const NATIVE_AGENT = "native";
519
+ /** The persisted last-selection key ui-workspace owns (survey §2.6). */
520
+ const SESSIONS_CURRENT_KEY = "dsh.sessions.current";
521
+ /**
522
+ * The fallback window: how long a switch may wait for the target world's
523
+ * generation (mux re-open + repull) before reverting to native. A
524
+ * human-scale constant; Task 5's config face may expose it later.
525
+ */
526
+ const SWITCH_FALLBACK_MS = 1e4;
527
+ /**
528
+ * The membership-wait quiet window (T-G): after both navigation caches are
529
+ * fresh, a persisted pointer the sessions list has not shown yet holds the
530
+ * landing gate — the list is minted INCREMENTALLY post-reconnect (upstream
531
+ * `handleSessionAdded` applies one session at a time, each application
532
+ * re-projecting the store with a fresh `ids` array, service.ts:677), so the
533
+ * first fresh edge sees a partial `byId` that legitimately lacks the
534
+ * pointer's session. The gate fires on membership; while the pointer is
535
+ * still absent, this window of list SILENCE (no `ids` identity change) is
536
+ * the concession point: the gate fires anyway and the plan takes the ladder
537
+ * — an honest miss, because a stored blank pointer legitimately never
538
+ * appears (matching upstream's own boot restore of a stale/absent id). An
539
+ * identity change during the window restarts it (the list is still growing).
540
+ * The machine's post-settle staleness watchdog remains the hard bound on
541
+ * the whole wait.
542
+ */
543
+ const LANDING_QUIET_MS = 600;
544
+ /**
545
+ * The persisted-selection parse feeding {@link planRestoreLanding}.
546
+ *
547
+ * A value is navigable when it parses to a ui-workspace `MainSelection`
548
+ * carrying a usable `SessionTarget`: a non-null `subagentAddress` object
549
+ * wins (upstream restore order), else a non-empty string `sessionId`.
550
+ * Everything else — no value, a read failure (surfaced here as `null`), a
551
+ * parse failure, an unusable shape — parses to `null` and the plan falls to
552
+ * the recent-workspace / default-workspace ladder.
553
+ *
554
+ * @returns the subagent address object, the session id string, or `null`.
555
+ */
556
+ function parseLandingTarget(raw) {
557
+ if (typeof raw !== "string" || raw === "") return null;
558
+ let parsed;
559
+ try {
560
+ parsed = JSON.parse(raw);
561
+ } catch {
562
+ return null;
563
+ }
564
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
565
+ const selection = parsed;
566
+ if (selection.subagentAddress !== void 0 && selection.subagentAddress !== null && typeof selection.subagentAddress === "object") return selection.subagentAddress;
567
+ if (typeof selection.sessionId === "string" && selection.sessionId !== "") return selection.sessionId;
568
+ return null;
569
+ }
570
+ /**
571
+ * The most-recent workspace per upstream `recentWorkspace`
572
+ * (navigation.ts:434-453): the latest member-session `updatedAt` wins, a
573
+ * workspace with no listed sessions falls back to its `createdAt`.
574
+ */
575
+ function recentWorkspaceId(items, byId) {
576
+ let selected;
577
+ let selectedTime = Number.NEGATIVE_INFINITY;
578
+ for (const workspace of items) {
579
+ let latest = Number.NEGATIVE_INFINITY;
580
+ for (const sessionId of workspace.sessionIds) {
581
+ const session = byId[sessionId];
582
+ if (session !== void 0 && typeof session.updatedAt === "number") latest = Math.max(latest, session.updatedAt);
583
+ }
584
+ if (latest === Number.NEGATIVE_INFINITY && workspace.createdAt !== void 0) latest = Date.parse(workspace.createdAt);
585
+ if (selected === void 0 || latest > selectedTime) {
586
+ selected = workspace.workspaceId;
587
+ selectedTime = latest;
588
+ }
589
+ }
590
+ return selected;
591
+ }
592
+ /**
593
+ * The landing decision with NATIVE restore semantics (T-F): replicate
594
+ * upstream `restoreSelection` (navigation.ts:318-347) against the fresh
595
+ * caches and the parsed persisted selection ({@link parseLandingTarget}'s
596
+ * result, which already applies the `subagentAddress`-first precedence).
597
+ *
598
+ * - `subagentAddress` → open it directly, no cache consultation
599
+ * (navigation.ts:320-323).
600
+ * - `sessionId` found in the FRESH `byId`: non-blank — or blank but
601
+ * workspace-less — opens the id (navigation.ts:324-329); blank with a
602
+ * workspace resolves to that workspace (navigation.ts:333-335's private
603
+ * `reuseBlank` + :337's `workspace.workspaceId` target — the public
604
+ * `startSession(workspaceId)` face covers both: its `connectWorkspace`
605
+ * → `reuseOrCreateBlank` reuses the pointer's own blank whenever the
606
+ * private pre-check would pass, because the eligibility scan is the
607
+ * same).
608
+ * - A stale id (absent from the fresh `byId`) falls through to the
609
+ * no-pointer ladder exactly as upstream's `summary === undefined` does.
610
+ * - No pointer / stale pointer: the recent workspace gets the new-session
611
+ * flow (navigation.ts:337); both lists empty is the fresh boot and
612
+ * mirrors `initializeDefaultWorkspace` (navigation.ts:338-342).
613
+ * - `caches` unavailable (the host's read failed) → `undefined`: land
614
+ * nothing — no landing beats a wrong one. Same for the degenerate
615
+ * zero-workspaces-with-sessions shape, where upstream simply leaves the
616
+ * view as it is (no target, both-empty guard false, :337-342).
617
+ *
618
+ * @param parsed - the persisted-selection parse ({@link parseLandingTarget}).
619
+ * @param caches - the fresh both-lists snapshots, or `undefined` unreadable.
620
+ * @returns the action to execute, or `undefined` to land nothing.
621
+ */
622
+ function planRestoreLanding(parsed, caches) {
623
+ if (caches === void 0) return void 0;
624
+ if (parsed !== null && typeof parsed === "object") return {
625
+ kind: "open-session",
626
+ target: parsed
627
+ };
628
+ if (typeof parsed === "string" && parsed !== "") {
629
+ const summary = caches.sessions.byId[parsed];
630
+ if (summary !== void 0) {
631
+ const workspace = caches.workspaces.items.find((item) => item.sessionIds.includes(parsed));
632
+ if (summary.blank !== true || workspace === void 0) return {
633
+ kind: "open-session",
634
+ target: parsed
635
+ };
636
+ return {
637
+ kind: "start-session",
638
+ workspaceId: workspace.workspaceId
639
+ };
640
+ }
641
+ }
642
+ const target = recentWorkspaceId(caches.workspaces.items, caches.sessions.byId);
643
+ if (target !== void 0) return {
644
+ kind: "start-session",
645
+ workspaceId: target
646
+ };
647
+ if (caches.workspaces.items.length === 0 && caches.sessions.ids.length === 0) return { kind: "default-workspace" };
648
+ }
649
+ /**
650
+ * The caches-fresh gate behind the deferred landing (T-F; T-G made it
651
+ * pointer-membership-aware): fires `listener` AT MOST once, when BOTH
652
+ * navigation caches demonstrably hold the TARGET plane's data — the
653
+ * client-face equivalent of upstream's both-lists ready gate
654
+ * (`watchNavigation`, navigation.ts:292: `workspace.phase !== 'ready'
655
+ * || sessions.phase !== 'ready'` → return).
656
+ *
657
+ * The freshness token is each snapshot's `items` ARRAY IDENTITY: upstream's
658
+ * workspace model mints a new array on every baseline install
659
+ * (`installViews` → `[...installed.values()]`, model.ts:398) and the session
660
+ * list projection mints a fresh `ids` array on every manager change
661
+ * (service.ts:607-677), while carrier churn keeps references. The gate arms
662
+ * at the first generation establishment OBSERVED BY ITS OWN subscription —
663
+ * the anchors are retaken there, and nothing before that point can pass, so
664
+ * the dying old plane's last increments racing the reconnect are absorbed;
665
+ * the first replacements after establishment are the target plane's
666
+ * baselines. The `phase === 'ready'` checks ride along as a guard against a
667
+ * page whose first baseline never arrived. Both edges are tracked
668
+ * independently, in either arrival order (the T-A gap: watching workspaces
669
+ * alone let a late sessions baseline leave the pointer lookup reading stale
670
+ * `byId` rows).
671
+ *
672
+ * T-G — the pointer holds the gate. The sessions list is minted
673
+ * INCREMENTALLY after the reconnect (one `handleSessionAdded` per session,
674
+ * each re-projecting the store with a fresh `ids` array), so the first fresh
675
+ * edge sees a partial `byId` that legitimately lacks the pointer's session;
676
+ * consuming the landing there laddered every switch into a brand-new
677
+ * session (the browser-proven defect). When a pointer exists (the injected
678
+ * {@link LandingGateOptions.pointerSessionId} face, read at decision time),
679
+ * the gate therefore does NOT fire on the first fresh edge. It fires when:
680
+ * - the sessions list CONTAINS the pointer's session — the membership
681
+ * face (`byId`, consulted on EVERY sessions notification: `byId` can
682
+ * gain a row without a new `ids` identity, service.ts:565), or
683
+ * - the sessions list has gone QUIET — no `ids` identity change for the
684
+ * configured quiet window ({@link LANDING_QUIET_MS}; an identity change
685
+ * during the window restarts it) while the pointer is still absent:
686
+ * the honest miss, firing with the ladder exactly like upstream's boot
687
+ * restore of a stored id its fresh list does not know.
688
+ * No pointer → the first fresh edge fires, byte-identical to T-F (the
689
+ * fresh-boot path). The machine's post-settle staleness watchdog bounds the
690
+ * whole membership wait: it disposes this gate, which also cancels a
691
+ * running quiet window.
692
+ *
693
+ * Establishment strictly precedes any target-plane baseline (the follow
694
+ * streams re-open on top of the established generation), so a fire implies
695
+ * both "generation established" and "repull landed" — the two preconditions
696
+ * of a correct landing resolution.
697
+ *
698
+ * Host-half pure logic: the client binding composes it with the real
699
+ * `ctx.workspaces.list` + `ctx.sessions.list` stores and the connection
700
+ * generation observable; the machine consumes it through
701
+ * {@link SwitchHost.onCachesRefreshed}.
702
+ *
703
+ * @returns the disposer stopping all three subscriptions (and the quiet window).
704
+ */
705
+ function observeCachesRefreshed(workspaces, sessions, generation, listener, gate = {}) {
706
+ let established = false;
707
+ let fired = false;
708
+ let workspacesAnchor = workspaces.getSnapshot().items;
709
+ let sessionsAnchor = sessions.getSnapshot().items;
710
+ let workspacesFresh = false;
711
+ let sessionsFresh = false;
712
+ let quietTimer;
713
+ const quietMs = gate.quietMs ?? LANDING_QUIET_MS;
714
+ const clearQuiet = () => {
715
+ if (quietTimer === void 0) return;
716
+ if (gate.clearTimeout !== void 0) gate.clearTimeout(quietTimer);
717
+ quietTimer = void 0;
718
+ };
719
+ const fire = () => {
720
+ if (fired) return;
721
+ fired = true;
722
+ clearQuiet();
723
+ listener();
724
+ };
725
+ const pointerSessionId = () => {
726
+ try {
727
+ return gate.pointerSessionId?.();
728
+ } catch {
729
+ return;
730
+ }
731
+ };
732
+ /**
733
+ * The decision point, run on every workspaces/sessions notification once
734
+ * establishment happened. `sessionsIdentityChanged` distinguishes a real
735
+ * list growth (restarts the quiet window) from a same-identity
736
+ * notification like publishRetention's byId-only set (consults
737
+ * membership, must NOT restart the window).
738
+ */
739
+ const check = (sessionsIdentityChanged) => {
740
+ if (fired || !established || !workspacesFresh || !sessionsFresh) return;
741
+ const pointer = pointerSessionId();
742
+ if (pointer === void 0) {
743
+ fire();
744
+ return;
745
+ }
746
+ if (sessions.contains?.(pointer)) {
747
+ fire();
748
+ return;
749
+ }
750
+ if (sessionsIdentityChanged || quietTimer === void 0) {
751
+ clearQuiet();
752
+ if (gate.setTimeout !== void 0) quietTimer = gate.setTimeout(() => {
753
+ quietTimer = void 0;
754
+ fire();
755
+ }, quietMs);
756
+ }
757
+ };
758
+ const disposeGeneration = generation.subscribe(() => {
759
+ if (generation.getSnapshot() === void 0) return;
760
+ established = true;
761
+ workspacesFresh = false;
762
+ sessionsFresh = false;
763
+ clearQuiet();
764
+ workspacesAnchor = workspaces.getSnapshot().items;
765
+ sessionsAnchor = sessions.getSnapshot().items;
766
+ });
767
+ const disposeWorkspaces = workspaces.subscribe(() => {
768
+ if (!established) return;
769
+ const snapshot = workspaces.getSnapshot();
770
+ if (snapshot.phase === "ready" && snapshot.items !== workspacesAnchor) workspacesFresh = true;
771
+ check(false);
772
+ });
773
+ const disposeSessions = sessions.subscribe(() => {
774
+ if (!established) return;
775
+ const snapshot = sessions.getSnapshot();
776
+ let identityChanged = false;
777
+ if (snapshot.phase === "ready" && snapshot.items !== sessionsAnchor) {
778
+ sessionsFresh = true;
779
+ identityChanged = true;
780
+ sessionsAnchor = snapshot.items;
781
+ }
782
+ check(identityChanged);
783
+ });
784
+ return () => {
785
+ clearQuiet();
786
+ disposeWorkspaces();
787
+ disposeSessions();
788
+ disposeGeneration();
789
+ };
790
+ }
791
+ /**
792
+ * The agent-selector click-interception contract as a pure table (native UI
793
+ * fusion; originally landed for the sidebar-bottom RuntimeSeat, which T-B
794
+ * retired — the Hub UI surfaces (HubPage rows + the HubPanelIcon chevron
795
+ * menu) are now the sole switch surface and consume this SAME table, so the
796
+ * contract below is inherited verbatim).
797
+ *
798
+ * - World page (`fusion: false`): no interception, no orchestration — the
799
+ * real hyperlink navigates (spec §5 双半边).
800
+ * - Fusion page, active entry: intercepted, no attempt — switching to the
801
+ * active agent is a no-op, and the click must never fall through to the
802
+ * anchor (an `href="/"` reload would discard the in-page switch state; a
803
+ * foreign `href` would abandon the fused page entirely).
804
+ * - Fusion page, inactive entry: intercepted and the attempt runs. The
805
+ * attempt's `false` (orchestration unbound) still keeps the page: a
806
+ * swallowed click beats silent navigation away from the fusion surface.
807
+ *
808
+ * Host-half pure logic so `test/client-switch.test.mjs` pins the table from
809
+ * `dist/`; the Hub surfaces map `preventDefault` onto the DOM event.
810
+ */
811
+ function seatClickDecision(input) {
812
+ if (!input.fusion) return {
813
+ preventDefault: false,
814
+ attempted: false
815
+ };
816
+ if (input.active) return {
817
+ preventDefault: true,
818
+ attempted: false
819
+ };
820
+ return {
821
+ preventDefault: true,
822
+ attempted: input.attempt() === true
823
+ };
824
+ }
825
+ /**
826
+ * Which Hub UI roster row shows as the active agent (T-B: the Hub surfaces —
827
+ * HubPage rows and the HubPanelIcon chevron menu — are the sole switch
828
+ * surface after the RuntimeSeat retirement).
829
+ *
830
+ * - Fusion page: the page atom's `activeAgent` — the switch state — decides
831
+ * by roster key; the URL is irrelevant on a fused page. The `native`
832
+ * entry participates like any other key.
833
+ * - Non-fusion page (flag off; the UI's navigation arm is retired since
834
+ * T-C): the mount path is the selection — the entry whose path equals
835
+ * the page's normalized mount. The pure arm survives as contract; no
836
+ * served page can hit the foreign case anymore.
837
+ *
838
+ * Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
839
+ */
840
+ function hubRowActive(input) {
841
+ if (input.fusion) return input.agentKey === input.activeAgent;
842
+ return input.agentPath === input.mountPath;
843
+ }
844
+ /** The degradation hint a Hub UI surface shows while the flag is off (T-E). */
845
+ const FUSION_OFF_HINT = "fusion off — no foreign entry";
846
+ /**
847
+ * The flag-off shape of the Hub UI switch surfaces (path-separation Part 2
848
+ * T-E closeout). With the per-label URL family retired (T-C) and the world
849
+ * web UIs disabled (T-D), a flag-OFF ctx0 page has NO foreign entry at all —
850
+ * and since T-C the Hub UI ships no hyperlinks either, so the smallest honest
851
+ * rendering is an inert roster with a visible degradation hint instead of a
852
+ * silently dead control:
853
+ *
854
+ * - `available: true` (fusion page): the affordance renders — the HubPage
855
+ * `switch` action and the HubPanelIcon chevron menu.
856
+ * - `available: false` (flag off): the affordance is NOT rendered; the
857
+ * roster stays informational and the surface shows the degradation hint
858
+ * ({@link FUSION_OFF_HINT}) once (HubPage: a single chip under the
859
+ * section head, not one per row). Nothing navigates — there is
860
+ * nothing to navigate to.
861
+ *
862
+ * Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
863
+ */
864
+ function switchAffordance(fusion) {
865
+ return fusion ? {
866
+ available: true,
867
+ hint: ""
868
+ } : {
869
+ available: false,
870
+ hint: FUSION_OFF_HINT
871
+ };
872
+ }
873
+ /**
874
+ * The active agent's display name for the Hub row's right-edge selector: the
875
+ * roster label of the atom's `activeAgent` key — the name the selector's
876
+ * `title` carries, since the row label itself is the constant `Hub`. A key the
877
+ * (seeded) roster does not know degrades to the raw key. `undefined`/empty
878
+ * (flag off, or no atom read) means "no active agent".
879
+ *
880
+ * Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
881
+ */
882
+ function activeAgentName(activeAgent, roster) {
883
+ if (activeAgent === void 0 || activeAgent === "") return void 0;
884
+ return roster.find((row) => row.key === activeAgent)?.label ?? activeAgent;
885
+ }
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
+ /**
918
+ * Build the switch machine over an injected {@link SwitchHost}.
919
+ *
920
+ * Only one attempt is ever armed at a time: starting a new switch finishes
921
+ * the previous one silently (timer cleared, subscription released, promise
922
+ * resolved) and cancels its pending landing, so a stale attempt can never
923
+ * revert or land over a newer switch. The landing cancel is held at
924
+ * CONTROLLER scope, not reached through the current-attempt pointer: the
925
+ * deferred landing deliberately survives the settle — which removes the
926
+ * attempt from `current` — so a supersede must still kill the armed landing
927
+ * of a PRIOR, ALREADY-SETTLED attempt (final-review round 2: without the
928
+ * controller-scope handle, a settled switch's landing fired over the newer
929
+ * switch's pointer-restored view).
930
+ */
931
+ function createSwitchController(host, options = {}) {
932
+ const fallbackMs = options.fallbackMs ?? SWITCH_FALLBACK_MS;
933
+ const quietWindowMs = options.quietWindowMs ?? LANDING_QUIET_MS;
934
+ let current;
935
+ let armedLanding;
936
+ return { run(label) {
937
+ const prior = current;
938
+ return new Promise((resolve) => {
939
+ let timer;
940
+ let watchdog;
941
+ let disposer = () => {};
942
+ let disposeLanding = () => {};
943
+ let landingArmed = false;
944
+ const cancelWatchdog = () => {
945
+ if (watchdog === void 0) return;
946
+ host.clearTimeout(watchdog);
947
+ watchdog = void 0;
948
+ };
949
+ const attempt = {
950
+ finished: false,
951
+ dead: false,
952
+ cancelLanding() {
953
+ attempt.dead = true;
954
+ landingArmed = false;
955
+ disposeLanding();
956
+ disposeLanding = () => {};
957
+ cancelWatchdog();
958
+ },
959
+ finish() {
960
+ if (attempt.finished) return;
961
+ attempt.finished = true;
962
+ host.clearTimeout(timer);
963
+ disposer();
964
+ if (current === attempt) current = void 0;
965
+ if (landingArmed) watchdog = host.setTimeout(() => {
966
+ attempt.cancelLanding();
967
+ }, fallbackMs);
968
+ resolve();
969
+ }
970
+ };
971
+ if (armedLanding !== void 0) {
972
+ const cancelPriorLanding = armedLanding;
973
+ armedLanding = void 0;
974
+ cancelPriorLanding();
975
+ }
976
+ if (prior !== void 0 && !prior.finished) prior.finish();
977
+ current = attempt;
978
+ const revert = (reason) => {
979
+ if (attempt.finished || current !== attempt) return;
980
+ attempt.cancelLanding();
981
+ try {
982
+ host.setAgent(NATIVE_AGENT);
983
+ } catch {}
984
+ try {
985
+ host.reconnect();
986
+ } catch {}
987
+ try {
988
+ host.toast(`Super D: switch to ${label} failed (${reason === "timeout" ? "no response" : "target not reachable"}) — back to native.`);
989
+ } catch {}
990
+ attempt.finish();
991
+ };
992
+ try {
993
+ host.setAgent(label);
994
+ } catch {}
995
+ try {
996
+ host.reconnect();
997
+ } catch {}
998
+ timer = host.setTimeout(() => {
999
+ revert("timeout");
1000
+ }, fallbackMs);
1001
+ disposer = host.onGenerationEstablished(() => {
1002
+ if (attempt.finished || current !== attempt) return;
1003
+ attempt.finish();
1004
+ });
1005
+ landingArmed = true;
1006
+ const cancelThisLanding = () => {
1007
+ attempt.cancelLanding();
1008
+ };
1009
+ armedLanding = cancelThisLanding;
1010
+ const gateRequest = {
1011
+ pointerSessionId: () => {
1012
+ let parsed;
1013
+ try {
1014
+ parsed = parseLandingTarget(host.readPersistedSelection());
1015
+ } catch {
1016
+ return;
1017
+ }
1018
+ return typeof parsed === "string" && parsed !== "" ? parsed : void 0;
1019
+ },
1020
+ quietMs: quietWindowMs,
1021
+ setTimeout: (fn, ms) => host.setTimeout(fn, ms),
1022
+ clearTimeout: (handle) => host.clearTimeout(handle)
1023
+ };
1024
+ disposeLanding = host.onCachesRefreshed(() => {
1025
+ if (attempt.dead) return;
1026
+ if (armedLanding === cancelThisLanding) armedLanding = void 0;
1027
+ landingArmed = false;
1028
+ cancelWatchdog();
1029
+ disposeLanding();
1030
+ disposeLanding = () => {};
1031
+ let caches;
1032
+ try {
1033
+ caches = host.readRestoreCaches();
1034
+ } catch {
1035
+ caches = void 0;
84
1036
  }
85
- if (listed.length > 0) {
86
- setAgents(listed);
87
- return;
1037
+ let raw = null;
1038
+ try {
1039
+ raw = host.readPersistedSelection();
1040
+ } catch {
1041
+ raw = null;
88
1042
  }
1043
+ const plan = planRestoreLanding(parseLandingTarget(raw), caches);
1044
+ if (plan === void 0) return;
1045
+ try {
1046
+ if (plan.kind === "open-session") host.openSession(plan.target);
1047
+ else if (plan.kind === "start-session") host.startSession(plan.workspaceId);
1048
+ else host.openDefaultWorkspace();
1049
+ } catch {}
1050
+ }, gateRequest);
1051
+ host.probeTarget(label).then((alive) => {
1052
+ if (alive !== true) revert("probe");
1053
+ }, () => {
1054
+ revert("probe");
1055
+ });
1056
+ });
1057
+ } };
1058
+ }
1059
+
1060
+ //#endregion
1061
+ //#region src/client/switch.ts
1062
+ /** Whether this page is a fusion page: the baked flag global is present. */
1063
+ function fusionPage() {
1064
+ return globalThis.__SUPERD_UI_FUSION__ === true;
1065
+ }
1066
+ /** The page atom, when the page carries one (it always does on a fusion page). */
1067
+ function uiState() {
1068
+ const state = globalThis.__SUPERD_UI_STATE__;
1069
+ if (state === void 0) return void 0;
1070
+ if (typeof state.setAgent !== "function") return void 0;
1071
+ if (typeof state.subscribe !== "function" || typeof state.activeAgent !== "string") return void 0;
1072
+ return state;
1073
+ }
1074
+ /** How long one revert toast stays on screen. */
1075
+ const TOAST_HOLD_MS = 6e3;
1076
+ /**
1077
+ * One transient message, fixed above the sidebar footer. Self-owned
1078
+ * presentation only — no read from any foreign DOM, nothing is a state input.
1079
+ */
1080
+ function showToast(message) {
1081
+ try {
1082
+ const doc = globalThis.document;
1083
+ if (doc === void 0 || doc.body === null) return;
1084
+ const el = doc.createElement("div");
1085
+ el.textContent = message;
1086
+ el.setAttribute("style", [
1087
+ "position:fixed",
1088
+ "left:50%",
1089
+ "bottom:64px",
1090
+ "transform:translateX(-50%)",
1091
+ "z-index:9999",
1092
+ "max-width:80vw",
1093
+ "padding:10px 14px",
1094
+ "border-radius:8px",
1095
+ "background:rgba(24,24,28,.94)",
1096
+ "color:#ececf0",
1097
+ "font:13px/1.4 system-ui,sans-serif",
1098
+ "box-shadow:0 8px 24px rgba(0,0,0,.35)"
1099
+ ].join(";"));
1100
+ doc.body.appendChild(el);
1101
+ globalThis.setTimeout(() => {
1102
+ try {
1103
+ el.remove();
1104
+ } catch {}
1105
+ }, TOAST_HOLD_MS);
1106
+ } catch {}
1107
+ }
1108
+ /** The bound `switchAgent`, or `undefined` until the entry binds it. */
1109
+ let bound;
1110
+ /**
1111
+ * Bind the page's switch agent from the client services. Idempotent; a no-op
1112
+ * without the page atom (a non-fusion page never has one).
1113
+ * @param deps - the `connection`, `uiWorkspace`, `workspaces`, `sessions`
1114
+ * and `locale` services (structural; the stock client tree always has all
1115
+ * five — upstream's ui-workspace injects the same set).
1116
+ */
1117
+ function bindSwitchAgent(deps) {
1118
+ if (bound !== void 0) return;
1119
+ const state = uiState();
1120
+ if (state === void 0) return;
1121
+ const readRestoreCaches = () => {
1122
+ const workspaces = deps.workspaces.list.getSnapshot();
1123
+ const sessions = deps.sessions.list.getSnapshot();
1124
+ return {
1125
+ workspaces: {
1126
+ items: workspaces.items,
1127
+ phase: workspaces.phase
1128
+ },
1129
+ sessions: {
1130
+ ids: sessions.ids,
1131
+ byId: sessions.byId,
1132
+ phase: sessions.phase
89
1133
  }
90
- if (!Array.isArray(j?.available)) return;
91
- const links = [];
92
- for (const key of j.available) {
93
- if (typeof key !== "string") continue;
94
- links.push(key === "native" ? NATIVE : {
95
- key,
96
- label: RUNTIME_LABELS[key] ?? key,
97
- path: `/${key}/`
98
- });
1134
+ };
1135
+ };
1136
+ const openDefaultWorkspace = () => {
1137
+ const language = deps.locale.getSnapshot().active.toLowerCase().split("-")[0];
1138
+ const title = language === "zh" ? "默认工作区" : "Default workspace";
1139
+ const directoryName = language === "zh" || language === "en" ? title : "default-workspace";
1140
+ deps.workspaces.initializeDefault({
1141
+ directoryName,
1142
+ title
1143
+ }).then((prepared) => {
1144
+ if (prepared !== void 0) deps.uiWorkspace.openWorkspace(prepared.workspaceId);
1145
+ }).catch(() => {
1146
+ showToast("Super D: unable to create the default workspace.");
1147
+ });
1148
+ };
1149
+ const controller = createSwitchController({
1150
+ setAgent(label) {
1151
+ state.setAgent(label);
1152
+ },
1153
+ readPersistedSelection() {
1154
+ const storage = globalThis.localStorage;
1155
+ return storage === void 0 ? null : storage.getItem(SESSIONS_CURRENT_KEY);
1156
+ },
1157
+ openSession(target) {
1158
+ deps.uiWorkspace.openSession(target);
1159
+ },
1160
+ startSession(workspaceId) {
1161
+ deps.uiWorkspace.startSession(workspaceId);
1162
+ },
1163
+ openDefaultWorkspace,
1164
+ readRestoreCaches,
1165
+ onCachesRefreshed(listener, gate) {
1166
+ return observeCachesRefreshed(deps.workspaces.list, {
1167
+ getSnapshot: () => {
1168
+ const snapshot = deps.sessions.list.getSnapshot();
1169
+ return {
1170
+ items: snapshot.ids,
1171
+ phase: snapshot.phase
1172
+ };
1173
+ },
1174
+ subscribe: (fn) => deps.sessions.list.subscribe(fn),
1175
+ contains: (id) => deps.sessions.list.getSnapshot().byId[id] !== void 0
1176
+ }, deps.connection.generation, listener, gate);
1177
+ },
1178
+ reconnect() {
1179
+ deps.connection.reconnect();
1180
+ },
1181
+ onGenerationEstablished(listener) {
1182
+ const generation = deps.connection.generation;
1183
+ return generation.subscribe(() => {
1184
+ if (generation.getSnapshot() !== void 0) listener();
1185
+ });
1186
+ },
1187
+ async probeTarget(label) {
1188
+ try {
1189
+ return (await fetchRoster()).some((agent) => agent.key === label);
1190
+ } catch {
1191
+ return false;
99
1192
  }
100
- if (links.length > 0) setAgents(links);
101
- }).catch(() => {});
1193
+ },
1194
+ toast: showToast,
1195
+ setTimeout(fn, ms) {
1196
+ return globalThis.setTimeout(fn, ms);
1197
+ },
1198
+ clearTimeout(handle) {
1199
+ globalThis.clearTimeout(handle);
1200
+ }
1201
+ });
1202
+ bound = (label) => controller.run(label);
1203
+ }
1204
+ /**
1205
+ * The Hub switch surfaces' entry point. Attempts the in-page switch for
1206
+ * `label`.
1207
+ * @returns `true` when the fusion switch took the click over; `false` when
1208
+ * this page is not a fusion page or the orchestration is unbound.
1209
+ * Informational only — the interception contract (`seatClickDecision`)
1210
+ * prevents navigation on every fusion-page left click regardless: a
1211
+ * `false` means the click is swallowed, never that the anchor navigates
1212
+ * the fused page away (review round 1).
1213
+ */
1214
+ function attemptSwitch(label) {
1215
+ const run = bound;
1216
+ if (run === void 0 || !fusionPage()) return false;
1217
+ run(label).catch(() => {});
1218
+ return true;
1219
+ }
1220
+
1221
+ //#endregion
1222
+ //#region src/client/HubPage.tsx
1223
+ const EMPTY_DRAFT = {
1224
+ name: "",
1225
+ url: ""
1226
+ };
1227
+ /** Chip text: drop the trailing slash for concision ('/claude/' → '/claude'). */
1228
+ function displayPath(path) {
1229
+ return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
1230
+ }
1231
+ /** Shared chip look (status pills, path tags). */
1232
+ const chip = {
1233
+ fontSize: 10,
1234
+ lineHeight: 1.4,
1235
+ padding: "2px 7px",
1236
+ borderRadius: 999,
1237
+ whiteSpace: "nowrap",
1238
+ background: "color-mix(in srgb, currentColor 10%, transparent)"
1239
+ };
1240
+ const chipSelf = {
1241
+ ...chip,
1242
+ background: "color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent)",
1243
+ color: "var(--dsw-alias-state-success-primary)"
1244
+ };
1245
+ const chipPending = {
1246
+ ...chip,
1247
+ color: "var(--dsw-alias-label-caption)"
1248
+ };
1249
+ const inputStyle = {
1250
+ boxSizing: "border-box",
1251
+ width: "100%",
1252
+ minWidth: 0,
1253
+ padding: "7px 10px",
1254
+ borderRadius: 8,
1255
+ border: "1px solid var(--dsw-alias-border-l3)",
1256
+ background: "transparent",
1257
+ color: "inherit",
1258
+ font: "inherit",
1259
+ fontSize: 13
1260
+ };
1261
+ const fieldLabel = {
1262
+ display: "block",
1263
+ marginBottom: 4,
1264
+ fontSize: 11,
1265
+ color: "var(--dsw-alias-label-caption)"
1266
+ };
1267
+ const listStyle = {
1268
+ margin: 0,
1269
+ padding: 0,
1270
+ listStyle: "none",
1271
+ display: "flex",
1272
+ flexDirection: "column",
1273
+ gap: 2
1274
+ };
1275
+ /** A row that lights its surface on hover (inline styles have no :hover). */
1276
+ function HoverRow({ children, onClick, clickable }) {
1277
+ const [hover, setHover] = react.useState(false);
1278
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
1279
+ onClick,
1280
+ onMouseEnter: () => {
1281
+ setHover(true);
1282
+ },
1283
+ onMouseLeave: () => {
1284
+ setHover(false);
1285
+ },
1286
+ style: {
1287
+ display: "flex",
1288
+ alignItems: "center",
1289
+ gap: 12,
1290
+ margin: "0 -8px",
1291
+ padding: "9px 8px",
1292
+ borderRadius: 12,
1293
+ listStyle: "none",
1294
+ background: hover ? "var(--dsw-alias-interactive-bg-hover)" : "transparent",
1295
+ cursor: clickable ? "pointer" : void 0
1296
+ },
1297
+ children
1298
+ });
1299
+ }
1300
+ /** Section head: title, count, and a trailing action slot. */
1301
+ function GroupHead({ title, count, action }) {
1302
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1303
+ style: {
1304
+ display: "flex",
1305
+ alignItems: "baseline",
1306
+ gap: 8
1307
+ },
1308
+ children: [
1309
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1310
+ style: {
1311
+ margin: 0,
1312
+ fontSize: 14,
1313
+ lineHeight: "22px",
1314
+ fontWeight: 500
1315
+ },
1316
+ children: title
1317
+ }),
1318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1319
+ style: {
1320
+ fontSize: 13,
1321
+ color: "var(--dsw-alias-label-caption)",
1322
+ fontVariantNumeric: "tabular-nums"
1323
+ },
1324
+ children: count
1325
+ }),
1326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
1327
+ action
1328
+ ]
1329
+ });
1330
+ }
1331
+ function HubPage() {
1332
+ const [machines, setMachines] = react.useState(() => loadMachines());
1333
+ const [showAdd, setShowAdd] = react.useState(false);
1334
+ const [draft, setDraft] = react.useState(EMPTY_DRAFT);
1335
+ const [agents, setAgents] = react.useState(() => rosterFromGlobal() ?? null);
1336
+ const [rosterFailed, setRosterFailed] = react.useState(false);
1337
+ const [tab, setTab] = react.useState("home");
1338
+ const fusion = fusionPage();
1339
+ const switchUi = switchAffordance(fusion);
1340
+ const [activeKey, setActiveKey] = react.useState(() => fusion ? uiState()?.activeAgent : void 0);
1341
+ react.useEffect(() => {
1342
+ let cancelled = false;
1343
+ fetchRoster().then((list) => {
1344
+ if (!cancelled) {
1345
+ setAgents(list);
1346
+ setRosterFailed(false);
1347
+ }
1348
+ }).catch(() => {
1349
+ if (!cancelled) {
1350
+ setAgents((prev) => prev ?? [NATIVE_LINK]);
1351
+ setRosterFailed(true);
1352
+ }
1353
+ });
102
1354
  return () => {
103
1355
  cancelled = true;
104
1356
  };
105
1357
  }, []);
106
1358
  react.useEffect(() => {
107
- if (!open) return;
108
- const onDown = (event) => {
109
- if (rootRef.current !== null && !rootRef.current.contains(event.target)) setOpen(false);
110
- };
111
- document.addEventListener("mousedown", onDown);
112
- return () => document.removeEventListener("mousedown", onDown);
113
- }, [open]);
114
- const here = currentPath();
115
- const active = agents.find((agent) => agent.path === here) ?? NATIVE;
116
- const styleBase = {
117
- display: "flex",
118
- alignItems: "center",
119
- gap: 8,
120
- width: "100%",
121
- padding: wide ? "7px 10px" : "7px 0",
122
- justifyContent: wide ? "flex-start" : "center",
123
- border: "none",
124
- borderRadius: 8,
125
- background: "transparent",
126
- color: "inherit",
127
- font: "inherit",
128
- fontSize: 13,
129
- cursor: "pointer",
130
- position: "relative"
131
- };
132
- const styleBadge = {
133
- fontSize: 10,
134
- lineHeight: 1.4,
135
- padding: "1px 6px",
136
- borderRadius: 999,
137
- background: "color-mix(in srgb, currentColor 12%, transparent)",
138
- whiteSpace: "nowrap"
139
- };
140
- const styleMenu = {
141
- position: "absolute",
142
- bottom: "calc(100% + 6px)",
143
- left: 6,
144
- right: 6,
145
- zIndex: 40,
146
- background: "var(--dsh-surface, #26262b)",
147
- border: "1px solid color-mix(in srgb, currentColor 18%, transparent)",
148
- borderRadius: 10,
149
- boxShadow: "0 8px 24px rgba(0,0,0,.35)",
150
- overflow: "hidden"
151
- };
152
- const styleItem = (isActive) => ({
153
- display: "block",
154
- width: "100%",
155
- padding: "8px 12px",
156
- border: "none",
157
- background: isActive ? "color-mix(in srgb, currentColor 10%, transparent)" : "transparent",
158
- color: "inherit",
159
- font: "inherit",
160
- fontSize: 13,
161
- textAlign: "left",
162
- cursor: "pointer"
1359
+ if (!fusion) return;
1360
+ const state = uiState();
1361
+ if (state === void 0) return;
1362
+ return state.subscribe((label) => {
1363
+ setActiveKey(label);
1364
+ });
1365
+ }, [fusion]);
1366
+ const here = currentMountPath();
1367
+ const isActive = (agent) => hubRowActive({
1368
+ fusion,
1369
+ agentKey: agent.key,
1370
+ agentPath: agent.path,
1371
+ activeAgent: activeKey,
1372
+ mountPath: here
163
1373
  });
164
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
165
- ref: rootRef,
1374
+ /**
1375
+ * The row's click decision — the SAME table the retired sidebar seat ran:
1376
+ * on a fusion page EVERY left click is intercepted (active entry included),
1377
+ * the attempt runs for non-active entries only, and `preventDefault` keeps
1378
+ * the page exactly where it is. Non-fusion pages have no navigation arm to
1379
+ * fall back to (T-C retired the URL family), so the pass-through decision
1380
+ * is simply inert.
1381
+ */
1382
+ const rowDecision = (agent, event) => {
1383
+ if (seatClickDecision({
1384
+ fusion,
1385
+ active: isActive(agent),
1386
+ attempt: () => attemptSwitch(agent.key)
1387
+ }).preventDefault) event.preventDefault();
1388
+ };
1389
+ const persist = (next) => {
1390
+ const list = [...next];
1391
+ setMachines(list);
1392
+ saveMachines(list);
1393
+ };
1394
+ const addMachine = () => {
1395
+ const name = draft.name.trim();
1396
+ const url = draft.url.trim();
1397
+ if (name === "" || url === "") return;
1398
+ persist([...machines, {
1399
+ id: freshMachineId(),
1400
+ name,
1401
+ label: name,
1402
+ url,
1403
+ self: false,
1404
+ addedAt: Date.now()
1405
+ }]);
1406
+ setDraft(EMPTY_DRAFT);
1407
+ setShowAdd(false);
1408
+ };
1409
+ const removeMachine = (id) => {
1410
+ persist(machines.filter((machine) => machine.id !== id));
1411
+ };
1412
+ const draftValid = draft.name.trim() !== "" && draft.url.trim() !== "";
1413
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
166
1414
  style: {
167
- position: "relative",
168
- width: "100%"
169
- },
170
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
171
- type: "button",
172
- style: styleBase,
173
- title: "Agent runtime — picking one opens that runtime's mount (/omp/, /codex/, …)",
174
- onClick: () => {
175
- setOpen((v) => !v);
1415
+ display: "flex",
1416
+ flexDirection: "column",
1417
+ alignItems: "center",
1418
+ height: "100%",
1419
+ overflowY: "auto",
1420
+ boxSizing: "border-box",
1421
+ padding: "28px clamp(24px, 4vw, 48px) 48px",
1422
+ color: "var(--dsw-alias-label-primary)"
1423
+ },
1424
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1425
+ style: {
1426
+ width: "100%",
1427
+ maxWidth: 960,
1428
+ display: "flex",
1429
+ flexDirection: "column",
1430
+ gap: 20
176
1431
  },
177
- children: wide ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
178
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
179
- "aria-hidden": true,
180
- children: "⌘"
1432
+ children: [
1433
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1434
+ style: {
1435
+ display: "flex",
1436
+ alignItems: "center",
1437
+ gap: 12
1438
+ },
1439
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(HubGlyph, { size: 36 }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1440
+ style: { minWidth: 0 },
1441
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
1442
+ style: {
1443
+ margin: 0,
1444
+ fontSize: 20,
1445
+ fontWeight: 500,
1446
+ lineHeight: "28px"
1447
+ },
1448
+ children: "Hub"
1449
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1450
+ style: {
1451
+ margin: "2px 0 0",
1452
+ fontSize: 13,
1453
+ lineHeight: "20px",
1454
+ color: "var(--dsw-alias-label-secondary)"
1455
+ },
1456
+ children: "Multi-agent across machines in one hub by SuperD"
1457
+ })]
1458
+ })]
181
1459
  }),
182
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1460
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
183
1461
  style: {
184
- flex: 1,
185
- textAlign: "left",
186
- whiteSpace: "nowrap",
187
- overflow: "hidden",
188
- textOverflow: "ellipsis"
1462
+ display: "flex",
1463
+ gap: 4,
1464
+ borderBottom: "1px solid var(--dsw-alias-border-l3)"
189
1465
  },
190
- children: ["Agent · ", active.label]
1466
+ children: [
1467
+ ["home", "Home"],
1468
+ ["workflows", "Workflows"],
1469
+ ["settings", "Settings"]
1470
+ ].map(([id, label]) => {
1471
+ const active = tab === id;
1472
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1473
+ type: "button",
1474
+ onClick: () => {
1475
+ setTab(id);
1476
+ },
1477
+ style: {
1478
+ padding: "8px 10px",
1479
+ border: "none",
1480
+ background: "transparent",
1481
+ font: "inherit",
1482
+ fontSize: 13,
1483
+ fontWeight: active ? 500 : 400,
1484
+ color: active ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-secondary)",
1485
+ borderBottom: `2px solid ${active ? "var(--dsw-alias-brand-primary)" : "transparent"}`,
1486
+ marginBottom: -1,
1487
+ cursor: "pointer"
1488
+ },
1489
+ children: label
1490
+ }, id);
1491
+ })
191
1492
  }),
192
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
193
- style: styleBadge,
194
- children: "open"
195
- })
196
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
197
- "aria-hidden": true,
198
- children: "⌘"
1493
+ tab === "workflows" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1494
+ style: {
1495
+ display: "flex",
1496
+ flexDirection: "column",
1497
+ gap: 8
1498
+ },
1499
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupHead, {
1500
+ title: "Workflows",
1501
+ count: 0
1502
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1503
+ style: {
1504
+ margin: 0,
1505
+ fontSize: 13,
1506
+ lineHeight: "20px",
1507
+ color: "var(--dsw-alias-label-tertiary)"
1508
+ },
1509
+ children: "Orchestration across the machines and runtimes above — workflow authoring lands with the hub service."
1510
+ })]
1511
+ }),
1512
+ tab === "settings" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1513
+ style: {
1514
+ display: "flex",
1515
+ flexDirection: "column",
1516
+ gap: 8
1517
+ },
1518
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupHead, {
1519
+ title: "Settings",
1520
+ count: 0
1521
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1522
+ style: {
1523
+ margin: 0,
1524
+ fontSize: 13,
1525
+ lineHeight: "20px",
1526
+ color: "var(--dsw-alias-label-tertiary)"
1527
+ },
1528
+ children: "Hub preferences — pairing, runtime visibility, and orchestration defaults land with the hub service."
1529
+ })]
1530
+ }),
1531
+ tab === "home" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1532
+ style: {
1533
+ display: "flex",
1534
+ flexDirection: "column",
1535
+ gap: 8
1536
+ },
1537
+ children: [
1538
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupHead, {
1539
+ title: "Machines",
1540
+ count: machines.length,
1541
+ action: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1542
+ type: "button",
1543
+ onClick: () => {
1544
+ setShowAdd((value) => !value);
1545
+ },
1546
+ style: {
1547
+ display: "inline-flex",
1548
+ alignItems: "center",
1549
+ gap: 6,
1550
+ padding: "5px 10px",
1551
+ borderRadius: 8,
1552
+ border: "none",
1553
+ background: "transparent",
1554
+ color: "var(--dsw-alias-label-secondary)",
1555
+ font: "inherit",
1556
+ fontSize: 12.5,
1557
+ cursor: "pointer"
1558
+ },
1559
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1560
+ width: "11",
1561
+ height: "11",
1562
+ viewBox: "0 0 11 11",
1563
+ fill: "none",
1564
+ "aria-hidden": "true",
1565
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1566
+ d: "M5.5 1.5V9.5M1.5 5.5H9.5",
1567
+ stroke: "currentColor",
1568
+ strokeWidth: "1.4",
1569
+ strokeLinecap: "round"
1570
+ })
1571
+ }), "Add machine"]
1572
+ })
1573
+ }),
1574
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1575
+ style: {
1576
+ margin: 0,
1577
+ fontSize: 13,
1578
+ lineHeight: "20px",
1579
+ color: "var(--dsw-alias-label-tertiary)"
1580
+ },
1581
+ children: "Add another DSH from remote machines, provide the token URL."
1582
+ }),
1583
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1584
+ style: listStyle,
1585
+ children: machines.map((machine) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverRow, { children: [
1586
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
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", {
1597
+ style: {
1598
+ fontWeight: 500,
1599
+ whiteSpace: "nowrap"
1600
+ },
1601
+ children: machine.name
1602
+ }),
1603
+ machine.label !== machine.name && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1604
+ style: chip,
1605
+ children: machine.label
1606
+ }),
1607
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1608
+ style: {
1609
+ flex: 1,
1610
+ minWidth: 0,
1611
+ overflow: "hidden",
1612
+ textOverflow: "ellipsis",
1613
+ whiteSpace: "nowrap",
1614
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1615
+ fontSize: 12,
1616
+ color: "var(--dsw-alias-label-secondary)"
1617
+ },
1618
+ children: machine.url
1619
+ }),
1620
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1621
+ style: machine.self ? chipSelf : chipPending,
1622
+ children: machine.self ? "self" : "link pending"
1623
+ }),
1624
+ !machine.self && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1625
+ type: "button",
1626
+ "aria-label": `Remove ${machine.name}`,
1627
+ onClick: () => {
1628
+ removeMachine(machine.id);
1629
+ },
1630
+ style: {
1631
+ display: "inline-flex",
1632
+ alignItems: "center",
1633
+ justifyContent: "center",
1634
+ width: 22,
1635
+ height: 22,
1636
+ borderRadius: 6,
1637
+ border: "none",
1638
+ background: "transparent",
1639
+ color: "var(--dsw-alias-label-caption)",
1640
+ font: "inherit",
1641
+ cursor: "pointer"
1642
+ },
1643
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1644
+ width: "11",
1645
+ height: "11",
1646
+ viewBox: "0 0 11 11",
1647
+ fill: "none",
1648
+ "aria-hidden": "true",
1649
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
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
+ })
1655
+ })
1656
+ })
1657
+ ] }, machine.id))
1658
+ }),
1659
+ showAdd && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1660
+ style: {
1661
+ display: "grid",
1662
+ gridTemplateColumns: "0.5fr 1.4fr",
1663
+ gap: 10,
1664
+ padding: 12,
1665
+ margin: "0 -8px",
1666
+ borderRadius: 12,
1667
+ border: "1px solid var(--dsw-alias-border-l3)"
1668
+ },
1669
+ children: [
1670
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1671
+ style: fieldLabel,
1672
+ children: "Name"
1673
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1674
+ style: inputStyle,
1675
+ value: draft.name,
1676
+ placeholder: "dev3",
1677
+ onChange: (event) => {
1678
+ setDraft({
1679
+ ...draft,
1680
+ name: event.target.value
1681
+ });
1682
+ }
1683
+ })] }),
1684
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1685
+ style: fieldLabel,
1686
+ children: "URL"
1687
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1688
+ style: inputStyle,
1689
+ value: draft.url,
1690
+ placeholder: "http://192.168.31.130:4999",
1691
+ onChange: (event) => {
1692
+ setDraft({
1693
+ ...draft,
1694
+ url: event.target.value
1695
+ });
1696
+ }
1697
+ })] }),
1698
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1699
+ style: {
1700
+ gridColumn: "1 / -1",
1701
+ display: "flex",
1702
+ gap: 8,
1703
+ justifyContent: "flex-end"
1704
+ },
1705
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1706
+ type: "button",
1707
+ onClick: () => {
1708
+ setShowAdd(false);
1709
+ setDraft(EMPTY_DRAFT);
1710
+ },
1711
+ style: {
1712
+ padding: "7px 14px",
1713
+ borderRadius: 8,
1714
+ border: "none",
1715
+ background: "transparent",
1716
+ color: "var(--dsw-alias-label-secondary)",
1717
+ font: "inherit",
1718
+ fontSize: 13,
1719
+ cursor: "pointer"
1720
+ },
1721
+ children: "Cancel"
1722
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1723
+ type: "button",
1724
+ disabled: !draftValid,
1725
+ onClick: () => {
1726
+ addMachine();
1727
+ },
1728
+ style: {
1729
+ padding: "7px 14px",
1730
+ borderRadius: 8,
1731
+ border: "none",
1732
+ background: "color-mix(in srgb, currentColor 16%, transparent)",
1733
+ color: "inherit",
1734
+ font: "inherit",
1735
+ fontSize: 13,
1736
+ fontWeight: 500,
1737
+ cursor: draftValid ? "pointer" : "default",
1738
+ opacity: draftValid ? 1 : .5
1739
+ },
1740
+ children: "Add machine"
1741
+ })]
1742
+ })
1743
+ ]
1744
+ })
1745
+ ]
1746
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1747
+ style: {
1748
+ display: "flex",
1749
+ flexDirection: "column",
1750
+ gap: 8
1751
+ },
1752
+ children: [
1753
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupHead, {
1754
+ title: "Agent Runtimes",
1755
+ count: agents?.length ?? 0
1756
+ }),
1757
+ !switchUi.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1758
+ style: { display: "flex" },
1759
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1760
+ style: {
1761
+ ...chip,
1762
+ color: "var(--dsw-alias-label-caption)"
1763
+ },
1764
+ children: switchUi.hint
1765
+ })
1766
+ }),
1767
+ rosterFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1768
+ style: {
1769
+ margin: 0,
1770
+ fontSize: 12.5,
1771
+ color: "var(--dsw-alias-state-warn-label, var(--dsw-alias-label-secondary))"
1772
+ },
1773
+ children: "Live roster unavailable — showing the last known list."
1774
+ }),
1775
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
1776
+ style: listStyle,
1777
+ children: [agents === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
1778
+ style: {
1779
+ padding: "9px 8px",
1780
+ fontSize: 13,
1781
+ color: "var(--dsw-alias-label-tertiary)",
1782
+ listStyle: "none"
1783
+ },
1784
+ children: "Loading roster…"
1785
+ }), agents?.map((agent) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverRow, {
1786
+ clickable: switchUi.available,
1787
+ onClick: switchUi.available ? (event) => {
1788
+ rowDecision(agent, event);
1789
+ } : void 0,
1790
+ children: [
1791
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeMark, {
1792
+ agentKey: agent.key,
1793
+ size: 20
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))]
1842
+ })
1843
+ ]
1844
+ })] })
1845
+ ]
1846
+ })
1847
+ });
1848
+ }
1849
+
1850
+ //#endregion
1851
+ //#region src/client/HubPanelIcon.tsx
1852
+ /** Runtime-mark edge inside the selector, and the chevron's own default edge. */
1853
+ const MARK_SIZE = 16;
1854
+ const CHEVRON_SIZE = 14;
1855
+ /**
1856
+ * Selector pill metrics (AgentPresetSeat's `.seat`): a 28px control with 8px
1857
+ * side padding and a 4px icon gap. Width is the sum, so the pill hugs its two
1858
+ * marks instead of leaving the mark floating in a fixed box.
1859
+ */
1860
+ const SELECTOR_HEIGHT = 28;
1861
+ const SELECTOR_PAD = 8;
1862
+ const SELECTOR_GAP = 4;
1863
+ /** The row's own horizontal padding (native `.panelRow`): the selector's inset. */
1864
+ const ROW_INSET = 8;
1865
+ function HubPanelIcon({ size }) {
1866
+ const wide = size <= 16;
1867
+ const rootRef = react.useRef(null);
1868
+ const selectorRef = react.useRef(null);
1869
+ const [open, setOpen] = react.useState(false);
1870
+ const [hover, setHover] = react.useState(false);
1871
+ const [box, setBox] = react.useState(null);
1872
+ const [agents, setAgents] = react.useState(() => rosterFromGlobal() ?? [NATIVE_LINK]);
1873
+ const fusion = fusionPage();
1874
+ const switchUi = switchAffordance(fusion);
1875
+ const [activeKey, setActiveKey] = react.useState(() => fusion ? uiState()?.activeAgent : void 0);
1876
+ react.useEffect(() => {
1877
+ if (!fusion) return;
1878
+ const state = uiState();
1879
+ if (state === void 0) return;
1880
+ return state.subscribe((label) => {
1881
+ setActiveKey(label);
1882
+ });
1883
+ }, [fusion]);
1884
+ const measure = react.useCallback(() => {
1885
+ const row = rootRef.current?.closest("button") ?? null;
1886
+ if (row === null) {
1887
+ setBox(null);
1888
+ return;
1889
+ }
1890
+ const rect = row.getBoundingClientRect();
1891
+ setBox((prev) => prev !== null && prev.top === rect.top && prev.bottom === rect.bottom && prev.right === rect.right ? prev : {
1892
+ top: rect.top,
1893
+ bottom: rect.bottom,
1894
+ right: rect.right
1895
+ });
1896
+ }, []);
1897
+ react.useLayoutEffect(() => {
1898
+ if (!wide || !switchUi.available) return;
1899
+ measure();
1900
+ const row = rootRef.current?.closest("button") ?? null;
1901
+ let observer;
1902
+ if (row !== null && typeof ResizeObserver !== "undefined") {
1903
+ observer = new ResizeObserver(() => {
1904
+ measure();
1905
+ });
1906
+ observer.observe(row);
1907
+ }
1908
+ window.addEventListener("resize", measure);
1909
+ return () => {
1910
+ observer?.disconnect();
1911
+ window.removeEventListener("resize", measure);
1912
+ };
1913
+ }, [
1914
+ wide,
1915
+ switchUi.available,
1916
+ measure
1917
+ ]);
1918
+ react.useEffect(() => {
1919
+ if (!open || !switchUi.available) return;
1920
+ let cancelled = false;
1921
+ fetchRoster().then((list) => {
1922
+ if (!cancelled) setAgents(list);
1923
+ }).catch(() => {});
1924
+ return () => {
1925
+ cancelled = true;
1926
+ };
1927
+ }, [open, switchUi.available]);
1928
+ react.useEffect(() => {
1929
+ if (!wide || !switchUi.available) setOpen(false);
1930
+ }, [wide, switchUi.available]);
1931
+ const anchorRect = react.useCallback(() => selectorRef.current?.getBoundingClientRect() ?? null, []);
1932
+ const here = currentMountPath();
1933
+ const isActive = (agent) => hubRowActive({
1934
+ fusion,
1935
+ agentKey: agent.key,
1936
+ agentPath: agent.path,
1937
+ activeAgent: activeKey,
1938
+ mountPath: here
1939
+ });
1940
+ const toggle = (event) => {
1941
+ event.stopPropagation();
1942
+ if (event.type !== "keydown") event.preventDefault();
1943
+ setOpen((value) => !value);
1944
+ };
1945
+ const items = [];
1946
+ machineGroups(agents).forEach((group, groupIndex) => {
1947
+ if (groupIndex > 0) items.push({
1948
+ type: "separator",
1949
+ id: `machine-divider:${group.machine}`
1950
+ });
1951
+ items.push({
1952
+ type: "label",
1953
+ id: `machine:${group.machine}`,
1954
+ text: group.machine
1955
+ });
1956
+ for (const agent of group.agents) items.push({
1957
+ id: agent.key,
1958
+ label: agent.label,
1959
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeMark, {
1960
+ agentKey: agent.key,
1961
+ size: 14
199
1962
  })
200
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
201
- style: styleMenu,
202
- children: agents.map((agent) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
203
- href: agent.path,
1963
+ });
1964
+ });
1965
+ /**
1966
+ * A menu pick. The row is a `menuitem` button — there is no anchor default
1967
+ * to cancel — but the shared table still owns whether the attempt runs
1968
+ * (the active entry is a no-op, the fusion arm never navigates).
1969
+ */
1970
+ const select = (id) => {
1971
+ setOpen(false);
1972
+ const agent = agents.find((row) => row.key === id);
1973
+ if (agent === void 0) return;
1974
+ seatClickDecision({
1975
+ fusion,
1976
+ active: isActive(agent),
1977
+ attempt: () => attemptSwitch(agent.key)
1978
+ });
1979
+ };
1980
+ const activeLabel = activeAgentName(activeKey, agents);
1981
+ const selectorWidth = SELECTOR_PAD * 2 + CHEVRON_SIZE + (activeKey === void 0 ? 0 : MARK_SIZE + SELECTOR_GAP);
1982
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1983
+ ref: rootRef,
1984
+ style: {
1985
+ display: "inline-flex",
1986
+ alignItems: "center",
1987
+ justifyContent: "center"
1988
+ },
1989
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(HubGlyph, { size }), wide && box !== null && switchUi.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Menu, {
1990
+ open,
1991
+ onClose: () => {
1992
+ setOpen(false);
1993
+ },
1994
+ items,
1995
+ selectedIds: activeKey === void 0 ? [] : [activeKey],
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}`,
204
2009
  style: {
205
- ...styleItem(agent.path === here),
206
- textDecoration: "none"
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
207
2027
  },
208
- "aria-current": agent.path === here ? "page" : void 0,
209
- onClick: () => {
210
- setOpen(false);
2028
+ onMouseEnter: () => {
2029
+ setHover(true);
211
2030
  },
212
- children: agent.path === here ? `${agent.label} ✓` : agent.label
213
- }, agent.key))
2031
+ onMouseLeave: () => {
2032
+ setHover(false);
2033
+ },
2034
+ onClick: toggle,
2035
+ onKeyDown: (event) => {
2036
+ if (event.key === "Enter" || event.key === " ") toggle(event);
2037
+ },
2038
+ children: [activeKey !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeMark, {
2039
+ agentKey: activeKey,
2040
+ size: MARK_SIZE
2041
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { size: CHEVRON_SIZE })]
2042
+ })
214
2043
  })]
215
2044
  });
216
2045
  }
@@ -220,19 +2049,34 @@ function RuntimeSeat({ wide }) {
220
2049
  /** Required services (cordis fiber inject): the slot registry. */
221
2050
  const inject = ["slots"];
222
2051
  /**
223
- * Mount the selector. Degrades silently when the slot service is absent (a
224
- * composition without the sidebar renders nothing — a footer action is an
225
- * optional occupant by contract).
2052
+ * Mount the client surfaces. Degrades silently when the slot service is
2053
+ * absent (a composition without the sidebar renders nothing — every seat
2054
+ * here is an optional occupant by contract).
226
2055
  *
227
2056
  * @param ctx - client root context.
228
2057
  */
229
2058
  function apply(ctx) {
230
2059
  ctx.inject(["slots"], (raw) => {
231
2060
  const scope = raw;
232
- scope.effect(() => scope.slots.inject("sidebar.footer.action", () => scope.slots.register({
233
- name: "sidebar.footer.action",
234
- id: "agent-runtime"
235
- }, RuntimeSeat)), "agent-hub: runtime footer action");
2061
+ scope.effect(() => scope.slots.inject("main", () => scope.slots.register({
2062
+ name: "main",
2063
+ key: "hub"
2064
+ }, HubPage)), "superd-hub: hub main panel");
2065
+ scope.effect(() => scope.slots.inject("sidebar.panellist", () => scope.slots.register({
2066
+ name: "sidebar.panellist",
2067
+ id: "hub",
2068
+ order: 1,
2069
+ label: "Hub"
2070
+ }, HubPanelIcon)), "superd-hub: hub sidebar row");
2071
+ });
2072
+ ctx.inject([
2073
+ "connection",
2074
+ "uiWorkspace",
2075
+ "workspaces",
2076
+ "sessions",
2077
+ "locale"
2078
+ ], (raw) => {
2079
+ bindSwitchAgent(raw);
236
2080
  });
237
2081
  }
238
2082