super-dsh 0.1.6 → 0.1.10-e
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 +25 -3
- package/agent-agy/cordis.patch.yml +7 -4
- package/agent-agy/dist/agent.d.ts +12 -11
- package/agent-agy/dist/agy-auth-gateway.d.ts +124 -0
- package/agent-agy/dist/agy-auth-gateway.js +233 -0
- package/agent-agy/dist/agy-auth.d.ts +153 -0
- package/agent-agy/dist/agy-auth.js +179 -0
- package/agent-agy/dist/agy-cli-client.d.ts +39 -9
- package/agent-agy/dist/agy-cli-client.js +82 -18
- package/agent-agy/dist/agy-store.d.ts +8 -0
- package/agent-agy/dist/index.d.ts +46 -16
- package/agent-agy/dist/index.js +82 -56
- package/agent-agy/dist/models.d.ts +70 -0
- package/agent-agy/dist/models.js +102 -3
- package/agent-agy/package.json +7 -5
- package/agent-agy-sdk/cordis.patch.yml +58 -0
- package/agent-agy-sdk/dist/adapter.d.ts +15 -0
- package/agent-agy-sdk/dist/adapter.js +42 -0
- package/agent-agy-sdk/dist/agent-preset-agy.d.ts +27 -0
- package/agent-agy-sdk/dist/agent-preset-agy.js +32 -0
- package/agent-agy-sdk/dist/agent.d.ts +138 -0
- package/agent-agy-sdk/dist/agent.js +494 -0
- package/{agent-agy → agent-agy-sdk}/dist/agy-client.d.ts +26 -6
- package/{agent-agy → agent-agy-sdk}/dist/agy-client.js +43 -25
- package/agent-agy-sdk/dist/agy-sessions.d.ts +25 -0
- package/agent-agy-sdk/dist/agy-sessions.js +107 -0
- package/agent-agy-sdk/dist/agy-store.d.ts +39 -0
- package/agent-agy-sdk/dist/agy-store.js +83 -0
- package/agent-agy-sdk/dist/inbox.d.ts +26 -0
- package/agent-agy-sdk/dist/inbox.js +53 -0
- package/agent-agy-sdk/dist/index.d.ts +93 -0
- package/agent-agy-sdk/dist/index.js +476 -0
- package/agent-agy-sdk/dist/models.d.ts +41 -0
- package/agent-agy-sdk/dist/models.js +74 -0
- package/agent-agy-sdk/dist/sdk-venv.d.ts +76 -0
- package/agent-agy-sdk/dist/sdk-venv.js +173 -0
- package/agent-agy-sdk/dist/world-plugin.d.ts +10 -0
- package/agent-agy-sdk/dist/world-plugin.js +82 -0
- package/agent-agy-sdk/package.json +64 -0
- package/agent-claude/dist/adapter.js +106 -31
- package/agent-claude/dist/agent.js +154 -17
- package/agent-claude/dist/claude-client.js +84 -43
- package/agent-claude/dist/claude-launch.js +101 -0
- package/agent-claude/dist/claude-native-config.js +169 -0
- package/agent-claude/dist/claude-provider-gateway.js +340 -0
- package/agent-claude/dist/claude-providers.js +391 -0
- package/agent-claude/dist/claude-route.js +152 -0
- package/agent-claude/dist/index.js +214 -37
- package/agent-claude/dist/models.js +14 -0
- package/agent-claude/dist/sdk-runtime.js +320 -0
- package/agent-claude/package.json +6 -4
- package/agent-codex/README.md +141 -36
- package/agent-codex/cordis.patch.yml +14 -10
- package/agent-codex/dist/adapter.js +64 -28
- package/agent-codex/dist/agent.js +255 -81
- package/agent-codex/dist/app-server-approvals.js +165 -0
- package/agent-codex/dist/app-server-client.js +723 -0
- package/agent-codex/dist/app-server-events.js +432 -0
- package/agent-codex/dist/app-server-protocol.js +95 -0
- package/agent-codex/dist/app-server-wire.js +299 -0
- package/agent-codex/dist/codex-launch.js +78 -0
- package/agent-codex/dist/codex-native-config.js +392 -0
- package/agent-codex/dist/codex-provider-gateway.js +402 -0
- package/agent-codex/dist/codex-providers.js +376 -0
- package/agent-codex/dist/codex-route.js +86 -0
- package/agent-codex/dist/index.js +256 -115
- package/agent-codex/dist/permission.js +75 -1
- package/agent-codex/package.json +1 -2
- package/agent-codex-sdk/README.md +79 -0
- package/agent-codex-sdk/cordis.patch.yml +81 -0
- package/agent-codex-sdk/dist/adapter.js +91 -0
- package/agent-codex-sdk/dist/agent-preset-codex.js +32 -0
- package/agent-codex-sdk/dist/agent.js +1085 -0
- package/agent-codex-sdk/dist/codex-native-config.js +392 -0
- package/agent-codex-sdk/dist/codex-provider-gateway.js +402 -0
- package/agent-codex-sdk/dist/codex-providers.js +376 -0
- package/agent-codex-sdk/dist/codex-route.js +86 -0
- package/{agent-codex/dist/codex-client.js → agent-codex-sdk/dist/codex-sdk-client.js} +253 -32
- package/agent-codex-sdk/dist/codex-sdk-launch.js +92 -0
- package/agent-codex-sdk/dist/codex-store.js +180 -0
- package/agent-codex-sdk/dist/inbox.js +53 -0
- package/agent-codex-sdk/dist/index.js +739 -0
- package/agent-codex-sdk/dist/permission.js +71 -0
- package/agent-codex-sdk/dist/sdk-runtime.js +307 -0
- package/agent-codex-sdk/dist/session-map.js +167 -0
- package/agent-codex-sdk/dist/world-plugin.js +93 -0
- package/agent-codex-sdk/package.json +79 -0
- package/agent-codex-sdk/scripts/fetch-sdk-runtime.mjs +57 -0
- package/agent-omp/README.md +72 -55
- package/agent-omp/dist/agent.js +36 -11
- package/agent-omp/dist/index.js +92 -52
- package/agent-omp/dist/lazy-rpc.js +11 -4
- package/agent-omp/dist/omp-cli.js +114 -97
- package/agent-omp/dist/omp-store.js +12 -4
- package/agent-omp/dist/permission.js +70 -5
- package/agent-omp/dist/rpc-client.js +409 -0
- package/agent-omp/dist/world-plugin.js +36 -3
- package/agent-omp/package.json +2 -6
- package/agent-omp-sdk/README.md +103 -0
- package/agent-omp-sdk/cordis.patch.yml +108 -0
- package/agent-omp-sdk/dist/adapter.d.ts +21 -0
- package/agent-omp-sdk/dist/adapter.js +36 -0
- package/agent-omp-sdk/dist/agent-preset-omp.d.ts +31 -0
- package/agent-omp-sdk/dist/agent-preset-omp.js +32 -0
- package/agent-omp-sdk/dist/agent.d.ts +64 -0
- package/agent-omp-sdk/dist/agent.js +1178 -0
- package/agent-omp-sdk/dist/discovery.d.ts +57 -0
- package/agent-omp-sdk/dist/discovery.js +116 -0
- package/agent-omp-sdk/dist/inbox.d.ts +28 -0
- package/agent-omp-sdk/dist/inbox.js +53 -0
- package/agent-omp-sdk/dist/index.d.ts +60 -0
- package/agent-omp-sdk/dist/index.js +632 -0
- package/agent-omp-sdk/dist/knobs.d.ts +34 -0
- package/agent-omp-sdk/dist/knobs.js +50 -0
- package/agent-omp-sdk/dist/lazy-rpc.d.ts +98 -0
- package/agent-omp-sdk/dist/lazy-rpc.js +178 -0
- package/agent-omp-sdk/dist/mobile/zoom-guard.d.ts +187 -0
- package/agent-omp-sdk/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp-sdk/dist/mobile-boot.d.ts +49 -0
- package/agent-omp-sdk/dist/mobile-boot.js +38 -0
- package/agent-omp-sdk/dist/models.d.ts +39 -0
- package/agent-omp-sdk/dist/models.js +299 -0
- package/agent-omp-sdk/dist/omp-cli.d.ts +67 -0
- package/agent-omp-sdk/dist/omp-cli.js +164 -0
- package/agent-omp-sdk/dist/omp-disk-discovery.d.ts +27 -0
- package/agent-omp-sdk/dist/omp-disk-discovery.js +178 -0
- package/agent-omp-sdk/dist/omp-executable.d.ts +2 -0
- package/agent-omp-sdk/dist/omp-executable.js +30 -0
- package/agent-omp-sdk/dist/omp-store.d.ts +134 -0
- package/agent-omp-sdk/dist/omp-store.js +526 -0
- package/agent-omp-sdk/dist/pairing.d.ts +7 -0
- package/agent-omp-sdk/dist/pairing.js +45 -0
- package/agent-omp-sdk/dist/permission.d.ts +138 -0
- package/agent-omp-sdk/dist/permission.js +161 -0
- package/agent-omp-sdk/dist/protocol.d.ts +189 -0
- package/agent-omp-sdk/dist/rpc-types.d.ts +77 -0
- package/agent-omp-sdk/dist/rpc-types.js +1 -0
- package/agent-omp-sdk/dist/sdk-client.d.ts +160 -0
- package/{agent-omp → agent-omp-sdk}/dist/sdk-client.js +108 -56
- package/agent-omp-sdk/dist/sdk-runtime.d.ts +211 -0
- package/agent-omp-sdk/dist/sdk-runtime.js +418 -0
- package/agent-omp-sdk/dist/sidecar-client.d.ts +87 -0
- package/agent-omp-sdk/dist/sidecar-client.js +197 -0
- package/agent-omp-sdk/dist/store/adapter.d.ts +17 -0
- package/agent-omp-sdk/dist/store/adapter.js +47 -0
- package/agent-omp-sdk/dist/store/db.d.ts +70 -0
- package/agent-omp-sdk/dist/store/db.js +210 -0
- package/agent-omp-sdk/dist/store/index.d.ts +23 -0
- package/agent-omp-sdk/dist/store/index.js +59 -0
- package/agent-omp-sdk/dist/store/reconcile.d.ts +15 -0
- package/agent-omp-sdk/dist/store/reconcile.js +51 -0
- package/agent-omp-sdk/dist/store/schema.d.ts +8 -0
- package/agent-omp-sdk/dist/store/schema.js +40 -0
- package/agent-omp-sdk/dist/transport.d.ts +49 -0
- package/agent-omp-sdk/dist/transport.js +35 -0
- package/agent-omp-sdk/dist/world-home.d.ts +4 -0
- package/agent-omp-sdk/dist/world-home.js +23 -0
- package/agent-omp-sdk/dist/world-plugin.d.ts +10 -0
- package/agent-omp-sdk/dist/world-plugin.js +137 -0
- package/agent-omp-sdk/package.json +87 -0
- package/client/index.js +1883 -287
- package/cordis.patch.yml +80 -0
- package/package.json +23 -9
- package/superd-hub/dist/a2a-delivery.d.ts +189 -0
- package/superd-hub/dist/a2a-delivery.js +240 -0
- package/superd-hub/dist/a2a-host.d.ts +76 -0
- package/superd-hub/dist/a2a-host.js +145 -0
- package/superd-hub/dist/a2a-ingest.d.ts +31 -0
- package/superd-hub/dist/a2a-ingest.js +636 -0
- package/superd-hub/dist/a2a-log.d.ts +17 -0
- package/superd-hub/dist/a2a-log.js +42 -0
- package/superd-hub/dist/a2a-row.d.ts +65 -0
- package/superd-hub/dist/a2a-row.js +162 -0
- package/superd-hub/dist/a2a-service.d.ts +357 -0
- package/superd-hub/dist/a2a-service.js +980 -0
- package/superd-hub/dist/a2a-wire.d.ts +220 -0
- package/superd-hub/dist/a2a-wire.js +305 -0
- package/superd-hub/dist/agent-ids.d.ts +10 -0
- package/superd-hub/dist/agent-ids.js +68 -0
- package/superd-hub/dist/agent-roster.d.ts +29 -18
- package/superd-hub/dist/agent-roster.js +54 -15
- package/superd-hub/dist/agent-status.d.ts +119 -0
- package/superd-hub/dist/agent-status.js +386 -0
- package/superd-hub/dist/client/agent-settings.d.ts +210 -0
- package/superd-hub/dist/client/agent-settings.js +300 -0
- package/superd-hub/dist/client/mesh-api.d.ts +134 -0
- package/superd-hub/dist/client/mesh-api.js +219 -0
- package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
- package/superd-hub/dist/client/mesh-groups.js +66 -0
- package/superd-hub/dist/client/roster.d.ts +48 -0
- package/superd-hub/dist/client/roster.js +102 -0
- package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
- package/superd-hub/dist/client/runtime-settings-api.js +137 -0
- package/superd-hub/dist/client/switch.d.ts +165 -0
- package/superd-hub/dist/client/switch.js +295 -0
- package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
- package/superd-hub/dist/exec-pty-channel.js +162 -0
- package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
- package/superd-hub/dist/exec-pty-framing.js +81 -0
- package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
- package/superd-hub/dist/exec-pty-rpc.js +128 -0
- package/superd-hub/dist/exec-pty-session.d.ts +76 -0
- package/superd-hub/dist/exec-pty-session.js +234 -0
- package/superd-hub/dist/exec.d.ts +56 -0
- package/superd-hub/dist/exec.js +225 -0
- package/superd-hub/dist/gateway.d.ts +39 -2
- package/superd-hub/dist/gateway.js +62 -3
- package/superd-hub/dist/hub-tool.d.ts +174 -0
- package/superd-hub/dist/hub-tool.js +464 -0
- package/superd-hub/dist/index.d.ts +15 -1
- package/superd-hub/dist/index.js +16 -0
- package/superd-hub/dist/machines.d.ts +45 -0
- package/superd-hub/dist/machines.js +125 -0
- package/superd-hub/dist/mesh-federate.d.ts +138 -0
- package/superd-hub/dist/mesh-federate.js +223 -0
- package/superd-hub/dist/mesh-gateway.d.ts +67 -0
- package/superd-hub/dist/mesh-gateway.js +408 -0
- package/superd-hub/dist/mesh-link.d.ts +274 -0
- package/superd-hub/dist/mesh-link.js +441 -0
- package/superd-hub/dist/mesh-rpc.d.ts +123 -0
- package/superd-hub/dist/mesh-rpc.js +250 -0
- package/superd-hub/dist/mux-client.d.ts +71 -0
- package/superd-hub/dist/mux-client.js +281 -0
- package/superd-hub/dist/pack-version.d.ts +2 -0
- package/superd-hub/dist/pack-version.js +30 -0
- package/superd-hub/dist/roster.d.ts +6 -0
- package/superd-hub/dist/rpc.d.ts +18 -2
- package/superd-hub/dist/rpc.js +34 -4
- package/superd-hub/dist/spawn-world.js +3 -1
- package/superd-hub/dist/switch-core.d.ts +62 -25
- package/superd-hub/dist/switch-core.js +53 -30
- package/superd-hub/dist/system-head.d.ts +22 -12
- package/superd-hub/dist/system-head.js +100 -84
- package/superd-hub/dist/wire-endpoints.d.ts +264 -0
- package/superd-hub/dist/wire-endpoints.js +180 -0
- package/superd-hub/lib/client/index.js +1883 -287
- package/superd-hub/package.json +11 -2
- package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
- package/agent-agy/dist/agent-preset-projection.js +0 -33
- package/agent-claude/dist/agent-preset-projection.js +0 -30
- package/agent-codex/dist/agent-preset-projection.js +0 -37
- package/agent-codex/dist/app-home.js +0 -65
- package/agent-codex/dist/models.js +0 -152
- package/agent-codex/dist/spike.js +0 -46
- package/agent-hermes/dist/agent-preset-projection.js +0 -34
- package/agent-omp/dist/agent-preset-projection.js +0 -36
- package/agent-omp/dist/app-home.js +0 -86
- package/agent-omp/dist/sidecar-client.js +0 -195
- package/agent-pi/dist/agent-preset-projection.js +0 -29
- package/superd-hub/dist/client-shim.d.ts +0 -28
- package/superd-hub/dist/client-shim.js +0 -251
- package/superd-hub/dist/index-pass.d.ts +0 -22
- package/superd-hub/dist/index-pass.js +0 -47
- package/superd-hub/dist/labels.d.ts +0 -45
- package/superd-hub/dist/labels.js +0 -83
- /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
- /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
- /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
- /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
- /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
package/client/index.js
CHANGED
|
@@ -25,10 +25,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
//#endregion
|
|
26
26
|
let react = require("react");
|
|
27
27
|
react = __toESM(react);
|
|
28
|
-
let react_jsx_runtime = require("react/jsx-runtime");
|
|
29
|
-
react_jsx_runtime = __toESM(react_jsx_runtime);
|
|
30
28
|
let __deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
31
29
|
__deepseek_ai_dsh_client_ui_primitives = __toESM(__deepseek_ai_dsh_client_ui_primitives);
|
|
30
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
31
|
+
react_jsx_runtime = __toESM(react_jsx_runtime);
|
|
32
32
|
|
|
33
33
|
//#region src/client/glyph.tsx
|
|
34
34
|
/**
|
|
@@ -241,9 +241,10 @@ function imgMark(uri, size) {
|
|
|
241
241
|
*/
|
|
242
242
|
function RuntimeMark({ agentKey, size }) {
|
|
243
243
|
if (agentKey === "native") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DshFish, { size });
|
|
244
|
-
const
|
|
244
|
+
const base = agentKey.endsWith("-sdk") ? agentKey.slice(0, -4) : agentKey;
|
|
245
|
+
const uri = RUNTIME_ICON_URIS[base];
|
|
245
246
|
if (uri === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubagentGlyph, { size });
|
|
246
|
-
const tile = RUNTIME_TILE_BG[
|
|
247
|
+
const tile = RUNTIME_TILE_BG[base];
|
|
247
248
|
if (tile === void 0) return imgMark(uri, size);
|
|
248
249
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
249
250
|
"aria-hidden": "true",
|
|
@@ -280,7 +281,10 @@ const FALLBACK_LABELS = {
|
|
|
280
281
|
claude: "Claude",
|
|
281
282
|
pi: "Pi",
|
|
282
283
|
hermes: "Hermes",
|
|
283
|
-
agy: "Antigravity"
|
|
284
|
+
agy: "Antigravity",
|
|
285
|
+
"omp-sdk": "OMP (SDK)",
|
|
286
|
+
"agy-sdk": "Antigravity (SDK)",
|
|
287
|
+
"codex-sdk": "Codex (SDK)"
|
|
284
288
|
};
|
|
285
289
|
/** Coerce a wire `agents` array into links; undefined when unusable/empty. */
|
|
286
290
|
function coerceAgents(raw) {
|
|
@@ -294,7 +298,7 @@ function coerceAgents(raw) {
|
|
|
294
298
|
key,
|
|
295
299
|
label: typeof label === "string" ? label : FALLBACK_LABELS[key] ?? key,
|
|
296
300
|
path,
|
|
297
|
-
machine: typeof machine === "string" ? machine :
|
|
301
|
+
machine: typeof machine === "string" && machine !== "" ? machine : void 0,
|
|
298
302
|
ready: entry.ready === true,
|
|
299
303
|
failed: entry.failed === true
|
|
300
304
|
});
|
|
@@ -308,7 +312,8 @@ function rosterFromGlobal() {
|
|
|
308
312
|
}
|
|
309
313
|
/** Normalize the current mount root: `/omp` and `/omp/` are the same page. */
|
|
310
314
|
function currentMountPath() {
|
|
311
|
-
const
|
|
315
|
+
const location$1 = globalThis.location;
|
|
316
|
+
const pathname = location$1 === void 0 ? "/" : location$1.pathname;
|
|
312
317
|
if (pathname === "" || pathname === "/") return "/";
|
|
313
318
|
return pathname.endsWith("/") ? pathname : `${pathname}/`;
|
|
314
319
|
}
|
|
@@ -343,78 +348,288 @@ async function fetchRoster() {
|
|
|
343
348
|
}
|
|
344
349
|
|
|
345
350
|
//#endregion
|
|
346
|
-
//#region src/client/
|
|
347
|
-
/**
|
|
348
|
-
|
|
351
|
+
//#region src/client/mesh-api.ts
|
|
352
|
+
/**
|
|
353
|
+
* Machine-mesh data access for the Hub machines panel (mesh P1, Task 5):
|
|
354
|
+
* the client half of `/api/superd/machines` (host side: mesh-rpc.ts) —
|
|
355
|
+
* same-origin fetch behind the standard browser-trust + cookie fence, the
|
|
356
|
+
* roster face's house style (`credentials: 'same-origin'`). The localStorage
|
|
357
|
+
* ledger era (machines-store.ts, retired with this module) ended: the hub's
|
|
358
|
+
* registry is the single source of truth, this file only fetches it and
|
|
359
|
+
* coerces the wire answer into row views.
|
|
360
|
+
*
|
|
361
|
+
* Node-safe by construction — no DOM, no react, no @deepseek-ai imports —
|
|
362
|
+
* so the host tsc emits it to `dist/client/mesh-api.js` and
|
|
363
|
+
* test/client-mesh-view.test.mjs runs the pure halves (coercion, token-URL
|
|
364
|
+
* pre-check) under `node --test` without jsdom: the switch-core/toast-core
|
|
365
|
+
* precedent in reverse (pure logic kept beside the fetch wrappers, all under
|
|
366
|
+
* src/client). No fetch happens at import time.
|
|
367
|
+
*
|
|
368
|
+
* @module superd-hub/client/mesh-api
|
|
369
|
+
*/
|
|
349
370
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
371
|
+
* Base path of the mesh machines API — the client-side spelling of the
|
|
372
|
+
* host's `MESH_API_BASE` (mesh-rpc.ts). Duplicated deliberately: importing
|
|
373
|
+
* the host module would drag its node:fs-backed dependencies into the
|
|
374
|
+
* browser bundle; a path literal cannot drift silently because the mesh-rpc
|
|
375
|
+
* route registration and this file are reviewed together.
|
|
353
376
|
*/
|
|
354
|
-
|
|
377
|
+
const MESH_API_BASE = "/api/superd/machines";
|
|
378
|
+
/** The statuses the wire may carry; anything else coerces to `'pending'`. */
|
|
379
|
+
const STATUSES = [
|
|
380
|
+
"pending",
|
|
381
|
+
"reachable",
|
|
382
|
+
"unreachable",
|
|
383
|
+
"auth-expired"
|
|
384
|
+
];
|
|
385
|
+
/** Coerce the self block; a shape without a non-empty string id + string label is simply absent. */
|
|
386
|
+
function coerceSelf(raw) {
|
|
387
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
388
|
+
const { id, label } = raw;
|
|
389
|
+
if (typeof id !== "string" || id === "" || typeof label !== "string") return void 0;
|
|
355
390
|
return {
|
|
356
|
-
id
|
|
357
|
-
|
|
358
|
-
label: "This machine",
|
|
359
|
-
url: typeof location === "undefined" ? "" : location.origin,
|
|
360
|
-
self: true,
|
|
361
|
-
addedAt: 0
|
|
391
|
+
id,
|
|
392
|
+
label
|
|
362
393
|
};
|
|
363
394
|
}
|
|
364
395
|
/**
|
|
365
|
-
*
|
|
396
|
+
* Coerce a raw GET body into the panel's view list. Any bad shape degrades
|
|
397
|
+
* to the empty list (rows dropped individually, self dropped as a block) —
|
|
398
|
+
* the UI never crashes on a malformed answer.
|
|
366
399
|
*
|
|
367
|
-
* @
|
|
400
|
+
* @param raw - the parsed JSON body (or anything else the wire produced).
|
|
401
|
+
* @returns self (when usable) + the coerced remote rows.
|
|
368
402
|
*/
|
|
369
|
-
function
|
|
370
|
-
|
|
371
|
-
|
|
403
|
+
function coerceMachineList(raw) {
|
|
404
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { machines: [] };
|
|
405
|
+
const record = raw;
|
|
406
|
+
const self = coerceSelf(record.self);
|
|
407
|
+
const machines = [];
|
|
408
|
+
if (Array.isArray(record.machines)) for (const entry of record.machines) {
|
|
409
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
410
|
+
const row = entry;
|
|
411
|
+
if (typeof row.id !== "string" || row.id === "") continue;
|
|
412
|
+
if (typeof row.name !== "string" || row.name === "") continue;
|
|
413
|
+
if (typeof row.origin !== "string" || row.origin === "") continue;
|
|
414
|
+
const status = typeof row.status === "string" && STATUSES.includes(row.status) ? row.status : "pending";
|
|
415
|
+
const base = {
|
|
416
|
+
id: row.id,
|
|
417
|
+
name: row.name,
|
|
418
|
+
label: typeof row.label === "string" && row.label !== "" ? row.label : row.name,
|
|
419
|
+
origin: row.origin,
|
|
420
|
+
status,
|
|
421
|
+
self: self !== void 0 && row.id === self.id
|
|
422
|
+
};
|
|
423
|
+
const withProbe = typeof row.lastProbeAt === "number" ? {
|
|
424
|
+
...base,
|
|
425
|
+
lastProbeAt: row.lastProbeAt
|
|
426
|
+
} : base;
|
|
427
|
+
machines.push(typeof row.versionWarning === "string" && row.versionWarning !== "" ? {
|
|
428
|
+
...withProbe,
|
|
429
|
+
versionWarning: row.versionWarning
|
|
430
|
+
} : withProbe);
|
|
431
|
+
}
|
|
432
|
+
return self === void 0 ? { machines } : {
|
|
433
|
+
self,
|
|
434
|
+
machines
|
|
435
|
+
};
|
|
372
436
|
}
|
|
373
|
-
/**
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
437
|
+
/**
|
|
438
|
+
* The version-warning chip for one machine row (I2): present EXACTLY when the
|
|
439
|
+
* coerced row carries a `versionWarning` — the Hub machines panel renders
|
|
440
|
+
* the chip iff this returns a value. Kept here beside the coercion so the
|
|
441
|
+
* presence contract stays unit-testable without jsdom (the HubPage seat is
|
|
442
|
+
* a one-line consumer following the existing chip patterns).
|
|
443
|
+
*/
|
|
444
|
+
function machineVersionChip(machine) {
|
|
445
|
+
return machine.versionWarning === void 0 ? void 0 : { text: machine.versionWarning };
|
|
446
|
+
}
|
|
447
|
+
/** Read a non-2xx body's `{error}` message; degrade to the bare status line. */
|
|
448
|
+
async function errorText(response) {
|
|
377
449
|
try {
|
|
378
|
-
const
|
|
379
|
-
if (
|
|
380
|
-
|
|
450
|
+
const body = await response.json();
|
|
451
|
+
if (typeof body?.error === "string" && body.error !== "") return body.error;
|
|
452
|
+
} catch {}
|
|
453
|
+
return `HTTP ${response.status}`;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* GET the machine list (self + remotes).
|
|
457
|
+
*
|
|
458
|
+
* @returns the coerced list — a malformed body degrades to the empty list, never a throw.
|
|
459
|
+
* @throws when the request fails or the face answers non-2xx.
|
|
460
|
+
*/
|
|
461
|
+
async function fetchMachines() {
|
|
462
|
+
const response = await fetch(MESH_API_BASE, { credentials: "same-origin" });
|
|
463
|
+
if (!response.ok) throw new Error(await errorText(response));
|
|
464
|
+
return coerceMachineList(await response.json().catch(() => void 0));
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* POST a pairing `{name, label?, tokenUrl}` (label omitted → the server
|
|
468
|
+
* collapses it to the name).
|
|
469
|
+
*
|
|
470
|
+
* @throws Error(body.error) when the face answers non-2xx — the message is
|
|
471
|
+
* mesh-link's own, already token-redacted.
|
|
472
|
+
*/
|
|
473
|
+
async function addMachine(input) {
|
|
474
|
+
const body = {
|
|
475
|
+
name: input.name,
|
|
476
|
+
tokenUrl: input.tokenUrl
|
|
477
|
+
};
|
|
478
|
+
if (typeof input.label === "string" && input.label !== "") body.label = input.label;
|
|
479
|
+
const response = await fetch(MESH_API_BASE, {
|
|
480
|
+
credentials: "same-origin",
|
|
481
|
+
method: "POST",
|
|
482
|
+
headers: { "content-type": "application/json" },
|
|
483
|
+
body: JSON.stringify(body)
|
|
484
|
+
});
|
|
485
|
+
if (!response.ok) throw new Error(await errorText(response));
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* DELETE one remote row by id (`{removed: boolean}` answers even for
|
|
489
|
+
* unknown ids — a false removal is a reload, not an error).
|
|
490
|
+
*
|
|
491
|
+
* @throws Error(body.error) on a non-2xx answer.
|
|
492
|
+
*/
|
|
493
|
+
async function removeMachine(id) {
|
|
494
|
+
const response = await fetch(`${MESH_API_BASE}?id=${encodeURIComponent(id)}`, {
|
|
495
|
+
credentials: "same-origin",
|
|
496
|
+
method: "DELETE"
|
|
497
|
+
});
|
|
498
|
+
if (!response.ok) throw new Error(await errorText(response));
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* POST `{op: 'reprobe', id}` — a fresh probe of one remote; the outcome
|
|
502
|
+
* (even unreachable/auth-expired) lands in the status table server-side, so
|
|
503
|
+
* the caller reloads rather than interpreting the probe body.
|
|
504
|
+
*
|
|
505
|
+
* @throws Error(body.error) on a non-2xx answer (the op itself failed).
|
|
506
|
+
*/
|
|
507
|
+
async function reprobeMachine(id) {
|
|
508
|
+
const response = await fetch(MESH_API_BASE, {
|
|
509
|
+
credentials: "same-origin",
|
|
510
|
+
method: "POST",
|
|
511
|
+
headers: { "content-type": "application/json" },
|
|
512
|
+
body: JSON.stringify({
|
|
513
|
+
op: "reprobe",
|
|
514
|
+
id
|
|
515
|
+
})
|
|
516
|
+
});
|
|
517
|
+
if (!response.ok) throw new Error(await errorText(response));
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* The standing cadence note (D18): machine status refresh rides the hub's
|
|
521
|
+
* 5-minute sweep — the panel renders this verbatim beside its live-fetch
|
|
522
|
+
* control. English copy to match the DSH UI.
|
|
523
|
+
*/
|
|
524
|
+
const AUTO_REFRESH_NOTE = "auto-refreshes every 5 min";
|
|
525
|
+
/**
|
|
526
|
+
* The per-machine last-fetch line, shaped from the row's `lastProbeAt`
|
|
527
|
+
* (the hub's status table stamp — mesh-rpc joins it on every GET).
|
|
528
|
+
* Pure by design (the UI passes its own `now`): headless-testable without
|
|
529
|
+
* jsdom, same presence contract as {@link machineVersionChip} — absent or
|
|
530
|
+
* non-finite stamp means "never probed" and the panel omits the line.
|
|
531
|
+
*
|
|
532
|
+
* @param lastProbeAt - epoch ms of the machine's last recorded probe.
|
|
533
|
+
* @param now - epoch ms the panel renders at.
|
|
534
|
+
* @returns "Updated 2m ago" / "Updated just now" / … , or undefined.
|
|
535
|
+
*/
|
|
536
|
+
function lastFetchLabel(lastProbeAt, now) {
|
|
537
|
+
if (typeof lastProbeAt !== "number" || !Number.isFinite(lastProbeAt)) return void 0;
|
|
538
|
+
const seconds = Math.max(0, Math.floor((now - lastProbeAt) / 1e3));
|
|
539
|
+
if (seconds < 60) return "Updated just now";
|
|
540
|
+
if (seconds < 3600) return `Updated ${Math.floor(seconds / 60)}m ago`;
|
|
541
|
+
if (seconds < 86400) return `Updated ${Math.floor(seconds / 3600)}h ago`;
|
|
542
|
+
return `Updated ${Math.floor(seconds / 86400)}d ago`;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Client-side pre-check before a pairing submit: a parseable absolute
|
|
546
|
+
* http(s) URL carrying a non-empty `token` query param. The server re-parses
|
|
547
|
+
* authoritatively (mesh-link's parseTokenUrl); this only keeps obviously
|
|
548
|
+
* dead drafts off the wire.
|
|
549
|
+
*
|
|
550
|
+
* @param input - the raw token-URL draft (trim before calling).
|
|
551
|
+
* @returns whether the draft is submit-shaped.
|
|
552
|
+
*/
|
|
553
|
+
function validTokenUrl(input) {
|
|
554
|
+
if (typeof input !== "string" || input === "") return false;
|
|
555
|
+
let url;
|
|
556
|
+
try {
|
|
557
|
+
url = new URL(input);
|
|
381
558
|
} catch {
|
|
382
|
-
return
|
|
383
|
-
}
|
|
384
|
-
if (!Array.isArray(parsed)) return [];
|
|
385
|
-
const list = [];
|
|
386
|
-
for (const entry of parsed) {
|
|
387
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
388
|
-
const { id, name, label, url, self, addedAt } = entry;
|
|
389
|
-
if (self === true) continue;
|
|
390
|
-
if (typeof id !== "string" || typeof name !== "string" || typeof url !== "string") continue;
|
|
391
|
-
list.push({
|
|
392
|
-
id,
|
|
393
|
-
name,
|
|
394
|
-
label: typeof label === "string" ? label : name,
|
|
395
|
-
url,
|
|
396
|
-
self: false,
|
|
397
|
-
addedAt: typeof addedAt === "number" ? addedAt : 0
|
|
398
|
-
});
|
|
559
|
+
return false;
|
|
399
560
|
}
|
|
400
|
-
return
|
|
561
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
562
|
+
const token = url.searchParams.get("token");
|
|
563
|
+
return token !== null && token !== "";
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
//#endregion
|
|
567
|
+
//#region src/client/mesh-groups.ts
|
|
568
|
+
/** The machine key every LOCAL roster row carries (older host shapes may omit the field — grouping treats absent as local). */
|
|
569
|
+
const LOCAL_MACHINE_KEY = "local";
|
|
570
|
+
/** The LOCAL section's fixed display label — this machine needs no registry row to be named. */
|
|
571
|
+
const LOCAL_MACHINE_LABEL = "This machine";
|
|
572
|
+
/** Whether a row's machine field parks it in the local section: `'local'` or absent (the coercion drops unknown values). */
|
|
573
|
+
function isLocalMachine(machine) {
|
|
574
|
+
return machine === void 0 || machine === LOCAL_MACHINE_KEY;
|
|
401
575
|
}
|
|
402
576
|
/**
|
|
403
|
-
*
|
|
577
|
+
* Group a roster into machine sections for the switch menu and the Hub
|
|
578
|
+
* roster rows: the LOCAL group first (label {@link LOCAL_MACHINE_LABEL};
|
|
579
|
+
* rows whose `machine` is `'local'` or absent), then one group per remote
|
|
580
|
+
* machine sorted by display label (code-unit order) with the machine key as
|
|
581
|
+
* the tiebreak. Members keep the roster's given order inside their group;
|
|
582
|
+
* groups only exist for machines actually present in the roster (no empty
|
|
583
|
+
* local section when every row is federated). An unknown label map entry —
|
|
584
|
+
* missing, or an empty string — falls back to the raw machine key.
|
|
404
585
|
*
|
|
405
|
-
* @param
|
|
586
|
+
* @param links - the roster rows (any order; the section order is derived, not inherited).
|
|
587
|
+
* @param machineLabels - machine key (m4) → display label; the machines API answer via {@link machineLabelsFrom}.
|
|
588
|
+
* @returns the sections, local first.
|
|
406
589
|
*/
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
590
|
+
function groupByMachine(links, machineLabels) {
|
|
591
|
+
const local = [];
|
|
592
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
593
|
+
for (const link of links) {
|
|
594
|
+
if (isLocalMachine(link.machine)) {
|
|
595
|
+
local.push(link);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
const machine = link.machine;
|
|
599
|
+
const bucket = byKey.get(machine);
|
|
600
|
+
if (bucket === void 0) byKey.set(machine, [link]);
|
|
601
|
+
else bucket.push(link);
|
|
602
|
+
}
|
|
603
|
+
const labelOf = (key) => {
|
|
604
|
+
const label = machineLabels[key];
|
|
605
|
+
return typeof label === "string" && label !== "" ? label : key;
|
|
606
|
+
};
|
|
607
|
+
const remote = [...byKey.entries()].map(([key, agents]) => ({
|
|
608
|
+
key,
|
|
609
|
+
label: labelOf(key),
|
|
610
|
+
agents
|
|
611
|
+
})).sort((a, b) => a.label !== b.label ? a.label < b.label ? -1 : 1 : a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
612
|
+
return local.length > 0 ? [{
|
|
613
|
+
key: LOCAL_MACHINE_KEY,
|
|
614
|
+
label: LOCAL_MACHINE_LABEL,
|
|
615
|
+
agents: local
|
|
616
|
+
}, ...remote] : remote;
|
|
412
617
|
}
|
|
413
|
-
/**
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
618
|
+
/**
|
|
619
|
+
* The machines API answer → the grouping label map: every remote row's
|
|
620
|
+
* `id → label` (mesh-api's coercion already collapsed an empty label to the
|
|
621
|
+
* machine name) plus the self block's own `m4 → label`. An absent list is
|
|
622
|
+
* the empty map — the caller's degrade-to-flat trigger, never a throw.
|
|
623
|
+
*
|
|
624
|
+
* @param list - the coerced `fetchMachines()` answer, when it succeeded.
|
|
625
|
+
* @returns machine key → display label (the raw key stays absent, not mapped).
|
|
626
|
+
*/
|
|
627
|
+
function machineLabelsFrom(list) {
|
|
628
|
+
const labels = {};
|
|
629
|
+
if (list === void 0) return labels;
|
|
630
|
+
for (const machine of list.machines) labels[machine.id] = machine.label !== "" ? machine.label : machine.name;
|
|
631
|
+
if (list.self !== void 0) labels[list.self.id] = list.self.label;
|
|
632
|
+
return labels;
|
|
418
633
|
}
|
|
419
634
|
|
|
420
635
|
//#endregion
|
|
@@ -888,37 +1103,6 @@ function activeAgentName(activeAgent, roster) {
|
|
|
888
1103
|
if (activeAgent === void 0 || activeAgent === "") return void 0;
|
|
889
1104
|
return roster.find((row) => row.key === activeAgent)?.label ?? activeAgent;
|
|
890
1105
|
}
|
|
891
|
-
/** Which machine one roster row groups under: the row's own machine (parser-guaranteed). */
|
|
892
|
-
function machineGroup(row) {
|
|
893
|
-
return row.machine;
|
|
894
|
-
}
|
|
895
|
-
/**
|
|
896
|
-
* Group a roster by machine for the HubPanelIcon chevron menu's native
|
|
897
|
-
* workspace-list look: one group per distinct `machineGroup` key, groups in
|
|
898
|
-
* first-appearance order, members keeping the roster's order inside their
|
|
899
|
-
* group. A single-group roster (today's local-only shape) renders exactly
|
|
900
|
-
* like the old flat list minus the card border.
|
|
901
|
-
*
|
|
902
|
-
* Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
|
|
903
|
-
*/
|
|
904
|
-
function machineGroups(rows) {
|
|
905
|
-
const groups = [];
|
|
906
|
-
const byMachine = /* @__PURE__ */ new Map();
|
|
907
|
-
for (const row of rows) {
|
|
908
|
-
const machine = machineGroup(row);
|
|
909
|
-
let group = byMachine.get(machine);
|
|
910
|
-
if (group === void 0) {
|
|
911
|
-
group = {
|
|
912
|
-
machine,
|
|
913
|
-
agents: []
|
|
914
|
-
};
|
|
915
|
-
byMachine.set(machine, group);
|
|
916
|
-
groups.push(group);
|
|
917
|
-
}
|
|
918
|
-
group.agents.push(row);
|
|
919
|
-
}
|
|
920
|
-
return groups;
|
|
921
|
-
}
|
|
922
1106
|
/**
|
|
923
1107
|
* Build the switch machine over an injected {@link SwitchHost}.
|
|
924
1108
|
*
|
|
@@ -1064,15 +1248,63 @@ function createSwitchController(host, options = {}) {
|
|
|
1064
1248
|
/**
|
|
1065
1249
|
* The pure pre-flight decision. `native` always runs; an absent row (roster
|
|
1066
1250
|
* fetch failed, or the key is not listed) also runs — the machine's roster
|
|
1067
|
-
* probe is the authority on unknown targets, exactly as before C1.
|
|
1251
|
+
* probe is the authority on unknown targets, exactly as before C1. A
|
|
1252
|
+
* not-ready FEDERATED row (`machine` set and not `'local'`) decides
|
|
1253
|
+
* `'machine-down'` — an immediate actionable failure, never a wait loop
|
|
1254
|
+
* (the hub's probe already knows the machine is unreachable or its auth
|
|
1255
|
+
* expired); `failed` keeps precedence everywhere (a peer-reported spawn
|
|
1256
|
+
* rejection still reports as failed).
|
|
1068
1257
|
*/
|
|
1069
1258
|
function decidePreFlight(label, row) {
|
|
1070
1259
|
if (label === NATIVE_AGENT) return "run";
|
|
1071
1260
|
if (row === void 0) return "run";
|
|
1072
1261
|
if (row.failed === true) return "failed";
|
|
1073
|
-
if (row.ready !== true)
|
|
1262
|
+
if (row.ready !== true) {
|
|
1263
|
+
if (row.machine !== void 0 && row.machine !== "local") return "machine-down";
|
|
1264
|
+
return "wait";
|
|
1265
|
+
}
|
|
1074
1266
|
return "run";
|
|
1075
1267
|
}
|
|
1268
|
+
/** Bound on the machine-down branch's enrichment fetch (mesh P3 Task 5a:
|
|
1269
|
+
* the parked P2 minor — an unbounded `fetchMachines` await — folds here; a
|
|
1270
|
+
* hung hub answer must never swallow the toast). */
|
|
1271
|
+
const MACHINE_DOWN_ENRICH_MS = 3e3;
|
|
1272
|
+
/**
|
|
1273
|
+
* The machine-down branch's pure half (I1(c) feedback + I1(b) light probe,
|
|
1274
|
+
* mesh P3 Task 5a): enrich the toast through the machines face — BOUNDED at
|
|
1275
|
+
* {@link MACHINE_DOWN_ENRICH_MS} — then, after toasting, fire ONE reprobe
|
|
1276
|
+
* for the row's machine and forget it: a stale-unreachable machine gets a
|
|
1277
|
+
* fresh attempt per failed click, so the NEXT click can succeed without
|
|
1278
|
+
* waiting for the hub's 5-minute sweep. Never blocks the UI path (the whole
|
|
1279
|
+
* body is detached), never surfaces a reprobe failure (the sweep retries).
|
|
1280
|
+
*/
|
|
1281
|
+
function runMachineDownFeedback(label, row, faces) {
|
|
1282
|
+
(async () => {
|
|
1283
|
+
let machineName = row.machine;
|
|
1284
|
+
let status;
|
|
1285
|
+
const enriched = await new Promise((resolve) => {
|
|
1286
|
+
const bound$1 = faces.setTimeout(() => resolve(void 0), MACHINE_DOWN_ENRICH_MS);
|
|
1287
|
+
faces.fetchMachines().then((list) => {
|
|
1288
|
+
faces.clearTimeout(bound$1);
|
|
1289
|
+
resolve(list);
|
|
1290
|
+
}, () => {
|
|
1291
|
+
faces.clearTimeout(bound$1);
|
|
1292
|
+
resolve(void 0);
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
if (enriched !== void 0) {
|
|
1296
|
+
const found = enriched.machines.find((entry) => entry.id === row.machine);
|
|
1297
|
+
if (found !== void 0) {
|
|
1298
|
+
machineName = found.label;
|
|
1299
|
+
if (found.status === "unreachable" || found.status === "auth-expired") status = found.status;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const who = row.label ?? label;
|
|
1303
|
+
const machine = machineName ?? "its machine";
|
|
1304
|
+
faces.toast(status === void 0 ? `Super D: ${who} is unavailable — machine ${machine} is not ready. Reprobe or re-add it in the Hub machines panel.` : `Super D: ${who} is unavailable — machine ${machine} reports ${status}. Reprobe or re-add it in the Hub machines panel.`);
|
|
1305
|
+
if (row.machine !== void 0) faces.reprobe(row.machine).catch(() => {});
|
|
1306
|
+
})();
|
|
1307
|
+
}
|
|
1076
1308
|
/** Default bound on one readiness wait (the page never locks behind a stuck world). */
|
|
1077
1309
|
const WAIT_BOUND_MS = 12e4;
|
|
1078
1310
|
/** Default poll interval for one readiness wait. */
|
|
@@ -1200,14 +1432,61 @@ function uiState() {
|
|
|
1200
1432
|
function showToast(message) {
|
|
1201
1433
|
showSuperdToast(message);
|
|
1202
1434
|
}
|
|
1435
|
+
/**
|
|
1436
|
+
* I1(c) feedback + I1(b) light probe (mesh P3 Task 5a): the machine-down
|
|
1437
|
+
* branch delegates to the pure {@link runMachineDownFeedback} — the
|
|
1438
|
+
* machines face (already part of the client half — the Hub machines panel's
|
|
1439
|
+
* own cheap same-origin GET) enriches the toast, bounded at 3s; after
|
|
1440
|
+
* toasting, ONE fire-and-forget reprobe POST per failed click refreshes the
|
|
1441
|
+
* hub's status table so the NEXT click can succeed without waiting for the
|
|
1442
|
+
* 5-minute sweep. Nothing here blocks the UI path.
|
|
1443
|
+
*/
|
|
1444
|
+
function reportMachineDown(label, row) {
|
|
1445
|
+
runMachineDownFeedback(label, row, {
|
|
1446
|
+
fetchMachines,
|
|
1447
|
+
reprobe: reprobeMachine,
|
|
1448
|
+
toast: showToast,
|
|
1449
|
+
setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
|
|
1450
|
+
clearTimeout: (handle) => globalThis.clearTimeout(handle)
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Build the `SwitchHost.openDefaultWorkspace` action.
|
|
1455
|
+
*
|
|
1456
|
+
* dsh 0.2.0-rc.2 removed the request object from the Host's
|
|
1457
|
+
* `workspaces.initializeDefault` Remote: the first-use directory and stored
|
|
1458
|
+
* title are fixed and language-neutral (`default-workspace` under
|
|
1459
|
+
* `<Documents>/deepseek-harness`), so the caller passes only its own lifetime
|
|
1460
|
+
* signal. This module shipped the pre-0.2 `{ directoryName, title }` shape for
|
|
1461
|
+
* a release cycle; the Host then received a plain object where it feeds
|
|
1462
|
+
* `AbortSignal.any(...)`, EVERY empty-plane landing rejected, and the browser
|
|
1463
|
+
* surfaced "Unable to create default workspace". Root-caused 2026-10-02 on
|
|
1464
|
+
* the Hermes world — the only world whose registry was still empty after the
|
|
1465
|
+
* 0.2.0-rc.2 upgrade, hence the only one that took the default-workspace
|
|
1466
|
+
* branch. Exported so the call shape is unit-testable without the
|
|
1467
|
+
* module-level single-bind guard.
|
|
1468
|
+
*
|
|
1469
|
+
* @param deps - the workspace projection and the navigation face.
|
|
1470
|
+
* @param toast - the feedback sink; a refusal is feedback, never a revert.
|
|
1471
|
+
* @returns the host action.
|
|
1472
|
+
*/
|
|
1473
|
+
function openDefaultWorkspaceFor(deps, toast) {
|
|
1474
|
+
return () => {
|
|
1475
|
+
deps.workspaces.initializeDefault().then((prepared) => {
|
|
1476
|
+
if (prepared !== void 0) deps.uiWorkspace.openWorkspace(prepared.workspaceId);
|
|
1477
|
+
}).catch(() => {
|
|
1478
|
+
toast("Super D: unable to create the default workspace.");
|
|
1479
|
+
});
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1203
1482
|
/** The bound `switchAgent`, or `undefined` until the entry binds it. */
|
|
1204
1483
|
let bound;
|
|
1205
1484
|
/**
|
|
1206
1485
|
* Bind the page's switch agent from the client services. Idempotent; a no-op
|
|
1207
1486
|
* without the page atom (a non-fusion page never has one).
|
|
1208
|
-
* @param deps - the `connection`, `uiWorkspace`, `workspaces
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1487
|
+
* @param deps - the `connection`, `uiWorkspace`, `workspaces` and `sessions`
|
|
1488
|
+
* services (structural; the stock client tree always has all four —
|
|
1489
|
+
* upstream's ui-workspace injects the same set).
|
|
1211
1490
|
*/
|
|
1212
1491
|
function bindSwitchAgent(deps) {
|
|
1213
1492
|
if (bound !== void 0) return;
|
|
@@ -1228,19 +1507,6 @@ function bindSwitchAgent(deps) {
|
|
|
1228
1507
|
}
|
|
1229
1508
|
};
|
|
1230
1509
|
};
|
|
1231
|
-
const openDefaultWorkspace = () => {
|
|
1232
|
-
const language = deps.locale.getSnapshot().active.toLowerCase().split("-")[0];
|
|
1233
|
-
const title = language === "zh" ? "默认工作区" : "Default workspace";
|
|
1234
|
-
const directoryName = language === "zh" || language === "en" ? title : "default-workspace";
|
|
1235
|
-
deps.workspaces.initializeDefault({
|
|
1236
|
-
directoryName,
|
|
1237
|
-
title
|
|
1238
|
-
}).then((prepared) => {
|
|
1239
|
-
if (prepared !== void 0) deps.uiWorkspace.openWorkspace(prepared.workspaceId);
|
|
1240
|
-
}).catch(() => {
|
|
1241
|
-
showToast("Super D: unable to create the default workspace.");
|
|
1242
|
-
});
|
|
1243
|
-
};
|
|
1244
1510
|
const controller = createSwitchController({
|
|
1245
1511
|
setAgent(label) {
|
|
1246
1512
|
state.setAgent(label);
|
|
@@ -1255,7 +1521,7 @@ function bindSwitchAgent(deps) {
|
|
|
1255
1521
|
startSession(workspaceId) {
|
|
1256
1522
|
deps.uiWorkspace.startSession(workspaceId);
|
|
1257
1523
|
},
|
|
1258
|
-
openDefaultWorkspace,
|
|
1524
|
+
openDefaultWorkspace: openDefaultWorkspaceFor(deps, showToast),
|
|
1259
1525
|
readRestoreCaches,
|
|
1260
1526
|
onCachesRefreshed(listener, gate) {
|
|
1261
1527
|
return observeCachesRefreshed(deps.workspaces.list, {
|
|
@@ -1312,7 +1578,10 @@ let pendingWait;
|
|
|
1312
1578
|
* `label`, gated on the target world's readiness (C1): a not-ready world
|
|
1313
1579
|
* holds the UI on native with starting feedback and a bounded poll; a
|
|
1314
1580
|
* failed world reports and never switches; a ready/native/unknown target
|
|
1315
|
-
* runs the machine (whose roster probe stays the fallback).
|
|
1581
|
+
* runs the machine (whose roster probe stays the fallback). A not-ready
|
|
1582
|
+
* FEDERATED row skips all of that — an immediate machine-error toast
|
|
1583
|
+
* (I1(c), `machine-down`), never a wait loop.
|
|
1584
|
+
*
|
|
1316
1585
|
* @returns `true` when the fusion switch took the click over; `false` when
|
|
1317
1586
|
* this page is not a fusion page or the orchestration is unbound.
|
|
1318
1587
|
* Informational only — the interception contract (`seatClickDecision`)
|
|
@@ -1337,6 +1606,10 @@ function attemptSwitch(label) {
|
|
|
1337
1606
|
showToast(`Super D: ${label} failed to start — see the service log.`);
|
|
1338
1607
|
return;
|
|
1339
1608
|
}
|
|
1609
|
+
if (decision === "machine-down") {
|
|
1610
|
+
if (row !== void 0) reportMachineDown(label, row);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1340
1613
|
if (decision === "wait") {
|
|
1341
1614
|
showToast(`Super D: ${label} is starting…`);
|
|
1342
1615
|
const waiter = createReadinessWaiter({
|
|
@@ -1366,16 +1639,365 @@ function attemptSwitch(label) {
|
|
|
1366
1639
|
return true;
|
|
1367
1640
|
}
|
|
1368
1641
|
|
|
1642
|
+
//#endregion
|
|
1643
|
+
//#region src/client/agent-settings.ts
|
|
1644
|
+
const SETTINGS_PANELS = {
|
|
1645
|
+
agy: {
|
|
1646
|
+
namespace: "agyAuth",
|
|
1647
|
+
kind: "auth"
|
|
1648
|
+
},
|
|
1649
|
+
codex: {
|
|
1650
|
+
namespace: "codexProviders",
|
|
1651
|
+
kind: "providers"
|
|
1652
|
+
},
|
|
1653
|
+
"codex-sdk": {
|
|
1654
|
+
namespace: "codexProviders",
|
|
1655
|
+
kind: "providers"
|
|
1656
|
+
},
|
|
1657
|
+
claude: {
|
|
1658
|
+
namespace: "claudeProviders",
|
|
1659
|
+
kind: "providers"
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
/** The panel spec for a runtime, or `undefined` when it has none yet. */
|
|
1663
|
+
function settingsPanelFor(agentKey) {
|
|
1664
|
+
return SETTINGS_PANELS[agentKey];
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Accordion transition: at most one row open. Clicking the open row closes it;
|
|
1668
|
+
* clicking another moves the panel there in one click.
|
|
1669
|
+
*/
|
|
1670
|
+
function toggleExpanded(current$1, key) {
|
|
1671
|
+
return current$1 === key ? null : key;
|
|
1672
|
+
}
|
|
1673
|
+
function str(value) {
|
|
1674
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
1675
|
+
}
|
|
1676
|
+
function methodOption(raw) {
|
|
1677
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1678
|
+
const record = raw;
|
|
1679
|
+
const key = str(record.key);
|
|
1680
|
+
const label = str(record.label);
|
|
1681
|
+
if (key === void 0 || label === void 0) return void 0;
|
|
1682
|
+
return {
|
|
1683
|
+
key,
|
|
1684
|
+
label,
|
|
1685
|
+
available: record.available === true,
|
|
1686
|
+
placeholder: record.placeholder === true,
|
|
1687
|
+
hint: str(record.hint) ?? ""
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Defensive parse of the RPC answer. Returns `undefined` for anything that is
|
|
1692
|
+
* not a usable view (no methods, wrong shape, adapter absent) so the panel can
|
|
1693
|
+
* render an explicit failure instead of an empty list that looks like success.
|
|
1694
|
+
*/
|
|
1695
|
+
function parseAuthView(raw) {
|
|
1696
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1697
|
+
const record = raw;
|
|
1698
|
+
if (!Array.isArray(record.methods)) return void 0;
|
|
1699
|
+
const methods = record.methods.map(methodOption).filter((m) => m !== void 0);
|
|
1700
|
+
if (methods.length === 0) return void 0;
|
|
1701
|
+
const settingsPath = str(record.settingsPath);
|
|
1702
|
+
const note = str(record.note);
|
|
1703
|
+
return {
|
|
1704
|
+
methods,
|
|
1705
|
+
selected: str(record.selected) ?? null,
|
|
1706
|
+
effective: str(record.effective) ?? null,
|
|
1707
|
+
...settingsPath === void 0 ? {} : { settingsPath },
|
|
1708
|
+
...note === void 0 ? {} : { note }
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* The option the panel marks as in force: the effective one when the adapter
|
|
1713
|
+
* named it, else the explicit selection. `undefined` when neither is known —
|
|
1714
|
+
* the panel then marks nothing rather than guessing.
|
|
1715
|
+
*/
|
|
1716
|
+
function activeMethod(view) {
|
|
1717
|
+
const wanted = view.effective ?? view.selected;
|
|
1718
|
+
if (wanted === null) return void 0;
|
|
1719
|
+
return view.methods.find((method) => method.key === wanted);
|
|
1720
|
+
}
|
|
1721
|
+
/** Whether an option may be picked (placeholders are rendered, never selected). */
|
|
1722
|
+
function selectable(option) {
|
|
1723
|
+
return !option.placeholder;
|
|
1724
|
+
}
|
|
1725
|
+
function nonEmpty(value) {
|
|
1726
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
1727
|
+
}
|
|
1728
|
+
function modelOption(raw) {
|
|
1729
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1730
|
+
const record = raw;
|
|
1731
|
+
const id = nonEmpty(record.id);
|
|
1732
|
+
if (id === void 0) return void 0;
|
|
1733
|
+
const contextWindow = record.contextWindow;
|
|
1734
|
+
return {
|
|
1735
|
+
id,
|
|
1736
|
+
label: nonEmpty(record.displayName) ?? id,
|
|
1737
|
+
...typeof contextWindow === "number" && Number.isSafeInteger(contextWindow) && contextWindow > 0 ? { contextWindow } : {}
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
function providerAuthKind(raw) {
|
|
1741
|
+
return raw === "env" ? "env" : raw === "authToken" ? "authToken" : "apiKey";
|
|
1742
|
+
}
|
|
1743
|
+
function providerOption(raw) {
|
|
1744
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1745
|
+
const record = raw;
|
|
1746
|
+
const id = nonEmpty(record.id);
|
|
1747
|
+
const label = nonEmpty(record.label);
|
|
1748
|
+
const baseUrl = nonEmpty(record.baseUrl);
|
|
1749
|
+
if (id === void 0 || label === void 0 || baseUrl === void 0) return void 0;
|
|
1750
|
+
const models = Array.isArray(record.models) ? record.models.map(modelOption).filter((entry) => entry !== void 0) : [];
|
|
1751
|
+
const envName = nonEmpty(record.envName);
|
|
1752
|
+
return {
|
|
1753
|
+
id,
|
|
1754
|
+
label,
|
|
1755
|
+
baseUrl,
|
|
1756
|
+
protocol: record.protocol === "chat" ? "chat" : "responses",
|
|
1757
|
+
models,
|
|
1758
|
+
authKind: providerAuthKind(record.authKind),
|
|
1759
|
+
...envName === void 0 ? {} : { envName },
|
|
1760
|
+
hasKey: record.hasKey === true
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
function protocolOption(raw) {
|
|
1764
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1765
|
+
const record = raw;
|
|
1766
|
+
const value = nonEmpty(record.value);
|
|
1767
|
+
const label = nonEmpty(record.label);
|
|
1768
|
+
if (value === void 0 || label === void 0) return void 0;
|
|
1769
|
+
return {
|
|
1770
|
+
value,
|
|
1771
|
+
label,
|
|
1772
|
+
available: record.available === true,
|
|
1773
|
+
hint: nonEmpty(record.hint) ?? ""
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
function providerPaths(raw) {
|
|
1777
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1778
|
+
const record = raw;
|
|
1779
|
+
const store = nonEmpty(record.store);
|
|
1780
|
+
if (store === void 0) return void 0;
|
|
1781
|
+
const config = nonEmpty(record.config);
|
|
1782
|
+
const auth = nonEmpty(record.auth);
|
|
1783
|
+
const catalogs = nonEmpty(record.catalogs);
|
|
1784
|
+
const settings = nonEmpty(record.settings);
|
|
1785
|
+
return {
|
|
1786
|
+
store,
|
|
1787
|
+
...config === void 0 ? {} : { config },
|
|
1788
|
+
...auth === void 0 ? {} : { auth },
|
|
1789
|
+
...catalogs === void 0 ? {} : { catalogs },
|
|
1790
|
+
...settings === void 0 ? {} : { settings }
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Defensive parse of the adapter's providers answer.
|
|
1795
|
+
*
|
|
1796
|
+
* `undefined` for anything that is not a usable view — but an empty `providers`
|
|
1797
|
+
* array is valid (a fresh install), and so is a provider whose `models` array
|
|
1798
|
+
* is empty. `protocols` is the codex face's field: an ABSENT field (the claude
|
|
1799
|
+
* face) reads as "no protocol choice" and never fails the parse, while a
|
|
1800
|
+
* present-but-unusable list is a malformed codex answer and rejects as before.
|
|
1801
|
+
*/
|
|
1802
|
+
function parseProvidersView(raw) {
|
|
1803
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1804
|
+
const record = raw;
|
|
1805
|
+
if (!Array.isArray(record.providers)) return void 0;
|
|
1806
|
+
let protocols = [];
|
|
1807
|
+
if (record.protocols !== void 0) {
|
|
1808
|
+
if (!Array.isArray(record.protocols)) return void 0;
|
|
1809
|
+
protocols = record.protocols.map(protocolOption).filter((entry) => entry !== void 0);
|
|
1810
|
+
if (protocols.length === 0) return void 0;
|
|
1811
|
+
}
|
|
1812
|
+
const paths = providerPaths(record.paths);
|
|
1813
|
+
if (paths === void 0) return void 0;
|
|
1814
|
+
const providers = record.providers.map(providerOption).filter((entry) => entry !== void 0);
|
|
1815
|
+
const syncedAt = record.syncedAt;
|
|
1816
|
+
const note = nonEmpty(record.note);
|
|
1817
|
+
return {
|
|
1818
|
+
providers,
|
|
1819
|
+
native: nativeConfig(record.native),
|
|
1820
|
+
protocols,
|
|
1821
|
+
paths,
|
|
1822
|
+
...typeof syncedAt === "number" && Number.isFinite(syncedAt) ? { syncedAt } : {},
|
|
1823
|
+
...note === void 0 ? {} : { note }
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* The user's own native config as the adapter last read it. `null` when the
|
|
1828
|
+
* adapter said nothing; otherwise an object carrying whichever halves were
|
|
1829
|
+
* named — codex names a provider id, claude an endpoint URL — and an empty
|
|
1830
|
+
* one is still news (the panel says the file names nothing).
|
|
1831
|
+
*/
|
|
1832
|
+
function nativeConfig(raw) {
|
|
1833
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
1834
|
+
const record = raw;
|
|
1835
|
+
const providerId = nonEmpty(record.providerId);
|
|
1836
|
+
const baseUrl = nonEmpty(record.baseUrl);
|
|
1837
|
+
const model = nonEmpty(record.model);
|
|
1838
|
+
return {
|
|
1839
|
+
...providerId === void 0 ? {} : { providerId },
|
|
1840
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
1841
|
+
...model === void 0 ? {} : { model }
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
/** The codex face: a stored key, or the name of a variable holding it. */
|
|
1845
|
+
const KEY_OR_ENV = [{
|
|
1846
|
+
value: "apiKey",
|
|
1847
|
+
label: "API key",
|
|
1848
|
+
hint: "Stored with the provider; injected at launch."
|
|
1849
|
+
}, {
|
|
1850
|
+
value: "env",
|
|
1851
|
+
label: "Environment variable",
|
|
1852
|
+
hint: "The child reads the key from a named variable."
|
|
1853
|
+
}];
|
|
1854
|
+
/** The claude face: both kinds are literal keys, differing only in the header. */
|
|
1855
|
+
const TWO_HEADER_SHAPES = [{
|
|
1856
|
+
value: "apiKey",
|
|
1857
|
+
label: "API key (x-api-key)",
|
|
1858
|
+
hint: "Sent as the x-api-key header."
|
|
1859
|
+
}, {
|
|
1860
|
+
value: "authToken",
|
|
1861
|
+
label: "Bearer token",
|
|
1862
|
+
hint: "Sent as the Authorization header."
|
|
1863
|
+
}];
|
|
1864
|
+
/**
|
|
1865
|
+
* The auth shapes the add/edit form may pick for a provider, in display order.
|
|
1866
|
+
*
|
|
1867
|
+
* The view carries no explicit vocabulary field, so the panel reads it off the
|
|
1868
|
+
* one field that distinguishes the faces today: `protocols`. The codex face
|
|
1869
|
+
* advertises wire protocols and offers key-or-env-var; the claude face carries
|
|
1870
|
+
* neither and offers its two header shapes instead. The list is per-view,
|
|
1871
|
+
* never a fixed union — each adapter rejects the other's kinds on save, so a
|
|
1872
|
+
* union dropdown would miscode silently. A third face extends the table.
|
|
1873
|
+
*/
|
|
1874
|
+
function authKindOptions(view) {
|
|
1875
|
+
return view.protocols.length > 0 ? KEY_OR_ENV : TWO_HEADER_SHAPES;
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* `provider / model` (codex) or `endpoint / model` (claude) — whichever halves
|
|
1879
|
+
* the user's own native config names, joined for the one-line fact the panel
|
|
1880
|
+
* states and never acts on.
|
|
1881
|
+
*/
|
|
1882
|
+
function nativeDescription(native) {
|
|
1883
|
+
const parts = [native?.providerId ?? native?.baseUrl, native?.model].filter((part) => part !== void 0);
|
|
1884
|
+
return parts.length === 0 ? "no provider" : parts.join(" / ");
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* The non-store paths joined for the panel's second footer line, in a fixed
|
|
1888
|
+
* slot order. `undefined` when the face named none — the line is omitted
|
|
1889
|
+
* entirely rather than rendered with blank slots.
|
|
1890
|
+
*/
|
|
1891
|
+
function nativePathsLine(paths) {
|
|
1892
|
+
const parts = [
|
|
1893
|
+
paths.config,
|
|
1894
|
+
paths.auth,
|
|
1895
|
+
paths.catalogs,
|
|
1896
|
+
paths.settings
|
|
1897
|
+
].filter((part) => part !== void 0);
|
|
1898
|
+
return parts.length === 0 ? void 0 : parts.join(" · ");
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
//#endregion
|
|
1902
|
+
//#region src/client/runtime-settings-api.ts
|
|
1903
|
+
/** One rpcId for request/response correlation. `randomUUID` needs a secure context. */
|
|
1904
|
+
function mintRpcId() {
|
|
1905
|
+
const webCrypto = globalThis.crypto;
|
|
1906
|
+
if (webCrypto !== void 0 && typeof webCrypto.randomUUID === "function") return webCrypto.randomUUID();
|
|
1907
|
+
if (webCrypto !== void 0 && typeof webCrypto.getRandomValues === "function") return [...webCrypto.getRandomValues(new Uint8Array(16))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1908
|
+
return `rpc-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1909
|
+
}
|
|
1910
|
+
function settingsEnvelope(namespace, op, args) {
|
|
1911
|
+
return {
|
|
1912
|
+
type: "client-request",
|
|
1913
|
+
rpcId: mintRpcId(),
|
|
1914
|
+
method: `${namespace}/${op}`,
|
|
1915
|
+
payload: { args }
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
/** What a caller can do with a failed panel call — kept structural for the tests. */
|
|
1919
|
+
function messageOf(error) {
|
|
1920
|
+
if (error !== null && typeof error === "object" && typeof error.message === "string") return error.message;
|
|
1921
|
+
return String(error);
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* One unary call at a runtime's world, unwrapped to the RPC's value.
|
|
1925
|
+
*
|
|
1926
|
+
* Every failure mode is reported as a thrown `Error` carrying the adapter's own
|
|
1927
|
+
* message where one exists: a transport failure, a malformed answer, and the
|
|
1928
|
+
* gateway's business error (`{ok:false,error}`) must not look alike to the panel.
|
|
1929
|
+
*
|
|
1930
|
+
* @param agentKey - roster key addressing the world (`?agent=`).
|
|
1931
|
+
* @param namespace - the adapter's Remote namespace (`agyAuth`).
|
|
1932
|
+
* @param op - the Remote method.
|
|
1933
|
+
* @param args - exact named wire arguments.
|
|
1934
|
+
* @param doFetch - fetch implementation (tests inject).
|
|
1935
|
+
* @returns the raw RPC value.
|
|
1936
|
+
* @throws {Error} on transport, framing, or gateway failure.
|
|
1937
|
+
*/
|
|
1938
|
+
async function callRuntimeSettings(agentKey, namespace, op, args, doFetch) {
|
|
1939
|
+
const send = doFetch ?? ((input, init) => fetch(input, init));
|
|
1940
|
+
const envelope = settingsEnvelope(namespace, op, args);
|
|
1941
|
+
const response = await send(`/hub/api?agent=${encodeURIComponent(agentKey)}`, {
|
|
1942
|
+
method: "POST",
|
|
1943
|
+
credentials: "same-origin",
|
|
1944
|
+
headers: { "content-type": "application/json" },
|
|
1945
|
+
body: JSON.stringify(envelope)
|
|
1946
|
+
});
|
|
1947
|
+
const text = await response.text();
|
|
1948
|
+
let body;
|
|
1949
|
+
try {
|
|
1950
|
+
body = JSON.parse(text);
|
|
1951
|
+
} catch {
|
|
1952
|
+
throw new Error(`settings ${envelope.method}: unreadable answer (HTTP ${response.status})`);
|
|
1953
|
+
}
|
|
1954
|
+
const result = body?.result;
|
|
1955
|
+
if (result === null || typeof result !== "object") throw new Error(`settings ${envelope.method}: malformed answer (HTTP ${response.status})`);
|
|
1956
|
+
const record = result;
|
|
1957
|
+
if (record.ok === true) return record.value;
|
|
1958
|
+
const error = record.error;
|
|
1959
|
+
if (error !== void 0 && typeof error.message === "string") throw new Error(error.message);
|
|
1960
|
+
throw new Error(`settings ${envelope.method}: failed (HTTP ${response.status})`);
|
|
1961
|
+
}
|
|
1962
|
+
/** Read one runtime's panel state, parsed. */
|
|
1963
|
+
async function fetchAuthView(agentKey, namespace, doFetch) {
|
|
1964
|
+
const view = parseAuthView(await callRuntimeSettings(agentKey, namespace, "get", {}, doFetch));
|
|
1965
|
+
if (view === void 0) throw new Error("settings: the runtime returned no usable auth options");
|
|
1966
|
+
return view;
|
|
1967
|
+
}
|
|
1968
|
+
/** Adopt one auth form (`null` clears the explicit choice). */
|
|
1969
|
+
async function setAuthMode(agentKey, namespace, mode, doFetch) {
|
|
1970
|
+
const view = parseAuthView(await callRuntimeSettings(agentKey, namespace, "set", { request: { mode } }, doFetch));
|
|
1971
|
+
if (view === void 0) throw new Error("settings: the runtime returned no usable auth options");
|
|
1972
|
+
return view;
|
|
1973
|
+
}
|
|
1974
|
+
function requireProvidersView(value) {
|
|
1975
|
+
const view = parseProvidersView(value);
|
|
1976
|
+
if (view === void 0) throw new Error("settings: the runtime returned no usable provider list");
|
|
1977
|
+
return view;
|
|
1978
|
+
}
|
|
1979
|
+
/** Read the multi-provider state of one runtime. */
|
|
1980
|
+
async function fetchProvidersView(agentKey, namespace, doFetch) {
|
|
1981
|
+
return requireProvidersView(await callRuntimeSettings(agentKey, namespace, "get", {}, doFetch));
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Add or edit one provider in the adapter's list. Which provider a session runs
|
|
1985
|
+
* on is DSH's own per-session model selection, so this touches no active half.
|
|
1986
|
+
*/
|
|
1987
|
+
async function saveProvider(agentKey, namespace, provider, doFetch) {
|
|
1988
|
+
return requireProvidersView(await callRuntimeSettings(agentKey, namespace, "save", { request: { provider } }, doFetch));
|
|
1989
|
+
}
|
|
1990
|
+
/** Drop one provider from the list. Does not rewrite the runtime's own config. */
|
|
1991
|
+
async function removeProvider(agentKey, namespace, id, doFetch) {
|
|
1992
|
+
return requireProvidersView(await callRuntimeSettings(agentKey, namespace, "remove", { request: { id } }, doFetch));
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1369
1995
|
//#endregion
|
|
1370
1996
|
//#region src/client/HubPage.tsx
|
|
1371
1997
|
const EMPTY_DRAFT = {
|
|
1372
1998
|
name: "",
|
|
1373
|
-
|
|
1999
|
+
tokenUrl: ""
|
|
1374
2000
|
};
|
|
1375
|
-
/** Chip text: drop the trailing slash for concision ('/claude/' → '/claude'). */
|
|
1376
|
-
function displayPath(path) {
|
|
1377
|
-
return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
1378
|
-
}
|
|
1379
2001
|
/** Shared chip look (status pills, path tags). */
|
|
1380
2002
|
const chip = {
|
|
1381
2003
|
fontSize: 10,
|
|
@@ -1394,6 +2016,144 @@ const chipPending = {
|
|
|
1394
2016
|
...chip,
|
|
1395
2017
|
color: "var(--dsw-alias-label-caption)"
|
|
1396
2018
|
};
|
|
2019
|
+
const chipWarn = {
|
|
2020
|
+
...chip,
|
|
2021
|
+
color: "var(--dsw-alias-state-warn-primary)"
|
|
2022
|
+
};
|
|
2023
|
+
const chipError = {
|
|
2024
|
+
...chip,
|
|
2025
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
2026
|
+
};
|
|
2027
|
+
/** Machine id chip (the m4): the monospace chip look the roster's path tags use. */
|
|
2028
|
+
const chipMonoId = {
|
|
2029
|
+
...chip,
|
|
2030
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
2031
|
+
fontSize: 10.5
|
|
2032
|
+
};
|
|
2033
|
+
/** The status dot's fixed part (the color is decided per row). */
|
|
2034
|
+
const dotStyle = {
|
|
2035
|
+
width: 8,
|
|
2036
|
+
height: 8,
|
|
2037
|
+
borderRadius: "50%",
|
|
2038
|
+
flex: "none"
|
|
2039
|
+
};
|
|
2040
|
+
/** The machine origin column: monospace, eats the leftover row width. */
|
|
2041
|
+
const originStyle = {
|
|
2042
|
+
flex: 1,
|
|
2043
|
+
minWidth: 0,
|
|
2044
|
+
overflow: "hidden",
|
|
2045
|
+
textOverflow: "ellipsis",
|
|
2046
|
+
whiteSpace: "nowrap",
|
|
2047
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
2048
|
+
fontSize: 12,
|
|
2049
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
2050
|
+
};
|
|
2051
|
+
/**
|
|
2052
|
+
* The row's switch control (P3: the `switch` word is a real BUTTON now — the
|
|
2053
|
+
* row body toggles the settings panel instead, so the switch needs its own
|
|
2054
|
+
* hit target that stops propagation).
|
|
2055
|
+
*/
|
|
2056
|
+
const switchButtonStyle = {
|
|
2057
|
+
border: "none",
|
|
2058
|
+
background: "transparent",
|
|
2059
|
+
padding: 0,
|
|
2060
|
+
font: "inherit",
|
|
2061
|
+
fontSize: 12.5,
|
|
2062
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
2063
|
+
whiteSpace: "nowrap"
|
|
2064
|
+
};
|
|
2065
|
+
/** The inline settings panel a row body expands into (P3 accordion). */
|
|
2066
|
+
const panelStyle = {
|
|
2067
|
+
listStyle: "none",
|
|
2068
|
+
margin: "0 -8px 4px",
|
|
2069
|
+
padding: "10px 12px 12px 42px",
|
|
2070
|
+
borderRadius: 12,
|
|
2071
|
+
background: "var(--dsw-alias-interactive-bg-hover)",
|
|
2072
|
+
fontSize: 12.5
|
|
2073
|
+
};
|
|
2074
|
+
/** The icon action (remove ✕) seat. */
|
|
2075
|
+
const iconActionStyle = {
|
|
2076
|
+
display: "inline-flex",
|
|
2077
|
+
alignItems: "center",
|
|
2078
|
+
justifyContent: "center",
|
|
2079
|
+
width: 22,
|
|
2080
|
+
height: 22,
|
|
2081
|
+
borderRadius: 6,
|
|
2082
|
+
border: "none",
|
|
2083
|
+
background: "transparent",
|
|
2084
|
+
color: "var(--dsw-alias-label-caption)",
|
|
2085
|
+
font: "inherit",
|
|
2086
|
+
cursor: "pointer"
|
|
2087
|
+
};
|
|
2088
|
+
/** The quiet text action (reprobe) — the switch/row-action look. */
|
|
2089
|
+
const textActionStyle = {
|
|
2090
|
+
border: "none",
|
|
2091
|
+
background: "transparent",
|
|
2092
|
+
padding: 0,
|
|
2093
|
+
font: "inherit",
|
|
2094
|
+
fontSize: 12.5,
|
|
2095
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
2096
|
+
cursor: "pointer",
|
|
2097
|
+
whiteSpace: "nowrap"
|
|
2098
|
+
};
|
|
2099
|
+
/** The row's settings panel heading. */
|
|
2100
|
+
const panelHeadStyle = {
|
|
2101
|
+
margin: "0 0 6px",
|
|
2102
|
+
fontSize: 12,
|
|
2103
|
+
fontWeight: 500,
|
|
2104
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
2105
|
+
};
|
|
2106
|
+
/** One auth-method option's row in the panel. */
|
|
2107
|
+
const optionStyle = {
|
|
2108
|
+
display: "flex",
|
|
2109
|
+
alignItems: "baseline",
|
|
2110
|
+
gap: 8,
|
|
2111
|
+
width: "100%",
|
|
2112
|
+
textAlign: "left",
|
|
2113
|
+
border: "none",
|
|
2114
|
+
background: "transparent",
|
|
2115
|
+
padding: "5px 6px",
|
|
2116
|
+
borderRadius: 8,
|
|
2117
|
+
font: "inherit",
|
|
2118
|
+
fontSize: 12.5,
|
|
2119
|
+
color: "var(--dsw-alias-label-primary)"
|
|
2120
|
+
};
|
|
2121
|
+
/** The status dot's color: reachable green, pending grey, unreachable amber, auth-expired red. */
|
|
2122
|
+
function statusDotColor(status) {
|
|
2123
|
+
if (status === "reachable") return "var(--dsw-alias-state-success-primary)";
|
|
2124
|
+
if (status === "auth-expired") return "var(--dsw-alias-state-error-primary)";
|
|
2125
|
+
if (status === "unreachable") return "var(--dsw-alias-state-warn-primary)";
|
|
2126
|
+
return "var(--dsw-alias-label-caption)";
|
|
2127
|
+
}
|
|
2128
|
+
/** The last-fetch caption's style (R7): quiet tertiary text beside the status chip. */
|
|
2129
|
+
const ageStyle = {
|
|
2130
|
+
fontSize: 11.5,
|
|
2131
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
2132
|
+
whiteSpace: "nowrap"
|
|
2133
|
+
};
|
|
2134
|
+
/** The status chip (style + text) for one remote row. */
|
|
2135
|
+
function statusChip(machine) {
|
|
2136
|
+
if (machine.status === "reachable") return {
|
|
2137
|
+
style: chipSelf,
|
|
2138
|
+
text: "reachable"
|
|
2139
|
+
};
|
|
2140
|
+
if (machine.status === "auth-expired") return {
|
|
2141
|
+
style: chipError,
|
|
2142
|
+
text: "auth expired"
|
|
2143
|
+
};
|
|
2144
|
+
if (machine.status === "unreachable") return {
|
|
2145
|
+
style: chipWarn,
|
|
2146
|
+
text: "unreachable"
|
|
2147
|
+
};
|
|
2148
|
+
return {
|
|
2149
|
+
style: chipPending,
|
|
2150
|
+
text: "pending"
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
/** One line of error text for the panel's inline error line. */
|
|
2154
|
+
function errText(err) {
|
|
2155
|
+
return err instanceof Error ? err.message : String(err);
|
|
2156
|
+
}
|
|
1397
2157
|
const inputStyle = {
|
|
1398
2158
|
boxSizing: "border-box",
|
|
1399
2159
|
width: "100%",
|
|
@@ -1420,11 +2180,27 @@ const listStyle = {
|
|
|
1420
2180
|
flexDirection: "column",
|
|
1421
2181
|
gap: 2
|
|
1422
2182
|
};
|
|
1423
|
-
/**
|
|
1424
|
-
|
|
2183
|
+
/** The machine section head inside the roster list (the grouped shape's caption row). */
|
|
2184
|
+
const sectionHeadStyle = {
|
|
2185
|
+
padding: "8px 8px 2px",
|
|
2186
|
+
fontSize: 11,
|
|
2187
|
+
lineHeight: "16px",
|
|
2188
|
+
color: "var(--dsw-alias-label-caption)",
|
|
2189
|
+
whiteSpace: "nowrap",
|
|
2190
|
+
listStyle: "none"
|
|
2191
|
+
};
|
|
2192
|
+
/**
|
|
2193
|
+
* A row that lights its surface on hover (inline styles have no :hover).
|
|
2194
|
+
*
|
|
2195
|
+
* P3: the row BODY is the settings-panel toggle, not the switch — `expanded`
|
|
2196
|
+
* keeps the surface lit while its panel is open, so the open row stays
|
|
2197
|
+
* identifiable after the pointer leaves. The switch lives in the row's button.
|
|
2198
|
+
*/
|
|
2199
|
+
function HoverRow({ children, onClick, clickable, expanded }) {
|
|
1425
2200
|
const [hover, setHover] = react.useState(false);
|
|
1426
2201
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
1427
2202
|
onClick,
|
|
2203
|
+
"aria-expanded": expanded,
|
|
1428
2204
|
onMouseEnter: () => {
|
|
1429
2205
|
setHover(true);
|
|
1430
2206
|
},
|
|
@@ -1439,7 +2215,7 @@ function HoverRow({ children, onClick, clickable }) {
|
|
|
1439
2215
|
padding: "9px 8px",
|
|
1440
2216
|
borderRadius: 12,
|
|
1441
2217
|
listStyle: "none",
|
|
1442
|
-
background: hover ? "var(--dsw-alias-interactive-bg-hover)" : "transparent",
|
|
2218
|
+
background: hover || expanded ? "var(--dsw-alias-interactive-bg-hover)" : "transparent",
|
|
1443
2219
|
cursor: clickable ? "pointer" : void 0
|
|
1444
2220
|
},
|
|
1445
2221
|
children
|
|
@@ -1476,8 +2252,693 @@ function GroupHead({ title, count, action }) {
|
|
|
1476
2252
|
]
|
|
1477
2253
|
});
|
|
1478
2254
|
}
|
|
2255
|
+
/**
|
|
2256
|
+
* One runtime's settings, rendered inside its expanded row (P3/P4).
|
|
2257
|
+
*
|
|
2258
|
+
* The adapter owns the config, so this component owns no config knowledge: each
|
|
2259
|
+
* body asks the adapter's Remote and renders what comes back. The round trip
|
|
2260
|
+
* after a change is authoritative — the adapter re-reads the machine and
|
|
2261
|
+
* answers with the new state, which is why nothing is optimistically patched
|
|
2262
|
+
* here (a selection can fall through a ladder, or a switch can leave a provider
|
|
2263
|
+
* the runtime is still running).
|
|
2264
|
+
*
|
|
2265
|
+
* Mounted only while its row is open, so expanding always re-reads the
|
|
2266
|
+
* machine rather than replaying a stale first read.
|
|
2267
|
+
*/
|
|
2268
|
+
function RuntimeSettingsPanel({ agent }) {
|
|
2269
|
+
const panel = settingsPanelFor(agent.key);
|
|
2270
|
+
if (panel === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2271
|
+
style: {
|
|
2272
|
+
margin: 0,
|
|
2273
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2274
|
+
},
|
|
2275
|
+
children: "No settings for this runtime yet."
|
|
2276
|
+
});
|
|
2277
|
+
return panel.kind === "providers" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidersPanel, {
|
|
2278
|
+
agent,
|
|
2279
|
+
namespace: panel.namespace
|
|
2280
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuthSettingsPanel, {
|
|
2281
|
+
agent,
|
|
2282
|
+
namespace: panel.namespace
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* The sign-in-form body (Antigravity): one form picked from the adapter's
|
|
2287
|
+
* ladder. `namespace` always resolves — the row would not mount this body
|
|
2288
|
+
* otherwise.
|
|
2289
|
+
*/
|
|
2290
|
+
function AuthSettingsPanel({ agent, namespace }) {
|
|
2291
|
+
const [view, setView] = react.useState(void 0);
|
|
2292
|
+
const [error, setError] = react.useState(null);
|
|
2293
|
+
const [busy, setBusy] = react.useState(null);
|
|
2294
|
+
const [loading, setLoading] = react.useState(true);
|
|
2295
|
+
react.useEffect(() => {
|
|
2296
|
+
let live = true;
|
|
2297
|
+
setLoading(true);
|
|
2298
|
+
fetchAuthView(agent.key, namespace).then((next) => {
|
|
2299
|
+
if (live) {
|
|
2300
|
+
setView(next);
|
|
2301
|
+
setError(null);
|
|
2302
|
+
}
|
|
2303
|
+
}).catch((err) => {
|
|
2304
|
+
if (live) {
|
|
2305
|
+
setView(void 0);
|
|
2306
|
+
setError(messageOf(err));
|
|
2307
|
+
}
|
|
2308
|
+
}).finally(() => {
|
|
2309
|
+
if (live) setLoading(false);
|
|
2310
|
+
});
|
|
2311
|
+
return () => {
|
|
2312
|
+
live = false;
|
|
2313
|
+
};
|
|
2314
|
+
}, [agent.key, namespace]);
|
|
2315
|
+
const choose = (mode) => {
|
|
2316
|
+
if (busy !== null) return;
|
|
2317
|
+
setBusy(mode);
|
|
2318
|
+
setAuthMode(agent.key, namespace, mode).then((next) => {
|
|
2319
|
+
setView(next);
|
|
2320
|
+
setError(null);
|
|
2321
|
+
}).catch((err) => {
|
|
2322
|
+
setError(messageOf(err));
|
|
2323
|
+
}).finally(() => {
|
|
2324
|
+
setBusy(null);
|
|
2325
|
+
});
|
|
2326
|
+
};
|
|
2327
|
+
if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2328
|
+
style: {
|
|
2329
|
+
margin: 0,
|
|
2330
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2331
|
+
},
|
|
2332
|
+
children: "Loading settings…"
|
|
2333
|
+
});
|
|
2334
|
+
if (view === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2335
|
+
style: {
|
|
2336
|
+
margin: 0,
|
|
2337
|
+
color: "var(--dsw-alias-state-warn-label, var(--dsw-alias-label-secondary))"
|
|
2338
|
+
},
|
|
2339
|
+
children: error ?? "Settings are unavailable for this runtime."
|
|
2340
|
+
});
|
|
2341
|
+
const current$1 = activeMethod(view);
|
|
2342
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2343
|
+
style: {
|
|
2344
|
+
display: "flex",
|
|
2345
|
+
flexDirection: "column",
|
|
2346
|
+
gap: 4
|
|
2347
|
+
},
|
|
2348
|
+
children: [
|
|
2349
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2350
|
+
style: panelHeadStyle,
|
|
2351
|
+
children: "Sign-in method"
|
|
2352
|
+
}),
|
|
2353
|
+
view.methods.map((option) => {
|
|
2354
|
+
const isCurrent = current$1?.key === option.key;
|
|
2355
|
+
const disabled = !selectable(option) || busy !== null;
|
|
2356
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2357
|
+
type: "button",
|
|
2358
|
+
role: "radio",
|
|
2359
|
+
"aria-checked": isCurrent,
|
|
2360
|
+
disabled,
|
|
2361
|
+
onClick: () => {
|
|
2362
|
+
choose(option.key);
|
|
2363
|
+
},
|
|
2364
|
+
style: {
|
|
2365
|
+
...optionStyle,
|
|
2366
|
+
color: disabled ? "var(--dsw-alias-label-tertiary)" : "var(--dsw-alias-label-primary)",
|
|
2367
|
+
cursor: disabled ? "default" : "pointer",
|
|
2368
|
+
background: isCurrent ? "var(--dsw-alias-bg-elevated, transparent)" : "transparent"
|
|
2369
|
+
},
|
|
2370
|
+
children: [
|
|
2371
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2372
|
+
"aria-hidden": true,
|
|
2373
|
+
style: {
|
|
2374
|
+
width: 12,
|
|
2375
|
+
flex: "0 0 12px"
|
|
2376
|
+
},
|
|
2377
|
+
children: isCurrent ? "●" : "○"
|
|
2378
|
+
}),
|
|
2379
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2380
|
+
style: {
|
|
2381
|
+
fontWeight: 500,
|
|
2382
|
+
whiteSpace: "nowrap"
|
|
2383
|
+
},
|
|
2384
|
+
children: option.label
|
|
2385
|
+
}),
|
|
2386
|
+
isCurrent && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2387
|
+
style: chipSelf,
|
|
2388
|
+
children: "in force"
|
|
2389
|
+
}),
|
|
2390
|
+
!option.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2391
|
+
style: chip,
|
|
2392
|
+
children: "not detected"
|
|
2393
|
+
}),
|
|
2394
|
+
option.placeholder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2395
|
+
style: chip,
|
|
2396
|
+
children: "placeholder"
|
|
2397
|
+
}),
|
|
2398
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2399
|
+
style: {
|
|
2400
|
+
flex: 1,
|
|
2401
|
+
minWidth: 0,
|
|
2402
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2403
|
+
},
|
|
2404
|
+
children: option.hint
|
|
2405
|
+
}),
|
|
2406
|
+
busy === option.key && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2407
|
+
style: chip,
|
|
2408
|
+
children: "applying…"
|
|
2409
|
+
})
|
|
2410
|
+
]
|
|
2411
|
+
}, option.key);
|
|
2412
|
+
}),
|
|
2413
|
+
view.note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2414
|
+
style: {
|
|
2415
|
+
margin: "2px 0 0",
|
|
2416
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2417
|
+
},
|
|
2418
|
+
children: view.note
|
|
2419
|
+
}),
|
|
2420
|
+
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2421
|
+
style: {
|
|
2422
|
+
margin: "2px 0 0",
|
|
2423
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
2424
|
+
},
|
|
2425
|
+
children: error
|
|
2426
|
+
}),
|
|
2427
|
+
view.settingsPath !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2428
|
+
style: {
|
|
2429
|
+
margin: "4px 0 0",
|
|
2430
|
+
color: "var(--dsw-alias-label-caption)",
|
|
2431
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
2432
|
+
fontSize: 11
|
|
2433
|
+
},
|
|
2434
|
+
children: ["writes ", view.settingsPath]
|
|
2435
|
+
})
|
|
2436
|
+
]
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
/** Field and control styling for the providers body — same visual language as the auth list. */
|
|
2440
|
+
const fieldStyle = {
|
|
2441
|
+
display: "flex",
|
|
2442
|
+
flexDirection: "column",
|
|
2443
|
+
gap: 3,
|
|
2444
|
+
fontSize: 12.5,
|
|
2445
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
2446
|
+
};
|
|
2447
|
+
const buttonStyle = {
|
|
2448
|
+
font: "inherit",
|
|
2449
|
+
fontSize: 12.5,
|
|
2450
|
+
padding: "5px 10px",
|
|
2451
|
+
borderRadius: 6,
|
|
2452
|
+
border: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,0.3))",
|
|
2453
|
+
background: "transparent",
|
|
2454
|
+
color: "var(--dsw-alias-label-primary)",
|
|
2455
|
+
cursor: "pointer"
|
|
2456
|
+
};
|
|
2457
|
+
const primaryButtonStyle = {
|
|
2458
|
+
...buttonStyle,
|
|
2459
|
+
border: "none",
|
|
2460
|
+
background: "var(--dsw-alias-label-primary)",
|
|
2461
|
+
color: "var(--dsw-alias-bg-base, #fff)",
|
|
2462
|
+
fontWeight: 500
|
|
2463
|
+
};
|
|
2464
|
+
/** The blank draft the Add form starts from. */
|
|
2465
|
+
const EMPTY_PROVIDER_DRAFT = {
|
|
2466
|
+
id: "",
|
|
2467
|
+
label: "",
|
|
2468
|
+
baseUrl: "",
|
|
2469
|
+
protocol: "responses",
|
|
2470
|
+
authKind: "apiKey",
|
|
2471
|
+
apiKey: "",
|
|
2472
|
+
envName: "",
|
|
2473
|
+
modelIds: "",
|
|
2474
|
+
contextWindow: ""
|
|
2475
|
+
};
|
|
2476
|
+
/** `a, b c` → `['a','b','c']` — the model field takes whatever separator is natural. */
|
|
2477
|
+
function parseModelIds(text) {
|
|
2478
|
+
return text.split(/[\s,]+/).map((part) => part.trim()).filter((part) => part !== "");
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* The multi-provider body (Codex 2026-10-03 ruling; Claude 2026-10-04 rides
|
|
2482
|
+
* the same body with its own view shape — no `protocols` field, two header
|
|
2483
|
+
* auth kinds, an endpoint-URL native).
|
|
2484
|
+
*
|
|
2485
|
+
* Super D hosts the list cc-switch otherwise holds: add, edit and remove only.
|
|
2486
|
+
* The active/select half is gone — which provider a Codex session runs on is
|
|
2487
|
+
* DSH's own per-session model selection, so no row is marked "in force". What
|
|
2488
|
+
* the user's OWN config.toml names is rendered as a passive fact (the adapter
|
|
2489
|
+
* reconciles the list against that file), and the files it touches are named so
|
|
2490
|
+
* an edit is never a surprise.
|
|
2491
|
+
*
|
|
2492
|
+
* Keys are write-only by construction: the adapter answers `hasKey`, never a
|
|
2493
|
+
* value, so the field is blank on every open and blank means "leave whatever is
|
|
2494
|
+
* already there".
|
|
2495
|
+
*/
|
|
2496
|
+
function ProvidersPanel({ agent, namespace }) {
|
|
2497
|
+
const [view, setView] = react.useState(void 0);
|
|
2498
|
+
const [error, setError] = react.useState(null);
|
|
2499
|
+
const [busy, setBusy] = react.useState(null);
|
|
2500
|
+
const [loading, setLoading] = react.useState(true);
|
|
2501
|
+
/** The id being edited, `NEW_PROVIDER` for the add form, `null` when closed. */
|
|
2502
|
+
const [editing, setEditing] = react.useState(null);
|
|
2503
|
+
const [draft, setDraft] = react.useState(EMPTY_PROVIDER_DRAFT);
|
|
2504
|
+
react.useEffect(() => {
|
|
2505
|
+
let live = true;
|
|
2506
|
+
setLoading(true);
|
|
2507
|
+
fetchProvidersView(agent.key, namespace).then((next) => {
|
|
2508
|
+
if (live) {
|
|
2509
|
+
setView(next);
|
|
2510
|
+
setError(null);
|
|
2511
|
+
}
|
|
2512
|
+
}).catch((err) => {
|
|
2513
|
+
if (live) {
|
|
2514
|
+
setView(void 0);
|
|
2515
|
+
setError(messageOf(err));
|
|
2516
|
+
}
|
|
2517
|
+
}).finally(() => {
|
|
2518
|
+
if (live) setLoading(false);
|
|
2519
|
+
});
|
|
2520
|
+
return () => {
|
|
2521
|
+
live = false;
|
|
2522
|
+
};
|
|
2523
|
+
}, [agent.key, namespace]);
|
|
2524
|
+
/** One mutation, one authoritative re-read; `key` drives the busy chip. */
|
|
2525
|
+
const run = (key, call) => {
|
|
2526
|
+
if (busy !== null) return;
|
|
2527
|
+
setBusy(key);
|
|
2528
|
+
call().then((next) => {
|
|
2529
|
+
setView(next);
|
|
2530
|
+
setError(null);
|
|
2531
|
+
setEditing(null);
|
|
2532
|
+
setDraft(EMPTY_PROVIDER_DRAFT);
|
|
2533
|
+
}).catch((err) => {
|
|
2534
|
+
setError(messageOf(err));
|
|
2535
|
+
}).finally(() => {
|
|
2536
|
+
setBusy(null);
|
|
2537
|
+
});
|
|
2538
|
+
};
|
|
2539
|
+
const openNew = () => {
|
|
2540
|
+
setEditing(NEW_PROVIDER);
|
|
2541
|
+
setDraft(EMPTY_PROVIDER_DRAFT);
|
|
2542
|
+
setError(null);
|
|
2543
|
+
};
|
|
2544
|
+
const openEdit = (provider) => {
|
|
2545
|
+
setEditing(provider.id);
|
|
2546
|
+
setDraft({
|
|
2547
|
+
id: provider.id,
|
|
2548
|
+
label: provider.label,
|
|
2549
|
+
baseUrl: provider.baseUrl,
|
|
2550
|
+
protocol: provider.protocol,
|
|
2551
|
+
authKind: provider.authKind,
|
|
2552
|
+
apiKey: "",
|
|
2553
|
+
envName: provider.envName ?? "",
|
|
2554
|
+
modelIds: provider.models.map((model) => model.id).join(", "),
|
|
2555
|
+
contextWindow: provider.models[0]?.contextWindow === void 0 ? "" : String(provider.models[0].contextWindow)
|
|
2556
|
+
});
|
|
2557
|
+
setError(null);
|
|
2558
|
+
};
|
|
2559
|
+
const submit = () => {
|
|
2560
|
+
const ids = parseModelIds(draft.modelIds);
|
|
2561
|
+
const window$1 = draft.contextWindow.trim() === "" ? void 0 : Number(draft.contextWindow);
|
|
2562
|
+
const provider = {
|
|
2563
|
+
id: draft.id.trim(),
|
|
2564
|
+
label: draft.label.trim(),
|
|
2565
|
+
baseUrl: draft.baseUrl.trim(),
|
|
2566
|
+
protocol: draft.protocol,
|
|
2567
|
+
auth: draft.authKind === "env" ? {
|
|
2568
|
+
kind: "env",
|
|
2569
|
+
name: draft.envName.trim()
|
|
2570
|
+
} : {
|
|
2571
|
+
kind: draft.authKind === "authToken" ? "authToken" : "apiKey",
|
|
2572
|
+
value: draft.apiKey
|
|
2573
|
+
},
|
|
2574
|
+
models: ids.map((id) => ({
|
|
2575
|
+
id,
|
|
2576
|
+
...window$1 !== void 0 && Number.isSafeInteger(window$1) && window$1 > 0 ? { contextWindow: window$1 } : {}
|
|
2577
|
+
}))
|
|
2578
|
+
};
|
|
2579
|
+
run("save", () => saveProvider(agent.key, namespace, provider));
|
|
2580
|
+
};
|
|
2581
|
+
if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2582
|
+
style: {
|
|
2583
|
+
margin: 0,
|
|
2584
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2585
|
+
},
|
|
2586
|
+
children: "Loading providers…"
|
|
2587
|
+
});
|
|
2588
|
+
if (view === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2589
|
+
style: {
|
|
2590
|
+
margin: 0,
|
|
2591
|
+
color: "var(--dsw-alias-state-warn-label, var(--dsw-alias-label-secondary))"
|
|
2592
|
+
},
|
|
2593
|
+
children: error ?? "Providers are unavailable for this runtime."
|
|
2594
|
+
});
|
|
2595
|
+
const nativePaths = nativePathsLine(view.paths);
|
|
2596
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2597
|
+
style: {
|
|
2598
|
+
display: "flex",
|
|
2599
|
+
flexDirection: "column",
|
|
2600
|
+
gap: 6
|
|
2601
|
+
},
|
|
2602
|
+
children: [
|
|
2603
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2604
|
+
style: panelHeadStyle,
|
|
2605
|
+
children: "Model provider"
|
|
2606
|
+
}),
|
|
2607
|
+
view.native !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2608
|
+
style: {
|
|
2609
|
+
margin: 0,
|
|
2610
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2611
|
+
},
|
|
2612
|
+
children: [
|
|
2613
|
+
"Your own ",
|
|
2614
|
+
agent.label,
|
|
2615
|
+
" config currently runs ",
|
|
2616
|
+
nativeDescription(view.native),
|
|
2617
|
+
"."
|
|
2618
|
+
]
|
|
2619
|
+
}),
|
|
2620
|
+
view.syncedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2621
|
+
style: {
|
|
2622
|
+
margin: 0,
|
|
2623
|
+
color: "var(--dsw-alias-label-caption)",
|
|
2624
|
+
fontSize: 11
|
|
2625
|
+
},
|
|
2626
|
+
children: ["synced from your native config ", new Date(view.syncedAt).toLocaleString()]
|
|
2627
|
+
}),
|
|
2628
|
+
view.providers.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2629
|
+
style: {
|
|
2630
|
+
margin: 0,
|
|
2631
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2632
|
+
},
|
|
2633
|
+
children: "No providers yet — add one to pin a model and endpoint."
|
|
2634
|
+
}),
|
|
2635
|
+
view.providers.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2636
|
+
style: optionStyle,
|
|
2637
|
+
children: [
|
|
2638
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2639
|
+
style: {
|
|
2640
|
+
fontWeight: 500,
|
|
2641
|
+
whiteSpace: "nowrap"
|
|
2642
|
+
},
|
|
2643
|
+
children: provider.label
|
|
2644
|
+
}),
|
|
2645
|
+
provider.authKind === "env" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2646
|
+
style: chip,
|
|
2647
|
+
children: ["env ", provider.envName]
|
|
2648
|
+
}),
|
|
2649
|
+
provider.authKind === "authToken" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2650
|
+
style: chip,
|
|
2651
|
+
children: "token"
|
|
2652
|
+
}),
|
|
2653
|
+
!provider.hasKey && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2654
|
+
style: chip,
|
|
2655
|
+
children: "no key"
|
|
2656
|
+
}),
|
|
2657
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2658
|
+
style: {
|
|
2659
|
+
flex: 1,
|
|
2660
|
+
minWidth: 0,
|
|
2661
|
+
overflow: "hidden",
|
|
2662
|
+
textOverflow: "ellipsis",
|
|
2663
|
+
whiteSpace: "nowrap",
|
|
2664
|
+
fontSize: 12,
|
|
2665
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2666
|
+
},
|
|
2667
|
+
children: [provider.baseUrl, provider.models.length === 0 ? "" : ` · ${provider.models.map((model) => model.id).join(", ")}`]
|
|
2668
|
+
}),
|
|
2669
|
+
provider.models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2670
|
+
style: chipWarn,
|
|
2671
|
+
children: "no models yet — add one"
|
|
2672
|
+
}),
|
|
2673
|
+
busy === `remove:${provider.id}` && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2674
|
+
style: chip,
|
|
2675
|
+
children: "removing…"
|
|
2676
|
+
}),
|
|
2677
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2678
|
+
type: "button",
|
|
2679
|
+
style: buttonStyle,
|
|
2680
|
+
disabled: busy !== null,
|
|
2681
|
+
onClick: () => {
|
|
2682
|
+
openEdit(provider);
|
|
2683
|
+
},
|
|
2684
|
+
children: "Edit"
|
|
2685
|
+
}),
|
|
2686
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2687
|
+
type: "button",
|
|
2688
|
+
style: {
|
|
2689
|
+
...buttonStyle,
|
|
2690
|
+
color: "var(--dsw-alias-state-error-primary, #c66)"
|
|
2691
|
+
},
|
|
2692
|
+
disabled: busy !== null,
|
|
2693
|
+
"aria-label": `Delete ${provider.label}`,
|
|
2694
|
+
onClick: () => {
|
|
2695
|
+
run(`remove:${provider.id}`, () => removeProvider(agent.key, namespace, provider.id));
|
|
2696
|
+
},
|
|
2697
|
+
children: "Delete"
|
|
2698
|
+
})
|
|
2699
|
+
]
|
|
2700
|
+
}, provider.id)),
|
|
2701
|
+
editing === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2702
|
+
type: "button",
|
|
2703
|
+
style: buttonStyle,
|
|
2704
|
+
disabled: busy !== null,
|
|
2705
|
+
onClick: openNew,
|
|
2706
|
+
children: "+ Add provider"
|
|
2707
|
+
}) }),
|
|
2708
|
+
editing !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2709
|
+
style: {
|
|
2710
|
+
display: "flex",
|
|
2711
|
+
flexDirection: "column",
|
|
2712
|
+
gap: 8,
|
|
2713
|
+
maxWidth: 520
|
|
2714
|
+
},
|
|
2715
|
+
children: [
|
|
2716
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2717
|
+
style: panelHeadStyle,
|
|
2718
|
+
children: editing === NEW_PROVIDER ? "Add provider" : `Edit ${editing}`
|
|
2719
|
+
}),
|
|
2720
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2721
|
+
style: fieldStyle,
|
|
2722
|
+
children: ["Provider id", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2723
|
+
style: inputStyle,
|
|
2724
|
+
value: draft.id,
|
|
2725
|
+
disabled: editing !== NEW_PROVIDER,
|
|
2726
|
+
placeholder: "zai-plan",
|
|
2727
|
+
onChange: (event) => {
|
|
2728
|
+
setDraft({
|
|
2729
|
+
...draft,
|
|
2730
|
+
id: event.target.value
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
})]
|
|
2734
|
+
}),
|
|
2735
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2736
|
+
style: fieldStyle,
|
|
2737
|
+
children: ["Display name", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2738
|
+
style: inputStyle,
|
|
2739
|
+
value: draft.label,
|
|
2740
|
+
placeholder: "Z.ai Plan",
|
|
2741
|
+
onChange: (event) => {
|
|
2742
|
+
setDraft({
|
|
2743
|
+
...draft,
|
|
2744
|
+
label: event.target.value
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
})]
|
|
2748
|
+
}),
|
|
2749
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2750
|
+
style: fieldStyle,
|
|
2751
|
+
children: ["Base URL", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2752
|
+
style: inputStyle,
|
|
2753
|
+
value: draft.baseUrl,
|
|
2754
|
+
placeholder: "https://api.z.ai/api/coding/paas/v4",
|
|
2755
|
+
onChange: (event) => {
|
|
2756
|
+
setDraft({
|
|
2757
|
+
...draft,
|
|
2758
|
+
baseUrl: event.target.value
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
})]
|
|
2762
|
+
}),
|
|
2763
|
+
view.protocols.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2764
|
+
style: fieldStyle,
|
|
2765
|
+
children: ["API protocol", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
2766
|
+
style: inputStyle,
|
|
2767
|
+
value: draft.protocol,
|
|
2768
|
+
onChange: (event) => {
|
|
2769
|
+
setDraft({
|
|
2770
|
+
...draft,
|
|
2771
|
+
protocol: event.target.value
|
|
2772
|
+
});
|
|
2773
|
+
},
|
|
2774
|
+
children: view.protocols.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
2775
|
+
value: option.value,
|
|
2776
|
+
disabled: !option.available,
|
|
2777
|
+
title: option.hint,
|
|
2778
|
+
children: [option.label, option.available ? "" : " — unsupported"]
|
|
2779
|
+
}, option.value))
|
|
2780
|
+
})]
|
|
2781
|
+
}), (() => {
|
|
2782
|
+
const option = view.protocols.find((candidate) => candidate.value === draft.protocol);
|
|
2783
|
+
return option === void 0 || option.available ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2784
|
+
style: {
|
|
2785
|
+
margin: 0,
|
|
2786
|
+
color: "var(--dsw-alias-state-error-primary, #c66)",
|
|
2787
|
+
fontSize: 12
|
|
2788
|
+
},
|
|
2789
|
+
children: option.hint
|
|
2790
|
+
});
|
|
2791
|
+
})()] }),
|
|
2792
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2793
|
+
style: fieldStyle,
|
|
2794
|
+
children: ["Model IDs", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2795
|
+
style: inputStyle,
|
|
2796
|
+
value: draft.modelIds,
|
|
2797
|
+
placeholder: "glm-5.2, glm-5.2-air",
|
|
2798
|
+
onChange: (event) => {
|
|
2799
|
+
setDraft({
|
|
2800
|
+
...draft,
|
|
2801
|
+
modelIds: event.target.value
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
})]
|
|
2805
|
+
}),
|
|
2806
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2807
|
+
style: fieldStyle,
|
|
2808
|
+
children: ["Context window (tokens, optional)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2809
|
+
style: inputStyle,
|
|
2810
|
+
value: draft.contextWindow,
|
|
2811
|
+
inputMode: "numeric",
|
|
2812
|
+
placeholder: "1000000",
|
|
2813
|
+
onChange: (event) => {
|
|
2814
|
+
setDraft({
|
|
2815
|
+
...draft,
|
|
2816
|
+
contextWindow: event.target.value
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
})]
|
|
2820
|
+
}),
|
|
2821
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2822
|
+
style: fieldStyle,
|
|
2823
|
+
children: ["Key shape", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
2824
|
+
style: inputStyle,
|
|
2825
|
+
value: draft.authKind,
|
|
2826
|
+
onChange: (event) => {
|
|
2827
|
+
setDraft({
|
|
2828
|
+
...draft,
|
|
2829
|
+
authKind: event.target.value
|
|
2830
|
+
});
|
|
2831
|
+
},
|
|
2832
|
+
children: authKindOptions(view).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2833
|
+
value: option.value,
|
|
2834
|
+
title: option.hint,
|
|
2835
|
+
children: option.label
|
|
2836
|
+
}, option.value))
|
|
2837
|
+
})]
|
|
2838
|
+
}),
|
|
2839
|
+
draft.authKind === "env" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2840
|
+
style: fieldStyle,
|
|
2841
|
+
children: ["Environment variable", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2842
|
+
style: inputStyle,
|
|
2843
|
+
value: draft.envName,
|
|
2844
|
+
placeholder: "DEEPSEEK_API_KEY",
|
|
2845
|
+
onChange: (event) => {
|
|
2846
|
+
setDraft({
|
|
2847
|
+
...draft,
|
|
2848
|
+
envName: event.target.value
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
})]
|
|
2852
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2853
|
+
style: fieldStyle,
|
|
2854
|
+
children: [
|
|
2855
|
+
draft.authKind === "authToken" ? "Bearer token" : "API key",
|
|
2856
|
+
" ",
|
|
2857
|
+
editing !== NEW_PROVIDER && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2858
|
+
style: { color: "var(--dsw-alias-label-caption)" },
|
|
2859
|
+
children: "(leave blank to keep the saved one)"
|
|
2860
|
+
}),
|
|
2861
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2862
|
+
style: inputStyle,
|
|
2863
|
+
type: "password",
|
|
2864
|
+
value: draft.apiKey,
|
|
2865
|
+
autoComplete: "off",
|
|
2866
|
+
onChange: (event) => {
|
|
2867
|
+
setDraft({
|
|
2868
|
+
...draft,
|
|
2869
|
+
apiKey: event.target.value
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
})
|
|
2873
|
+
]
|
|
2874
|
+
}),
|
|
2875
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2876
|
+
style: {
|
|
2877
|
+
display: "flex",
|
|
2878
|
+
gap: 6
|
|
2879
|
+
},
|
|
2880
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2881
|
+
type: "button",
|
|
2882
|
+
style: primaryButtonStyle,
|
|
2883
|
+
disabled: busy !== null,
|
|
2884
|
+
onClick: submit,
|
|
2885
|
+
children: busy === "save" ? "Saving…" : "Save"
|
|
2886
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2887
|
+
type: "button",
|
|
2888
|
+
style: buttonStyle,
|
|
2889
|
+
disabled: busy !== null,
|
|
2890
|
+
onClick: () => {
|
|
2891
|
+
setEditing(null);
|
|
2892
|
+
setDraft(EMPTY_PROVIDER_DRAFT);
|
|
2893
|
+
},
|
|
2894
|
+
children: "Cancel"
|
|
2895
|
+
})]
|
|
2896
|
+
})
|
|
2897
|
+
]
|
|
2898
|
+
}),
|
|
2899
|
+
view.note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2900
|
+
style: {
|
|
2901
|
+
margin: "2px 0 0",
|
|
2902
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2903
|
+
},
|
|
2904
|
+
children: view.note
|
|
2905
|
+
}),
|
|
2906
|
+
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2907
|
+
style: {
|
|
2908
|
+
margin: "2px 0 0",
|
|
2909
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
2910
|
+
},
|
|
2911
|
+
children: error
|
|
2912
|
+
}),
|
|
2913
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2914
|
+
style: {
|
|
2915
|
+
margin: "4px 0 0",
|
|
2916
|
+
color: "var(--dsw-alias-label-caption)",
|
|
2917
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
2918
|
+
fontSize: 11
|
|
2919
|
+
},
|
|
2920
|
+
children: ["list ", view.paths.store]
|
|
2921
|
+
}),
|
|
2922
|
+
nativePaths !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2923
|
+
style: {
|
|
2924
|
+
margin: 0,
|
|
2925
|
+
color: "var(--dsw-alias-label-caption)",
|
|
2926
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
2927
|
+
fontSize: 11
|
|
2928
|
+
},
|
|
2929
|
+
children: ["native ", nativePaths]
|
|
2930
|
+
})
|
|
2931
|
+
]
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
/** Sentinel for "the add form", distinct from any real provider id. */
|
|
2935
|
+
const NEW_PROVIDER = "\0new";
|
|
1479
2936
|
function HubPage() {
|
|
1480
|
-
const [
|
|
2937
|
+
const [mesh, setMesh] = react.useState({ machines: [] });
|
|
2938
|
+
const [meshLoaded, setMeshLoaded] = react.useState(false);
|
|
2939
|
+
const [meshError, setMeshError] = react.useState(null);
|
|
2940
|
+
const [meshBusy, setMeshBusy] = react.useState(false);
|
|
2941
|
+
const [meshReloads, setMeshReloads] = react.useState(0);
|
|
1481
2942
|
const [showAdd, setShowAdd] = react.useState(false);
|
|
1482
2943
|
const [draft, setDraft] = react.useState(EMPTY_DRAFT);
|
|
1483
2944
|
const [agents, setAgents] = react.useState(() => rosterFromGlobal() ?? null);
|
|
@@ -1486,6 +2947,11 @@ function HubPage() {
|
|
|
1486
2947
|
const fusion = fusionPage();
|
|
1487
2948
|
const switchUi = switchAffordance(fusion);
|
|
1488
2949
|
const [activeKey, setActiveKey] = react.useState(() => fusion ? uiState()?.activeAgent : void 0);
|
|
2950
|
+
/**
|
|
2951
|
+
* The open settings panel (P3 accordion): one runtime key at a time, or
|
|
2952
|
+
* `null` when every row is collapsed. `toggleExpanded` owns the transition.
|
|
2953
|
+
*/
|
|
2954
|
+
const [expandedKey, setExpandedKey] = react.useState(null);
|
|
1489
2955
|
react.useEffect(() => {
|
|
1490
2956
|
let cancelled = false;
|
|
1491
2957
|
fetchRoster().then((list) => {
|
|
@@ -1503,6 +2969,24 @@ function HubPage() {
|
|
|
1503
2969
|
cancelled = true;
|
|
1504
2970
|
};
|
|
1505
2971
|
}, []);
|
|
2972
|
+
react.useEffect(() => {
|
|
2973
|
+
let cancelled = false;
|
|
2974
|
+
fetchMachines().then((list) => {
|
|
2975
|
+
if (!cancelled) {
|
|
2976
|
+
setMesh(list);
|
|
2977
|
+
setMeshError(null);
|
|
2978
|
+
setMeshLoaded(true);
|
|
2979
|
+
}
|
|
2980
|
+
}).catch((err) => {
|
|
2981
|
+
if (!cancelled) {
|
|
2982
|
+
setMeshError(errText(err));
|
|
2983
|
+
setMeshLoaded(true);
|
|
2984
|
+
}
|
|
2985
|
+
});
|
|
2986
|
+
return () => {
|
|
2987
|
+
cancelled = true;
|
|
2988
|
+
};
|
|
2989
|
+
}, [meshReloads]);
|
|
1506
2990
|
react.useEffect(() => {
|
|
1507
2991
|
if (!fusion) return;
|
|
1508
2992
|
const state = uiState();
|
|
@@ -1512,6 +2996,7 @@ function HubPage() {
|
|
|
1512
2996
|
});
|
|
1513
2997
|
}, [fusion]);
|
|
1514
2998
|
const here = currentMountPath();
|
|
2999
|
+
const hereOrigin = typeof location === "undefined" ? "" : location.origin;
|
|
1515
3000
|
const isActive = (agent) => hubRowActive({
|
|
1516
3001
|
fusion,
|
|
1517
3002
|
agentKey: agent.key,
|
|
@@ -1534,30 +3019,116 @@ function HubPage() {
|
|
|
1534
3019
|
attempt: () => attemptSwitch(agent.key)
|
|
1535
3020
|
}).preventDefault) event.preventDefault();
|
|
1536
3021
|
};
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
3022
|
+
const machineLabels = meshLoaded && meshError === null ? machineLabelsFrom(mesh) : void 0;
|
|
3023
|
+
const groups = agents !== null && machineLabels !== void 0 ? groupByMachine(agents, machineLabels) : void 0;
|
|
3024
|
+
/**
|
|
3025
|
+
* One Agent Runtimes row — the SAME row in both shapes; `showMachine`
|
|
3026
|
+
* keeps the flat fallback's per-row machine text (the grouped shape moves
|
|
3027
|
+
* it onto the section head).
|
|
3028
|
+
*
|
|
3029
|
+
* P3 interaction (2026-10-02 user ruling): the ROW BODY expands this
|
|
3030
|
+
* runtime's OWN settings inline (accordion — one panel at a time), and the
|
|
3031
|
+
* right-hand `switch` is the row's ONLY switch, now a real button. The two
|
|
3032
|
+
* are separate controls on purpose: expanding is navigation of config, the
|
|
3033
|
+
* button performs `seatClickDecision` (intercept + in-place switchAgent).
|
|
3034
|
+
*/
|
|
3035
|
+
const renderRow = (agent, showMachine) => {
|
|
3036
|
+
const expanded = expandedKey === agent.key;
|
|
3037
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverRow, {
|
|
3038
|
+
clickable: true,
|
|
3039
|
+
expanded,
|
|
3040
|
+
onClick: () => {
|
|
3041
|
+
setExpandedKey((current$1) => toggleExpanded(current$1, agent.key));
|
|
3042
|
+
},
|
|
3043
|
+
children: [
|
|
3044
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeMark, {
|
|
3045
|
+
agentKey: agent.key,
|
|
3046
|
+
size: 20
|
|
3047
|
+
}),
|
|
3048
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3049
|
+
style: {
|
|
3050
|
+
fontWeight: 500,
|
|
3051
|
+
whiteSpace: "nowrap"
|
|
3052
|
+
},
|
|
3053
|
+
children: agent.label
|
|
3054
|
+
}),
|
|
3055
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3056
|
+
style: {
|
|
3057
|
+
flex: 1,
|
|
3058
|
+
minWidth: 0,
|
|
3059
|
+
overflow: "hidden",
|
|
3060
|
+
textOverflow: "ellipsis",
|
|
3061
|
+
whiteSpace: "nowrap",
|
|
3062
|
+
fontSize: 12.5,
|
|
3063
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
3064
|
+
},
|
|
3065
|
+
children: showMachine ? agent.machine : null
|
|
3066
|
+
}),
|
|
3067
|
+
isActive(agent) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3068
|
+
style: chipSelf,
|
|
3069
|
+
children: "current"
|
|
3070
|
+
}),
|
|
3071
|
+
switchUi.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3072
|
+
type: "button",
|
|
3073
|
+
"aria-current": isActive(agent) ? "page" : void 0,
|
|
3074
|
+
"aria-label": isActive(agent) ? `${agent.label} is current` : `Switch to ${agent.label}`,
|
|
3075
|
+
style: {
|
|
3076
|
+
...switchButtonStyle,
|
|
3077
|
+
color: isActive(agent) ? "var(--dsw-alias-label-tertiary)" : "var(--dsw-alias-label-secondary)",
|
|
3078
|
+
cursor: "pointer"
|
|
3079
|
+
},
|
|
3080
|
+
onClick: (event) => {
|
|
3081
|
+
event.stopPropagation();
|
|
3082
|
+
rowDecision(agent, event);
|
|
3083
|
+
},
|
|
3084
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconPlayOutlineRegular, { size: 16 })
|
|
3085
|
+
})
|
|
3086
|
+
]
|
|
3087
|
+
}), expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
3088
|
+
style: panelStyle,
|
|
3089
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeSettingsPanel, { agent })
|
|
3090
|
+
})] }, agent.key);
|
|
3091
|
+
};
|
|
3092
|
+
/** Bump the reload sequence — every mutating op refreshes through the API. */
|
|
3093
|
+
const reloadMachines = () => {
|
|
3094
|
+
setMeshReloads((n) => n + 1);
|
|
1541
3095
|
};
|
|
1542
|
-
const
|
|
3096
|
+
const addRemote = () => {
|
|
1543
3097
|
const name = draft.name.trim();
|
|
1544
|
-
const
|
|
1545
|
-
if (name === "" ||
|
|
1546
|
-
|
|
1547
|
-
|
|
3098
|
+
const tokenUrl = draft.tokenUrl.trim();
|
|
3099
|
+
if (name === "" || !validTokenUrl(tokenUrl)) return;
|
|
3100
|
+
setMeshBusy(true);
|
|
3101
|
+
addMachine({
|
|
1548
3102
|
name,
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
3103
|
+
tokenUrl
|
|
3104
|
+
}).then(() => {
|
|
3105
|
+
setDraft(EMPTY_DRAFT);
|
|
3106
|
+
setShowAdd(false);
|
|
3107
|
+
setMeshError(null);
|
|
3108
|
+
reloadMachines();
|
|
3109
|
+
}).catch((err) => {
|
|
3110
|
+
setMeshError(errText(err));
|
|
3111
|
+
}).finally(() => {
|
|
3112
|
+
setMeshBusy(false);
|
|
3113
|
+
});
|
|
3114
|
+
};
|
|
3115
|
+
const removeRemote = (id) => {
|
|
3116
|
+
removeMachine(id).then(() => {
|
|
3117
|
+
setMeshError(null);
|
|
3118
|
+
reloadMachines();
|
|
3119
|
+
}).catch((err) => {
|
|
3120
|
+
setMeshError(errText(err));
|
|
3121
|
+
});
|
|
1556
3122
|
};
|
|
1557
|
-
const
|
|
1558
|
-
|
|
3123
|
+
const reprobeRemote = (id) => {
|
|
3124
|
+
reprobeMachine(id).then(() => {
|
|
3125
|
+
setMeshError(null);
|
|
3126
|
+
reloadMachines();
|
|
3127
|
+
}).catch((err) => {
|
|
3128
|
+
setMeshError(errText(err));
|
|
3129
|
+
});
|
|
1559
3130
|
};
|
|
1560
|
-
const draftValid = draft.name.trim() !== "" && draft.
|
|
3131
|
+
const draftValid = draft.name.trim() !== "" && validTokenUrl(draft.tokenUrl.trim());
|
|
1561
3132
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1562
3133
|
style: {
|
|
1563
3134
|
display: "flex",
|
|
@@ -1685,7 +3256,7 @@ function HubPage() {
|
|
|
1685
3256
|
children: [
|
|
1686
3257
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupHead, {
|
|
1687
3258
|
title: "Machines",
|
|
1688
|
-
count: machines.length,
|
|
3259
|
+
count: (mesh.self !== void 0 ? 1 : 0) + mesh.machines.length,
|
|
1689
3260
|
action: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1690
3261
|
type: "button",
|
|
1691
3262
|
onClick: () => {
|
|
@@ -1719,90 +3290,152 @@ function HubPage() {
|
|
|
1719
3290
|
}), "Add machine"]
|
|
1720
3291
|
})
|
|
1721
3292
|
}),
|
|
1722
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
3293
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1723
3294
|
style: {
|
|
1724
3295
|
margin: 0,
|
|
1725
3296
|
fontSize: 13,
|
|
1726
3297
|
lineHeight: "20px",
|
|
1727
3298
|
color: "var(--dsw-alias-label-tertiary)"
|
|
1728
3299
|
},
|
|
1729
|
-
children:
|
|
3300
|
+
children: [
|
|
3301
|
+
"Add another DSH from remote machines, provide the token URL. Status ",
|
|
3302
|
+
AUTO_REFRESH_NOTE,
|
|
3303
|
+
"."
|
|
3304
|
+
]
|
|
3305
|
+
}),
|
|
3306
|
+
meshError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3307
|
+
style: {
|
|
3308
|
+
margin: 0,
|
|
3309
|
+
fontSize: 12.5,
|
|
3310
|
+
lineHeight: "20px",
|
|
3311
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
3312
|
+
},
|
|
3313
|
+
children: meshError
|
|
1730
3314
|
}),
|
|
1731
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
3315
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
|
|
1732
3316
|
style: listStyle,
|
|
1733
|
-
children:
|
|
1734
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
1735
|
-
"aria-hidden": true,
|
|
1736
|
-
style: {
|
|
1737
|
-
width: 8,
|
|
1738
|
-
height: 8,
|
|
1739
|
-
borderRadius: "50%",
|
|
1740
|
-
flex: "none",
|
|
1741
|
-
background: machine.self ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-label-caption)"
|
|
1742
|
-
}
|
|
1743
|
-
}),
|
|
1744
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3317
|
+
children: [
|
|
3318
|
+
!meshLoaded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
1745
3319
|
style: {
|
|
1746
|
-
|
|
1747
|
-
|
|
3320
|
+
padding: "9px 8px",
|
|
3321
|
+
fontSize: 13,
|
|
3322
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
3323
|
+
listStyle: "none"
|
|
1748
3324
|
},
|
|
1749
|
-
children:
|
|
1750
|
-
}),
|
|
1751
|
-
machine.label !== machine.name && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1752
|
-
style: chip,
|
|
1753
|
-
children: machine.label
|
|
3325
|
+
children: "Loading machines…"
|
|
1754
3326
|
}),
|
|
1755
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
3327
|
+
meshLoaded && meshError === null && mesh.machines.length === 0 && mesh.self === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
1756
3328
|
style: {
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
whiteSpace: "nowrap",
|
|
1762
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
1763
|
-
fontSize: 12,
|
|
1764
|
-
color: "var(--dsw-alias-label-secondary)"
|
|
3329
|
+
padding: "9px 8px",
|
|
3330
|
+
fontSize: 13,
|
|
3331
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
3332
|
+
listStyle: "none"
|
|
1765
3333
|
},
|
|
1766
|
-
children:
|
|
3334
|
+
children: "No machines yet — add the first one with its token URL."
|
|
1767
3335
|
}),
|
|
1768
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
},
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
viewBox: "0 0 11 11",
|
|
1795
|
-
fill: "none",
|
|
1796
|
-
"aria-hidden": "true",
|
|
1797
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1798
|
-
d: "M2.5 2.5L8.5 8.5M8.5 2.5L2.5 8.5",
|
|
1799
|
-
stroke: "currentColor",
|
|
1800
|
-
strokeWidth: "1.4",
|
|
1801
|
-
strokeLinecap: "round"
|
|
1802
|
-
})
|
|
3336
|
+
mesh.self !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverRow, { children: [
|
|
3337
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3338
|
+
"aria-hidden": true,
|
|
3339
|
+
style: {
|
|
3340
|
+
...dotStyle,
|
|
3341
|
+
background: "var(--dsw-alias-state-success-primary)"
|
|
3342
|
+
}
|
|
3343
|
+
}),
|
|
3344
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3345
|
+
style: {
|
|
3346
|
+
fontWeight: 500,
|
|
3347
|
+
whiteSpace: "nowrap"
|
|
3348
|
+
},
|
|
3349
|
+
children: mesh.self.label
|
|
3350
|
+
}),
|
|
3351
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3352
|
+
style: originStyle,
|
|
3353
|
+
children: hereOrigin
|
|
3354
|
+
}),
|
|
3355
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3356
|
+
style: chipMonoId,
|
|
3357
|
+
children: mesh.self.id
|
|
3358
|
+
}),
|
|
3359
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3360
|
+
style: chipSelf,
|
|
3361
|
+
children: "self"
|
|
1803
3362
|
})
|
|
3363
|
+
] }),
|
|
3364
|
+
mesh.machines.map((machine) => {
|
|
3365
|
+
const status = statusChip(machine);
|
|
3366
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(HoverRow, { children: [
|
|
3367
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3368
|
+
"aria-hidden": true,
|
|
3369
|
+
style: {
|
|
3370
|
+
...dotStyle,
|
|
3371
|
+
background: statusDotColor(machine.status)
|
|
3372
|
+
}
|
|
3373
|
+
}),
|
|
3374
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3375
|
+
style: {
|
|
3376
|
+
fontWeight: 500,
|
|
3377
|
+
whiteSpace: "nowrap"
|
|
3378
|
+
},
|
|
3379
|
+
children: machine.name
|
|
3380
|
+
}),
|
|
3381
|
+
machine.label !== machine.name && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3382
|
+
style: chip,
|
|
3383
|
+
children: machine.label
|
|
3384
|
+
}),
|
|
3385
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3386
|
+
style: originStyle,
|
|
3387
|
+
children: machine.origin
|
|
3388
|
+
}),
|
|
3389
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3390
|
+
style: chipMonoId,
|
|
3391
|
+
children: machine.id
|
|
3392
|
+
}),
|
|
3393
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3394
|
+
style: status.style,
|
|
3395
|
+
children: status.text
|
|
3396
|
+
}),
|
|
3397
|
+
lastFetchLabel(machine.lastProbeAt, Date.now()) !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3398
|
+
style: ageStyle,
|
|
3399
|
+
children: lastFetchLabel(machine.lastProbeAt, Date.now())
|
|
3400
|
+
}),
|
|
3401
|
+
machineVersionChip(machine) !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3402
|
+
style: chipWarn,
|
|
3403
|
+
title: `machine ${machine.id}`,
|
|
3404
|
+
children: machine.versionWarning
|
|
3405
|
+
}),
|
|
3406
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3407
|
+
type: "button",
|
|
3408
|
+
"aria-label": `Refresh ${machine.name}`,
|
|
3409
|
+
onClick: () => {
|
|
3410
|
+
reprobeRemote(machine.id);
|
|
3411
|
+
},
|
|
3412
|
+
style: textActionStyle,
|
|
3413
|
+
children: "refresh"
|
|
3414
|
+
}),
|
|
3415
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3416
|
+
type: "button",
|
|
3417
|
+
"aria-label": `Remove ${machine.name}`,
|
|
3418
|
+
onClick: () => {
|
|
3419
|
+
removeRemote(machine.id);
|
|
3420
|
+
},
|
|
3421
|
+
style: iconActionStyle,
|
|
3422
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
3423
|
+
width: "11",
|
|
3424
|
+
height: "11",
|
|
3425
|
+
viewBox: "0 0 11 11",
|
|
3426
|
+
fill: "none",
|
|
3427
|
+
"aria-hidden": "true",
|
|
3428
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
3429
|
+
d: "M2.5 2.5L8.5 8.5M8.5 2.5L2.5 8.5",
|
|
3430
|
+
stroke: "currentColor",
|
|
3431
|
+
strokeWidth: "1.4",
|
|
3432
|
+
strokeLinecap: "round"
|
|
3433
|
+
})
|
|
3434
|
+
})
|
|
3435
|
+
})
|
|
3436
|
+
] }, machine.id);
|
|
1804
3437
|
})
|
|
1805
|
-
]
|
|
3438
|
+
]
|
|
1806
3439
|
}),
|
|
1807
3440
|
showAdd && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1808
3441
|
style: {
|
|
@@ -1831,15 +3464,15 @@ function HubPage() {
|
|
|
1831
3464
|
})] }),
|
|
1832
3465
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1833
3466
|
style: fieldLabel,
|
|
1834
|
-
children: "URL"
|
|
3467
|
+
children: "Token URL"
|
|
1835
3468
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1836
3469
|
style: inputStyle,
|
|
1837
|
-
value: draft.
|
|
1838
|
-
placeholder: "
|
|
3470
|
+
value: draft.tokenUrl,
|
|
3471
|
+
placeholder: "https://192.168.31.130:4999/?token=…",
|
|
1839
3472
|
onChange: (event) => {
|
|
1840
3473
|
setDraft({
|
|
1841
3474
|
...draft,
|
|
1842
|
-
|
|
3475
|
+
tokenUrl: event.target.value
|
|
1843
3476
|
});
|
|
1844
3477
|
}
|
|
1845
3478
|
})] }),
|
|
@@ -1869,9 +3502,9 @@ function HubPage() {
|
|
|
1869
3502
|
children: "Cancel"
|
|
1870
3503
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1871
3504
|
type: "button",
|
|
1872
|
-
disabled: !draftValid,
|
|
3505
|
+
disabled: !draftValid || meshBusy,
|
|
1873
3506
|
onClick: () => {
|
|
1874
|
-
|
|
3507
|
+
addRemote();
|
|
1875
3508
|
},
|
|
1876
3509
|
style: {
|
|
1877
3510
|
padding: "7px 14px",
|
|
@@ -1922,71 +3555,23 @@ function HubPage() {
|
|
|
1922
3555
|
}),
|
|
1923
3556
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
|
|
1924
3557
|
style: listStyle,
|
|
1925
|
-
children: [
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
}),
|
|
1943
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1944
|
-
style: {
|
|
1945
|
-
fontWeight: 500,
|
|
1946
|
-
whiteSpace: "nowrap"
|
|
1947
|
-
},
|
|
1948
|
-
children: agent.label
|
|
1949
|
-
}),
|
|
1950
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1951
|
-
style: {
|
|
1952
|
-
flex: 1,
|
|
1953
|
-
minWidth: 0,
|
|
1954
|
-
overflow: "hidden",
|
|
1955
|
-
textOverflow: "ellipsis",
|
|
1956
|
-
whiteSpace: "nowrap",
|
|
1957
|
-
fontSize: 12.5,
|
|
1958
|
-
color: "var(--dsw-alias-label-tertiary)"
|
|
1959
|
-
},
|
|
1960
|
-
children: agent.machine
|
|
1961
|
-
}),
|
|
1962
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1963
|
-
style: {
|
|
1964
|
-
...chip,
|
|
1965
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
1966
|
-
fontSize: 10.5
|
|
1967
|
-
},
|
|
1968
|
-
children: displayPath(agent.path)
|
|
1969
|
-
}),
|
|
1970
|
-
isActive(agent) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1971
|
-
style: chipSelf,
|
|
1972
|
-
children: "current"
|
|
1973
|
-
}),
|
|
1974
|
-
switchUi.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1975
|
-
"aria-current": isActive(agent) ? "page" : void 0,
|
|
1976
|
-
style: {
|
|
1977
|
-
fontSize: 12.5,
|
|
1978
|
-
color: "var(--dsw-alias-label-secondary)",
|
|
1979
|
-
textDecoration: "none",
|
|
1980
|
-
cursor: "pointer"
|
|
1981
|
-
},
|
|
1982
|
-
onClick: (event) => {
|
|
1983
|
-
event.stopPropagation();
|
|
1984
|
-
rowDecision(agent, event);
|
|
1985
|
-
},
|
|
1986
|
-
children: "switch"
|
|
1987
|
-
})
|
|
1988
|
-
]
|
|
1989
|
-
}, agent.key))]
|
|
3558
|
+
children: [
|
|
3559
|
+
agents === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
3560
|
+
style: {
|
|
3561
|
+
padding: "9px 8px",
|
|
3562
|
+
fontSize: 13,
|
|
3563
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
3564
|
+
listStyle: "none"
|
|
3565
|
+
},
|
|
3566
|
+
children: "Loading roster…"
|
|
3567
|
+
}),
|
|
3568
|
+
groups !== void 0 && groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
3569
|
+
"aria-hidden": true,
|
|
3570
|
+
style: sectionHeadStyle,
|
|
3571
|
+
children: group.label
|
|
3572
|
+
}), group.agents.map((agent) => renderRow(agent, false))] }, group.key)),
|
|
3573
|
+
groups === void 0 && agents?.map((agent) => renderRow(agent, true))
|
|
3574
|
+
]
|
|
1990
3575
|
})
|
|
1991
3576
|
]
|
|
1992
3577
|
})] })
|
|
@@ -2046,6 +3631,7 @@ function HubPanelIcon({ size }) {
|
|
|
2046
3631
|
const [hover, setHover] = react.useState(false);
|
|
2047
3632
|
const [box, setBox] = react.useState(null);
|
|
2048
3633
|
const [agents, setAgents] = react.useState(() => rosterFromGlobal() ?? [NATIVE_LINK]);
|
|
3634
|
+
const [machineLabels, setMachineLabels] = react.useState(void 0);
|
|
2049
3635
|
const fusion = fusionPage();
|
|
2050
3636
|
const switchUi = switchAffordance(fusion);
|
|
2051
3637
|
const [activeKey, setActiveKey] = react.useState(() => fusion ? uiState()?.activeAgent : void 0);
|
|
@@ -2097,6 +3683,9 @@ function HubPanelIcon({ size }) {
|
|
|
2097
3683
|
fetchRoster().then((list) => {
|
|
2098
3684
|
if (!cancelled) setAgents(list);
|
|
2099
3685
|
}).catch(() => {});
|
|
3686
|
+
fetchMachines().then((list) => {
|
|
3687
|
+
if (!cancelled) setMachineLabels(machineLabelsFrom(list));
|
|
3688
|
+
}).catch(() => {});
|
|
2100
3689
|
return () => {
|
|
2101
3690
|
cancelled = true;
|
|
2102
3691
|
};
|
|
@@ -2119,15 +3708,15 @@ function HubPanelIcon({ size }) {
|
|
|
2119
3708
|
setOpen((value) => !value);
|
|
2120
3709
|
};
|
|
2121
3710
|
const items = [];
|
|
2122
|
-
|
|
3711
|
+
if (machineLabels !== void 0) groupByMachine(agents, machineLabels).forEach((group, groupIndex) => {
|
|
2123
3712
|
if (groupIndex > 0) items.push({
|
|
2124
3713
|
type: "separator",
|
|
2125
|
-
id: `machine-divider:${group.
|
|
3714
|
+
id: `machine-divider:${group.key}`
|
|
2126
3715
|
});
|
|
2127
3716
|
items.push({
|
|
2128
3717
|
type: "label",
|
|
2129
|
-
id: `machine:${group.
|
|
2130
|
-
text: group.
|
|
3718
|
+
id: `machine:${group.key}`,
|
|
3719
|
+
text: group.label
|
|
2131
3720
|
});
|
|
2132
3721
|
for (const agent of group.agents) items.push({
|
|
2133
3722
|
id: agent.key,
|
|
@@ -2138,6 +3727,14 @@ function HubPanelIcon({ size }) {
|
|
|
2138
3727
|
})
|
|
2139
3728
|
});
|
|
2140
3729
|
});
|
|
3730
|
+
else for (const agent of agents) items.push({
|
|
3731
|
+
id: agent.key,
|
|
3732
|
+
label: agent.label,
|
|
3733
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeMark, {
|
|
3734
|
+
agentKey: agent.key,
|
|
3735
|
+
size: 14
|
|
3736
|
+
})
|
|
3737
|
+
});
|
|
2141
3738
|
/**
|
|
2142
3739
|
* A menu pick. The row is a `menuitem` button — there is no anchor default
|
|
2143
3740
|
* to cancel — but the shared table still owns whether the attempt runs
|
|
@@ -2253,8 +3850,7 @@ function apply(ctx) {
|
|
|
2253
3850
|
"connection",
|
|
2254
3851
|
"uiWorkspace",
|
|
2255
3852
|
"workspaces",
|
|
2256
|
-
"sessions"
|
|
2257
|
-
"locale"
|
|
3853
|
+
"sessions"
|
|
2258
3854
|
], (raw) => {
|
|
2259
3855
|
bindSwitchAgent(raw);
|
|
2260
3856
|
});
|