@monster-orange/den 0.4.0 → 0.4.2
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/CHANGELOG.md +457 -0
- package/README.md +88 -158
- package/docs/distributions.md +216 -16
- package/docs/reference.md +177 -0
- package/package.json +3 -1
- package/src/shared/actor-token.ts +275 -0
- package/src/shared/agent-actor.ts +12 -2
- package/src/shared/agents/den-claude.ts +99 -1
- package/src/shared/agents/runtime.ts +7 -0
- package/src/shared/agents/runtimes.ts +9 -2
- package/src/shared/den-config.ts +52 -3
- package/src/shared/environment-manager/scope.ts +22 -0
- package/src/shared/environment-manager/service-runner.ts +94 -2
- package/src/shared/profile.ts +15 -4
- package/src/shared/sessions/client.ts +16 -2
- package/src/shared/sessions/den-pty.ts +10 -2
- package/src/shared/sessions/env.ts +13 -3
- package/src/shared/sessions/host.ts +56 -8
- package/src/shared/sessions/types.ts +8 -0
- package/src/shared/web-secret.ts +181 -0
- package/src/shared/web-session.ts +491 -0
- package/src/ui/cli/agent-ops.ts +843 -0
- package/src/ui/cli/agents-command.ts +97 -615
- package/src/ui/cli/flag-parser.ts +21 -0
- package/src/ui/cli/index.ts +127 -11
- package/src/ui/cli/sessions-command.ts +49 -5
- package/src/ui/cli/target-workspace.ts +105 -0
- package/src/ui/cli/web-command.ts +542 -14
- package/src/ui/mcp/api.ts +7 -0
- package/src/ui/mcp/server.ts +74 -6
- package/src/ui/server/agent-routes.ts +324 -0
- package/src/ui/server/audit.ts +81 -27
- package/src/ui/server/http-errors.ts +21 -3
- package/src/ui/server/index.ts +821 -66
- package/src/ui/server/public/agents-page.js +654 -0
- package/src/ui/server/public/agents-section.js +299 -0
- package/src/ui/server/public/agents.html +439 -0
- package/src/ui/server/public/appearance.js +3 -0
- package/src/ui/server/public/error-page.js +103 -0
- package/src/ui/server/public/header.js +45 -11
- package/src/ui/server/public/i18n/es.js +90 -1
- package/src/ui/server/public/i18n/ja.js +86 -1
- package/src/ui/server/public/local-dev.html +53 -0
- package/src/ui/server/public/logs.html +3 -0
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/release.html +8 -0
- package/src/ui/server/public/settings-page.js +5 -0
- package/src/ui/server/public/settings-registry.js +33 -0
- package/src/ui/server/public/shell.js +248 -5
- package/src/ui/server/public/topbar.js +51 -2
- package/src/ui/server/request-guard.ts +670 -0
- package/src/ui/server/route-classes.ts +317 -0
- package/src/ui/server/session-routes.ts +445 -0
- package/src/ui/server/shutdown-guard.ts +91 -0
- package/src/ui/server/sudo-store.ts +29 -3
- package/src/ui/server/ui-assets.ts +10 -1
- package/src/ui/server/websocket.ts +326 -0
- package/src/ui/server/workspace-routes.ts +13 -7
|
@@ -254,7 +254,7 @@
|
|
|
254
254
|
// The lock chip and its popover (R8).
|
|
255
255
|
"lock.locked": "Locked",
|
|
256
256
|
"lock.unlocked": "Unlocked",
|
|
257
|
-
"lock.title": "Unlock to start, stop and restart services",
|
|
257
|
+
"lock.title": "Unlock to start, stop and restart services and agents",
|
|
258
258
|
"lock.nopw": "Passwordless sudo (NOPASSWD): no admin password needed",
|
|
259
259
|
"lock.session": "Unlocked for this session",
|
|
260
260
|
"lock.sessionDesc":
|
|
@@ -353,6 +353,24 @@
|
|
|
353
353
|
"err.500.time": "Time",
|
|
354
354
|
"err.500.id": "Request id",
|
|
355
355
|
"err.500.version": "Version",
|
|
356
|
+
"err.421.title": "Wrong address",
|
|
357
|
+
"err.421.lead":
|
|
358
|
+
"This server answers only on its own addresses, so a page on another name can't reach it.",
|
|
359
|
+
"err.421.open": "Open on localhost",
|
|
360
|
+
"err.refused.title": "Request refused",
|
|
361
|
+
"err.signin.title": "Sign in to use {name}",
|
|
362
|
+
"err.signin.terminal": "Sign in to use the terminal",
|
|
363
|
+
"err.signin.agents": "Sign in to start agents",
|
|
364
|
+
"err.signin.sub": "This browser isn't signed in",
|
|
365
|
+
"err.signin.lead": "Run {cmd} and open the link.",
|
|
366
|
+
"err.signin.expiredTitle": "Sign-in link expired",
|
|
367
|
+
"err.signin.expired":
|
|
368
|
+
"That sign-in link expired or was used. Run {cmd} for a new one.",
|
|
369
|
+
"err.signin.local":
|
|
370
|
+
"A sign-in belongs to one address. This page is on {host}: open the same page on localhost, where this browser is signed in.",
|
|
371
|
+
"err.signin.why":
|
|
372
|
+
"There is no password: the link comes from a file only your user account can read.",
|
|
373
|
+
"err.signin.copy": "Copy command",
|
|
356
374
|
"err.422.title": "Config is invalid",
|
|
357
375
|
"err.422.sub": "The distribution config fails validation",
|
|
358
376
|
"err.422.lead":
|
|
@@ -388,6 +406,7 @@
|
|
|
388
406
|
"set.cli.cli_tabs": "Terminal tabs",
|
|
389
407
|
"set.cli.log_window_lines": "Log pane lines",
|
|
390
408
|
"set.cli.web_port": "Your web port",
|
|
409
|
+
"set.cli.signin": "Browser sign-in",
|
|
391
410
|
"set.distribution.brand": "Product name",
|
|
392
411
|
"set.distribution.github_org": "GitHub org",
|
|
393
412
|
"set.distribution.orgs": "Org labels",
|
|
@@ -397,6 +416,74 @@
|
|
|
397
416
|
"err.retry": "Retry",
|
|
398
417
|
"err.copy": "Copy details",
|
|
399
418
|
"err.copied": "Copied",
|
|
419
|
+
// The Workspaces page's Agents section (AG-4a, agents-section.js).
|
|
420
|
+
"ag.title": "Agents",
|
|
421
|
+
"ag.caption": "Agents in {ws}",
|
|
422
|
+
"ag.count": "{n} agents",
|
|
423
|
+
"ag.count.one": "{n} agent",
|
|
424
|
+
"ag.col.agent": "Agent",
|
|
425
|
+
"ag.col.runtime": "Runtime",
|
|
426
|
+
"ag.col.state": "State",
|
|
427
|
+
"ag.col.cwd": "Directory",
|
|
428
|
+
"ag.col.started": "Started",
|
|
429
|
+
"ag.col.actions": "Actions",
|
|
430
|
+
"ag.loading": "Loading agents…",
|
|
431
|
+
"ag.none": "No agents in this workspace yet",
|
|
432
|
+
"ag.personaOf": "persona {persona}",
|
|
433
|
+
"ag.state.running": "running",
|
|
434
|
+
"ag.state.stopping": "stopping…",
|
|
435
|
+
"ag.state.stopped": "stopped",
|
|
436
|
+
"ag.state.ended": "ended",
|
|
437
|
+
"ag.state.exited": "exited",
|
|
438
|
+
"ag.state.exitedCode": "exited ({code})",
|
|
439
|
+
"ag.stop": "Stop",
|
|
440
|
+
"ag.resume": "Resume",
|
|
441
|
+
"ag.stopName": "Stop {id}",
|
|
442
|
+
"ag.resumeName": "Resume {id}",
|
|
443
|
+
"ag.start": "+ Start agent",
|
|
444
|
+
"ag.persona": "Persona",
|
|
445
|
+
"ag.runtime": "Runtime",
|
|
446
|
+
"ag.runtime.auto": "The persona's",
|
|
447
|
+
"ag.unavailable": "unavailable",
|
|
448
|
+
"ag.go": "Start",
|
|
449
|
+
"ag.starting": "Starting…",
|
|
450
|
+
"ag.cancel": "Cancel",
|
|
451
|
+
"ag.started": "Started agent {id}",
|
|
452
|
+
"ag.stopped": "Stopped agent {id}",
|
|
453
|
+
"ag.resumed": "Resumed agent {id}",
|
|
454
|
+
// The Agents tab (AG-4a-term, agents-page.js).
|
|
455
|
+
"n.agents": "Agents",
|
|
456
|
+
"kind.agent": "Agent",
|
|
457
|
+
"ag.sub": "{n} sessions · {w} waiting · {scope}",
|
|
458
|
+
"ag.sub.one": "{n} session · {w} waiting · {scope}",
|
|
459
|
+
"ag.all": "all workspaces",
|
|
460
|
+
"ag.waiting": "waiting",
|
|
461
|
+
"ag.session": "session",
|
|
462
|
+
"ag.empty":
|
|
463
|
+
"Pick a session on the left to open its terminal. Open more to tile them (up to 4).",
|
|
464
|
+
"ag.none.scope": "No sessions or agents in {scope} yet",
|
|
465
|
+
"ag.detach": "Close pane",
|
|
466
|
+
"ag.detachName": "Close {id}'s pane (it keeps running)",
|
|
467
|
+
"ag.detached": "Closed {id}'s pane: it keeps running",
|
|
468
|
+
"ag.open": "Open",
|
|
469
|
+
"ag.openName": "Open {id} in Agents",
|
|
470
|
+
"ws.openAgents": "Open in Agents →",
|
|
471
|
+
"ag.signin": "Sign in to open terminals: run {cli} web login",
|
|
472
|
+
"ag.revoked": "token revoked",
|
|
473
|
+
"ag.revokedTip":
|
|
474
|
+
"Its token was revoked (web logout --all): stop and resume it, or start it again, to give it a new one",
|
|
475
|
+
"ag.ended":
|
|
476
|
+
"This session has ended. Its record stays until it is resumed or removed.",
|
|
477
|
+
"ag.exitedLine": "[the session exited{code}]",
|
|
478
|
+
"ag.lost": "[disconnected: {why}]",
|
|
479
|
+
"ag.reconnect": "Reconnect",
|
|
480
|
+
"ag.noBackend":
|
|
481
|
+
"Terminals need the optional PTY module (den-pty is unavailable): {cli} doctor says why",
|
|
482
|
+
"ag.noHost": "No session host is running: nothing has started yet",
|
|
483
|
+
"ag.ws": "Workspace",
|
|
484
|
+
"ag.pal": "Agents › {ws} / {name}",
|
|
485
|
+
"ag.palStart": "Start agent…",
|
|
486
|
+
"ag.cap": "Up to 4 panes: {id}'s closed to make room (it keeps running)",
|
|
400
487
|
};
|
|
401
488
|
// The languages the UI has a catalog for, each named in itself (a name is
|
|
402
489
|
// never translated). The server's list is ui-language.ts's UI_LANGUAGES.
|
|
@@ -479,6 +566,9 @@
|
|
|
479
566
|
settings:
|
|
480
567
|
'<circle cx="12" cy="12" r="3"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/>',
|
|
481
568
|
page: '<rect x="5" y="4" width="14" height="16" rx="2"/><path d="M9 9h6M9 13h6"/>',
|
|
569
|
+
// The prototype's Agents icon: a terminal window with a prompt.
|
|
570
|
+
agents:
|
|
571
|
+
'<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M7 10l3 2-3 2M12 15h5"/>',
|
|
482
572
|
edge: '<path d="M12 3l8 4v5c0 4.5-3.4 8.3-8 9-4.6-.7-8-4.5-8-9V7z"/><path d="M8 12h8M12 8v8"/>',
|
|
483
573
|
};
|
|
484
574
|
function icon(name) {
|
|
@@ -1146,6 +1236,17 @@
|
|
|
1146
1236
|
order: 15,
|
|
1147
1237
|
href: "/graph",
|
|
1148
1238
|
},
|
|
1239
|
+
// The Agents tab (AG-4a-term, AG spec §5d): sessions and terminal
|
|
1240
|
+
// panes, after Graph as the prototype orders them.
|
|
1241
|
+
{
|
|
1242
|
+
id: "agents",
|
|
1243
|
+
label: "n.agents",
|
|
1244
|
+
icon: "agents",
|
|
1245
|
+
group: "develop",
|
|
1246
|
+
order: 17,
|
|
1247
|
+
href: "/agents",
|
|
1248
|
+
keywords: ["sessions", "terminal"],
|
|
1249
|
+
},
|
|
1149
1250
|
{
|
|
1150
1251
|
id: "services",
|
|
1151
1252
|
label: "n.services",
|
|
@@ -3011,6 +3112,19 @@
|
|
|
3011
3112
|
: typeof data === "string" && data.trim()
|
|
3012
3113
|
? data.trim().slice(0, 300)
|
|
3013
3114
|
: t("api.failed", { what: what }) + " (" + r.status + ")";
|
|
3115
|
+
// SEC-1: a sign-in refusal in the page's language, with its command.
|
|
3116
|
+
if (
|
|
3117
|
+
data &&
|
|
3118
|
+
data.code === "signin_required" &&
|
|
3119
|
+
(data.scope === "agents" || data.scope === "terminal")
|
|
3120
|
+
) {
|
|
3121
|
+
var cmd = /`([^`]+)`/.exec(String(data.error || ""));
|
|
3122
|
+
msg =
|
|
3123
|
+
(data.scope === "agents"
|
|
3124
|
+
? t("err.signin.agents")
|
|
3125
|
+
: t("err.signin.terminal")) +
|
|
3126
|
+
(cmd ? ". " + t("err.signin.lead", { cmd: cmd[1] }) : "");
|
|
3127
|
+
}
|
|
3014
3128
|
throw apiError(msg, {
|
|
3015
3129
|
status: r.status,
|
|
3016
3130
|
code: data && typeof data === "object" ? data.code : null,
|
|
@@ -3247,12 +3361,43 @@
|
|
|
3247
3361
|
}
|
|
3248
3362
|
}
|
|
3249
3363
|
|
|
3364
|
+
// The config fails validation (0.4.1): the server marks every page it
|
|
3365
|
+
// serves while it does (data-config-invalid on #den-shell), and the first
|
|
3366
|
+
// /api answer of 422 config_invalid marks it too. From then on the shell
|
|
3367
|
+
// and its modules start none of the polls and streams that read the
|
|
3368
|
+
// config (health, edge, workspaces, applications, the org config, …):
|
|
3369
|
+
// each would only answer 422 again. A reload once fixed starts them.
|
|
3370
|
+
var configBad = host.dataset.configInvalid === "1";
|
|
3371
|
+
// SEC-1b: a refusal page (421 "Wrong address", or signed out): the server
|
|
3372
|
+
// marks it (data-api-off), since every /api call from it would answer
|
|
3373
|
+
// the same refusal. The shell starts none of its polls and streams, the
|
|
3374
|
+
// same gate as an invalid config, and the status chip reads unknown.
|
|
3375
|
+
var apiOffPage = host.dataset.apiOff === "1";
|
|
3376
|
+
function apiOff() {
|
|
3377
|
+
return apiOffPage;
|
|
3378
|
+
}
|
|
3379
|
+
function configInvalid() {
|
|
3380
|
+
return configBad || apiOffPage;
|
|
3381
|
+
}
|
|
3382
|
+
function markConfigInvalid() {
|
|
3383
|
+
if (configBad) return;
|
|
3384
|
+
configBad = true;
|
|
3385
|
+
try {
|
|
3386
|
+
window.dispatchEvent(new CustomEvent("den:config-invalid"));
|
|
3387
|
+
} catch (e) {
|
|
3388
|
+
/* no CustomEvent */
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3250
3392
|
// R26d: the server unreachable.
|
|
3251
3393
|
var server = { up: true, delay: 0, timer: null, tick: null, nextAt: 0 };
|
|
3252
3394
|
var rawFetch =
|
|
3253
3395
|
typeof window.fetch === "function" ? window.fetch.bind(window) : null;
|
|
3254
3396
|
function sameOrigin(input) {
|
|
3255
|
-
var u =
|
|
3397
|
+
var u =
|
|
3398
|
+
typeof input === "string"
|
|
3399
|
+
? input
|
|
3400
|
+
: input && (typeof input.url === "string" ? input.url : input.href);
|
|
3256
3401
|
if (!u) return false;
|
|
3257
3402
|
if (u.charAt(0) === "/" && u.charAt(1) !== "/") return true;
|
|
3258
3403
|
try {
|
|
@@ -3319,13 +3464,102 @@
|
|
|
3319
3464
|
hideBanner("net");
|
|
3320
3465
|
serverState(true);
|
|
3321
3466
|
}
|
|
3467
|
+
// SEC-1a: every same-origin call that changes something (POST, PUT,
|
|
3468
|
+
// PATCH, DELETE), through denShell.api() or a page's own fetch(), carries
|
|
3469
|
+
// X-Den-CSRF with the token GET /api/session hands out. The token is per
|
|
3470
|
+
// server start: a 403 `csrf` (the server restarted) fetches a fresh one
|
|
3471
|
+
// and retries once. Never sent to another origin.
|
|
3472
|
+
var CSRF_METHODS = { POST: 1, PUT: 1, PATCH: 1, DELETE: 1 };
|
|
3473
|
+
var csrf = { token: null, pending: null };
|
|
3474
|
+
function csrfToken(fresh) {
|
|
3475
|
+
if (fresh) csrf.token = null;
|
|
3476
|
+
if (csrf.token) return Promise.resolve(csrf.token);
|
|
3477
|
+
if (!csrf.pending)
|
|
3478
|
+
csrf.pending = rawFetch("/api/session", {
|
|
3479
|
+
cache: "no-store",
|
|
3480
|
+
credentials: "same-origin",
|
|
3481
|
+
})
|
|
3482
|
+
.then(function (r) {
|
|
3483
|
+
return r.ok ? r.json() : null;
|
|
3484
|
+
})
|
|
3485
|
+
.then(
|
|
3486
|
+
function (b) {
|
|
3487
|
+
csrf.pending = null;
|
|
3488
|
+
csrf.token = b && typeof b.csrf === "string" ? b.csrf : null;
|
|
3489
|
+
return csrf.token;
|
|
3490
|
+
},
|
|
3491
|
+
function () {
|
|
3492
|
+
csrf.pending = null;
|
|
3493
|
+
return null;
|
|
3494
|
+
},
|
|
3495
|
+
);
|
|
3496
|
+
return csrf.pending;
|
|
3497
|
+
}
|
|
3498
|
+
function isRequest(input) {
|
|
3499
|
+
return typeof Request !== "undefined" && input instanceof Request;
|
|
3500
|
+
}
|
|
3501
|
+
function methodOf(input, init) {
|
|
3502
|
+
var m = (init && init.method) || (isRequest(input) && input.method);
|
|
3503
|
+
return String(m || "GET").toUpperCase();
|
|
3504
|
+
}
|
|
3505
|
+
function withCsrf(input, init, token) {
|
|
3506
|
+
var headers = new Headers(
|
|
3507
|
+
(init && init.headers) || (isRequest(input) ? input.headers : undefined),
|
|
3508
|
+
);
|
|
3509
|
+
headers.set("X-Den-CSRF", token);
|
|
3510
|
+
return Object.assign({}, init || {}, { headers: headers });
|
|
3511
|
+
}
|
|
3512
|
+
function csrfFetch(input, init) {
|
|
3513
|
+
// A body that can be sent twice (not a stream) may be retried.
|
|
3514
|
+
var body = init && init.body;
|
|
3515
|
+
var again =
|
|
3516
|
+
!isRequest(input) &&
|
|
3517
|
+
(body == null ||
|
|
3518
|
+
typeof body === "string" ||
|
|
3519
|
+
(typeof FormData !== "undefined" && body instanceof FormData) ||
|
|
3520
|
+
(typeof URLSearchParams !== "undefined" &&
|
|
3521
|
+
body instanceof URLSearchParams) ||
|
|
3522
|
+
(typeof Blob !== "undefined" && body instanceof Blob));
|
|
3523
|
+
var send = function (token) {
|
|
3524
|
+
return rawFetch(input, token ? withCsrf(input, init, token) : init);
|
|
3525
|
+
};
|
|
3526
|
+
return csrfToken(false).then(function (token) {
|
|
3527
|
+
return send(token).then(function (r) {
|
|
3528
|
+
if (!again || !r || r.status !== 403) return r;
|
|
3529
|
+
return r
|
|
3530
|
+
.clone()
|
|
3531
|
+
.json()
|
|
3532
|
+
.then(
|
|
3533
|
+
function (b) {
|
|
3534
|
+
if (!b || b.code !== "csrf") return r;
|
|
3535
|
+
return csrfToken(true).then(function (fresh) {
|
|
3536
|
+
return fresh ? send(fresh) : r;
|
|
3537
|
+
});
|
|
3538
|
+
},
|
|
3539
|
+
function () {
|
|
3540
|
+
return r;
|
|
3541
|
+
},
|
|
3542
|
+
);
|
|
3543
|
+
});
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
3322
3546
|
if (rawFetch && !window.fetch.__denShell) {
|
|
3323
3547
|
var wrapped = function (input, init) {
|
|
3324
|
-
var p =
|
|
3548
|
+
var p =
|
|
3549
|
+
CSRF_METHODS[methodOf(input, init)] && sameOrigin(input)
|
|
3550
|
+
? csrfFetch(input, init)
|
|
3551
|
+
: rawFetch(input, init);
|
|
3325
3552
|
if (!p || typeof p.then !== "function") return p;
|
|
3326
3553
|
return p.then(
|
|
3327
3554
|
function (r) {
|
|
3328
3555
|
if (!server.up && sameOrigin(input)) serverBack();
|
|
3556
|
+
if (r && r.status === 422 && !configBad && sameOrigin(input))
|
|
3557
|
+
r.clone()
|
|
3558
|
+
.json()
|
|
3559
|
+
.then(function (b) {
|
|
3560
|
+
if (b && b.code === "config_invalid") markConfigInvalid();
|
|
3561
|
+
})
|
|
3562
|
+
.catch(function () {});
|
|
3329
3563
|
return r;
|
|
3330
3564
|
},
|
|
3331
3565
|
function (err) {
|
|
@@ -3337,6 +3571,8 @@
|
|
|
3337
3571
|
};
|
|
3338
3572
|
wrapped.__denShell = true;
|
|
3339
3573
|
window.fetch = wrapped;
|
|
3574
|
+
// The token is in hand before the first click needs it.
|
|
3575
|
+
if (!apiOffPage) csrfToken(false);
|
|
3340
3576
|
}
|
|
3341
3577
|
|
|
3342
3578
|
// ── the UI language (R19): a user pref ───────────────────────────────
|
|
@@ -3401,6 +3637,13 @@
|
|
|
3401
3637
|
serverUp: function () {
|
|
3402
3638
|
return server.up;
|
|
3403
3639
|
},
|
|
3640
|
+
/**
|
|
3641
|
+
* 0.4.1: true once the config is known to fail validation, or on a
|
|
3642
|
+
* refusal page (apiOff): the config polls and streams don't start.
|
|
3643
|
+
*/
|
|
3644
|
+
configInvalid: configInvalid,
|
|
3645
|
+
/** SEC-1b: a refusal page (421, signed out): no /api call would answer. */
|
|
3646
|
+
apiOff: apiOff,
|
|
3404
3647
|
/** The browser-tab icon (appearance.js: the active theme's, per mode). */
|
|
3405
3648
|
setFavicon: setFavicon,
|
|
3406
3649
|
/** The org part of the scope: "/ ORG <title>", static (one org). */
|
|
@@ -3480,7 +3723,7 @@
|
|
|
3480
3723
|
checkOrphansSoon();
|
|
3481
3724
|
|
|
3482
3725
|
// The server's prefs win over the local copy.
|
|
3483
|
-
if (typeof fetch === "function")
|
|
3726
|
+
if (typeof fetch === "function" && !apiOff())
|
|
3484
3727
|
fetch("/api/settings")
|
|
3485
3728
|
.then(function (r) {
|
|
3486
3729
|
return r.ok ? r.json() : null;
|
|
@@ -3502,7 +3745,7 @@
|
|
|
3502
3745
|
.catch(function () {});
|
|
3503
3746
|
|
|
3504
3747
|
// The Edge page shows once the edge is configured (no Docker asked).
|
|
3505
|
-
if (typeof fetch === "function")
|
|
3748
|
+
if (typeof fetch === "function" && !configInvalid())
|
|
3506
3749
|
fetch("/api/edge?probe=1")
|
|
3507
3750
|
.then(function (r) {
|
|
3508
3751
|
return r.ok ? r.json() : null;
|
|
@@ -411,7 +411,7 @@ export function runOf(url, base = "http://localhost/") {
|
|
|
411
411
|
const LS_LAST_RUN = "__DEN_CLI__-last-run";
|
|
412
412
|
const LS_LOCK = "__DEN_CLI__-lock";
|
|
413
413
|
const SS_PENDING = "__DEN_CLI__-pending-action";
|
|
414
|
-
const KIND = ["page", "service", "app", "action", "setting"];
|
|
414
|
+
const KIND = ["page", "service", "app", "agent", "action", "setting"];
|
|
415
415
|
const kindLabel = (k) =>
|
|
416
416
|
KIND.includes(k)
|
|
417
417
|
? t(`kind.${k}`)
|
|
@@ -507,7 +507,9 @@ let serverDown = false;
|
|
|
507
507
|
function paintChip() {
|
|
508
508
|
const chip = $("den-status");
|
|
509
509
|
if (!chip) return;
|
|
510
|
-
|
|
510
|
+
// R26d, and SEC-1b's refusal pages (421, signed out: sh().apiOff), where
|
|
511
|
+
// no health is asked for: unknown, neutral, never "Checking…" forever.
|
|
512
|
+
if (serverDown || sh()?.apiOff?.() === true) {
|
|
511
513
|
chip.classList.remove("bad");
|
|
512
514
|
chip.setAttribute("aria-busy", "false");
|
|
513
515
|
chip.title = t("status.unknownTip");
|
|
@@ -779,6 +781,25 @@ function openLogs(svc) {
|
|
|
779
781
|
|
|
780
782
|
// ── command palette (R17) ───────────────────────────────────────────────────
|
|
781
783
|
|
|
784
|
+
/**
|
|
785
|
+
* The sessions and agents in view (GET /api/sessions), asked when the
|
|
786
|
+
* palette opens: each is "Agents › <ws> / <name>", which opens its pane on
|
|
787
|
+
* the Agents page (AG-4a-term).
|
|
788
|
+
*/
|
|
789
|
+
let palSessions = [];
|
|
790
|
+
function askPaletteSessions(redraw) {
|
|
791
|
+
if (sh()?.apiOff?.()) return;
|
|
792
|
+
fetch("/api/sessions")
|
|
793
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
794
|
+
.then((d) => {
|
|
795
|
+
palSessions = Array.isArray(d?.sessions) ? d.sessions : [];
|
|
796
|
+
redraw();
|
|
797
|
+
})
|
|
798
|
+
.catch(() => {
|
|
799
|
+
/* no sessions to offer */
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
782
803
|
/** Every palette row: pages, apps, services, their actions, then commands. */
|
|
783
804
|
function paletteItems() {
|
|
784
805
|
const s = sh();
|
|
@@ -846,6 +867,26 @@ function paletteItems() {
|
|
|
846
867
|
run: () => openLogs(svcOf(id)),
|
|
847
868
|
});
|
|
848
869
|
}
|
|
870
|
+
for (const x of palSessions) {
|
|
871
|
+
const name = x.kind === "agent" ? (x.persona ?? x.label) : x.label;
|
|
872
|
+
items.push({
|
|
873
|
+
key: `agent:${x.id}`,
|
|
874
|
+
kind: "agent",
|
|
875
|
+
label: t("ag.pal", { ws: x.workspace, name }),
|
|
876
|
+
sub: x.kind === "agent" && x.persona !== x.label ? x.label : undefined,
|
|
877
|
+
dot: x.state === "running" ? "" : "off",
|
|
878
|
+
keywords: [x.workspace, x.label, x.persona ?? "", "agents", "terminal"],
|
|
879
|
+
run: () => go(`/agents?open=${encodeURIComponent(x.id)}`),
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
if (s?.nav?.().some((n) => n.id === "agents"))
|
|
883
|
+
items.push({
|
|
884
|
+
key: "agent:start",
|
|
885
|
+
kind: "action",
|
|
886
|
+
label: t("ag.palStart"),
|
|
887
|
+
keywords: ["agents", "start", "persona"],
|
|
888
|
+
run: () => go("/agents?start=1"),
|
|
889
|
+
});
|
|
849
890
|
for (const c of s?.commands?.() ?? [])
|
|
850
891
|
items.push({
|
|
851
892
|
key: `cmd:${c.id}`,
|
|
@@ -955,6 +996,9 @@ function openPalette() {
|
|
|
955
996
|
};
|
|
956
997
|
draw();
|
|
957
998
|
input.focus();
|
|
999
|
+
askPaletteSessions(() => {
|
|
1000
|
+
if (overlay?.kind === "palette") draw();
|
|
1001
|
+
});
|
|
958
1002
|
}
|
|
959
1003
|
|
|
960
1004
|
// ── Runs chip (R7) ──────────────────────────────────────────────────────────
|
|
@@ -1153,6 +1197,9 @@ async function getJson(url, fallback, init) {
|
|
|
1153
1197
|
*/
|
|
1154
1198
|
let vaultAsked = null;
|
|
1155
1199
|
function checkVault() {
|
|
1200
|
+
// 0.4.1: it reads the config's secrets refs; an invalid one answers 422.
|
|
1201
|
+
if (window.denShell?.configInvalid?.() === true)
|
|
1202
|
+
return Promise.resolve(lock.vault);
|
|
1156
1203
|
vaultAsked ??= getJson("/api/sudo-password/vault-status", {
|
|
1157
1204
|
backend: "none",
|
|
1158
1205
|
label: "",
|
|
@@ -1178,6 +1225,8 @@ function checkVault() {
|
|
|
1178
1225
|
}
|
|
1179
1226
|
|
|
1180
1227
|
async function initLock() {
|
|
1228
|
+
// A refusal page (SEC-1b): /api would answer the same refusal.
|
|
1229
|
+
if (sh()?.apiOff?.() === true) return;
|
|
1181
1230
|
try {
|
|
1182
1231
|
if (sessionStorage.getItem(LS_LOCK) === "1") {
|
|
1183
1232
|
lock.unlocked = true;
|