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.
- package/README.md +3 -3
- package/agent-agy/cordis.patch.yml +8 -4
- package/agent-agy/dist/agent-preset-agy.d.ts +23 -43
- package/agent-agy/dist/agent-preset-agy.js +29 -166
- package/agent-agy/dist/agent.d.ts +2 -1
- package/agent-agy/dist/agent.js +3 -2
- package/agent-agy/dist/agy-sessions.d.ts +2 -1
- package/agent-agy/dist/agy-sessions.js +2 -11
- package/agent-agy/dist/index.js +13 -12
- package/agent-agy/dist/world-plugin.d.ts +2 -1
- package/agent-agy/dist/world-plugin.js +31 -51
- package/agent-agy/package.json +6 -6
- package/agent-claude/cordis.patch.yml +8 -4
- package/agent-claude/dist/agent-preset-claude.js +29 -166
- package/agent-claude/dist/agent.js +10 -10
- package/agent-claude/dist/claude-client.js +22 -3
- package/agent-claude/dist/claude-home.js +0 -8
- package/agent-claude/dist/index.js +12 -15
- package/agent-claude/dist/session-map.js +2 -5
- package/agent-claude/dist/world-plugin.js +3 -3
- package/agent-claude/package.json +9 -9
- package/agent-codex/README.md +8 -8
- package/agent-codex/cordis.patch.yml +9 -5
- package/agent-codex/dist/agent-preset-codex.js +29 -168
- package/agent-codex/dist/agent.js +17 -12
- package/agent-codex/dist/codex-store.js +0 -9
- package/agent-codex/dist/index.js +13 -16
- package/agent-codex/dist/session-map.js +2 -5
- package/agent-codex/dist/world-plugin.js +31 -51
- package/agent-codex/package.json +14 -11
- package/agent-hermes/README.md +9 -8
- package/agent-hermes/cordis.patch.yml +8 -4
- package/agent-hermes/dist/agent-preset-hermes.js +29 -168
- package/agent-hermes/dist/agent.js +33 -19
- package/agent-hermes/dist/hermes-client.js +5 -5
- package/agent-hermes/dist/hermes-store.js +2 -11
- package/agent-hermes/dist/index.js +13 -12
- package/agent-hermes/dist/world-plugin.js +31 -52
- package/agent-hermes/package.json +14 -11
- package/agent-omp/README.md +2 -2
- package/agent-omp/cordis.patch.yml +12 -7
- package/agent-omp/dist/agent-preset-omp.js +30 -169
- package/agent-omp/dist/agent.js +13 -5
- package/agent-omp/dist/index.js +12 -22
- package/agent-omp/dist/mobile/gesture.js +2 -2
- package/agent-omp/dist/mobile/zoom-guard.js +4 -4
- package/agent-omp/dist/mobile-boot.js +2 -2
- package/agent-omp/dist/omp-cli.js +1 -1
- package/agent-omp/dist/omp-disk-discovery.js +1 -1
- package/agent-omp/dist/protocol.js +1 -1
- package/agent-omp/dist/sidecar-client.js +1 -1
- package/agent-omp/dist/world-plugin.js +37 -50
- package/agent-omp/package.json +18 -14
- package/agent-omp/sidecar/main.ts +2 -2
- package/agent-pi/README.md +5 -5
- package/agent-pi/cordis.patch.yml +8 -4
- package/agent-pi/dist/agent-preset-pi.js +29 -166
- package/agent-pi/dist/agent.js +18 -12
- package/agent-pi/dist/index.js +14 -16
- package/agent-pi/dist/pi-home.js +2 -12
- package/agent-pi/dist/world-plugin.js +31 -51
- package/agent-pi/package.json +14 -11
- package/client/index.js +1994 -150
- package/cordis.patch.yml +39 -15
- package/package.json +32 -30
- package/{agent-hub → superd-hub}/cordis.patch.yml +3 -3
- package/{agent-hub → superd-hub}/dist/agent-roster.d.ts +3 -1
- package/{agent-hub → superd-hub}/dist/agent-roster.js +4 -2
- package/superd-hub/dist/carrier.d.ts +62 -0
- package/superd-hub/dist/carrier.js +213 -0
- package/superd-hub/dist/fusion-transport.d.ts +150 -0
- package/superd-hub/dist/fusion-transport.js +369 -0
- package/{agent-hub → superd-hub}/dist/gateway.d.ts +9 -4
- package/superd-hub/dist/gateway.js +44 -0
- package/superd-hub/dist/hub-route.d.ts +74 -0
- package/superd-hub/dist/hub-route.js +188 -0
- package/{agent-hub → superd-hub}/dist/index.d.ts +14 -6
- package/{agent-hub → superd-hub}/dist/index.js +14 -6
- package/{agent-hub → superd-hub}/dist/roster.d.ts +2 -2
- package/{agent-hub → superd-hub}/dist/rpc.js +1 -1
- package/{agent-hub → superd-hub}/dist/spawn-world.d.ts +27 -6
- package/superd-hub/dist/spawn-world.js +279 -0
- package/superd-hub/dist/switch-core.d.ts +630 -0
- package/superd-hub/dist/switch-core.js +734 -0
- package/superd-hub/dist/system-head.d.ts +69 -0
- package/superd-hub/dist/system-head.js +194 -0
- package/superd-hub/dist/web-trust.d.ts +112 -0
- package/superd-hub/dist/web-trust.js +126 -0
- package/superd-hub/dist/world-entry.d.ts +38 -0
- package/superd-hub/dist/world-entry.js +47 -0
- package/{agent-hub → superd-hub}/dist/world-host.d.ts +15 -20
- package/superd-hub/dist/world-host.js +65 -0
- package/{agent-hub → superd-hub}/dist/world-join.d.ts +2 -1
- package/{agent-hub → superd-hub}/dist/world-join.js +30 -50
- package/superd-hub/dist/world-mount.d.ts +82 -0
- package/superd-hub/dist/world-mount.js +105 -0
- package/{agent-hub → superd-hub}/dist/world-mux.d.ts +21 -6
- package/superd-hub/dist/world-mux.js +392 -0
- package/{agent-hub → superd-hub}/dist/world-provision.d.ts +20 -4
- package/superd-hub/dist/world-provision.js +203 -0
- package/superd-hub/dist/world-web-server.d.ts +113 -0
- package/superd-hub/dist/world-web-server.js +99 -0
- package/superd-hub/lib/client/index.js +2086 -0
- package/{agent-hub → superd-hub}/package.json +12 -7
- package/agent-hub/dist/carrier.d.ts +0 -43
- package/agent-hub/dist/carrier.js +0 -241
- package/agent-hub/dist/gateway.js +0 -18
- package/agent-hub/dist/spawn-world.js +0 -72
- package/agent-hub/dist/world-entry.d.ts +0 -31
- package/agent-hub/dist/world-entry.js +0 -51
- package/agent-hub/dist/world-host.js +0 -44
- package/agent-hub/dist/world-mount.d.ts +0 -52
- package/agent-hub/dist/world-mount.js +0 -52
- package/agent-hub/dist/world-mux.js +0 -224
- package/agent-hub/dist/world-provision.js +0 -122
- package/agent-hub/dist/world-web-server.d.ts +0 -162
- package/agent-hub/dist/world-web-server.js +0 -248
- package/agent-hub/lib/client/index.js +0 -242
- /package/{agent-hub → superd-hub}/dist/client-shim.d.ts +0 -0
- /package/{agent-hub → superd-hub}/dist/client-shim.js +0 -0
- /package/{agent-hub → superd-hub}/dist/envelope.d.ts +0 -0
- /package/{agent-hub → superd-hub}/dist/envelope.js +0 -0
- /package/{agent-hub → superd-hub}/dist/index-pass.d.ts +0 -0
- /package/{agent-hub → superd-hub}/dist/index-pass.js +0 -0
- /package/{agent-hub → superd-hub}/dist/labels.d.ts +0 -0
- /package/{agent-hub → superd-hub}/dist/labels.js +0 -0
- /package/{agent-hub → superd-hub}/dist/ownership.d.ts +0 -0
- /package/{agent-hub → superd-hub}/dist/ownership.js +0 -0
- /package/{agent-hub → superd-hub}/dist/roster.js +0 -0
- /package/{agent-hub → superd-hub}/dist/rpc.d.ts +0 -0
- /package/{agent-hub → superd-hub}/dist/targets.d.ts +0 -0
- /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/
|
|
32
|
-
|
|
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
|
-
/**
|
|
38
|
-
const
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
if (!Array.isArray(
|
|
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
|
|
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 :
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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 (!
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}, [
|
|
114
|
-
const here =
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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:
|
|
178
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
179
|
-
|
|
180
|
-
|
|
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.
|
|
1460
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
183
1461
|
style: {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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: [
|
|
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.
|
|
193
|
-
style:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
})
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
setOpen(false);
|
|
2028
|
+
onMouseEnter: () => {
|
|
2029
|
+
setHover(true);
|
|
211
2030
|
},
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
224
|
-
* composition without the sidebar renders nothing —
|
|
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("
|
|
233
|
-
name: "
|
|
234
|
-
|
|
235
|
-
},
|
|
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
|
|