@monster-orange/den 0.4.3 → 0.4.4
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 +179 -0
- package/README.md +9 -9
- package/docs/distributions.md +215 -65
- package/package.json +1 -1
- package/src/shared/agents/catalog-runtime.ts +178 -0
- package/src/shared/agents/catalog.ts +343 -0
- package/src/shared/agents/den-claude.ts +14 -18
- package/src/shared/agents/runtime-search.ts +142 -15
- package/src/shared/agents/runtime.ts +18 -1
- package/src/shared/agents/runtimes.ts +23 -1
- package/src/shared/auth/sudo-vault.ts +16 -0
- package/src/shared/den-config.ts +57 -3
- package/src/shared/edge/status.ts +134 -0
- package/src/shared/environment-manager/health.ts +82 -8
- package/src/shared/environment-manager/workspaces.ts +54 -0
- package/src/shared/health/not-checked.ts +42 -0
- package/src/shared/health/rollup.ts +4 -0
- package/src/shared/process/spawn-streaming.ts +8 -3
- package/src/shared/sessions/client.ts +5 -1
- package/src/shared/sessions/host.ts +29 -1
- package/src/shared/sessions/protocol.ts +6 -1
- package/src/shared/sessions/types.ts +6 -0
- package/src/shared/stacks/status.ts +24 -0
- package/src/shared/strategies/registry.ts +5 -0
- package/src/shared/types/index.ts +6 -0
- package/src/ui/cli/agent-ops.ts +210 -19
- package/src/ui/cli/agents-command.ts +50 -21
- package/src/ui/cli/edge-command.ts +16 -3
- package/src/ui/cli/health-display.ts +4 -1
- package/src/ui/mcp/scope.ts +23 -3
- package/src/ui/mcp/server.ts +31 -7
- package/src/ui/mcp/tools.ts +327 -20
- package/src/ui/mcp/views.ts +450 -0
- package/src/ui/server/agent-routes.ts +118 -28
- package/src/ui/server/index.ts +77 -6
- package/src/ui/server/public/agent-start.js +46 -7
- package/src/ui/server/public/agents-page.js +232 -97
- package/src/ui/server/public/agents-section.js +3 -5
- package/src/ui/server/public/agents.html +12 -28
- package/src/ui/server/public/graph-model.js +2 -0
- package/src/ui/server/public/header.js +2 -3
- package/src/ui/server/public/health-rollup.js +20 -8
- package/src/ui/server/public/i18n/es.js +15 -13
- package/src/ui/server/public/i18n/ja.js +14 -13
- package/src/ui/server/public/index.html +17 -4
- package/src/ui/server/public/local-dev.html +28 -17
- package/src/ui/server/public/services.html +21 -5
- package/src/ui/server/public/settings-page.js +3 -2
- package/src/ui/server/public/settings-registry.js +225 -0
- package/src/ui/server/public/settings.html +90 -1
- package/src/ui/server/public/shell.js +28 -23
- package/src/ui/server/public/topbar.js +166 -206
- package/src/ui/server/readonly-routes.ts +29 -5
- package/src/ui/server/request-guard.ts +1 -1
- package/src/ui/server/route-classes.ts +14 -2
- package/src/ui/server/session-routes.ts +9 -1
- package/src/ui/server/sudo-store.ts +170 -6
- package/src/ui/server/workspace-routes.ts +10 -3
|
@@ -8,26 +8,30 @@
|
|
|
8
8
|
* the scrollback first, then the live output; keystrokes go to the
|
|
9
9
|
* session and the pane's size follows the pane. Closing a pane (or
|
|
10
10
|
* leaving the page) detaches: the session keeps running. Stop (an agent
|
|
11
|
-
* only
|
|
11
|
+
* only) is the Workspaces page's own agent route.
|
|
12
12
|
*
|
|
13
13
|
* The open panes are the URL (?open=<id>,<id>), so a reload, Back or a
|
|
14
14
|
* link reopens them, each reattaching with its scrollback.
|
|
15
15
|
*
|
|
16
|
+
* The size. A session has one PTY size and every viewer shares it (the
|
|
17
|
+
* last resize wins: another browser, a CLI attach). The server says the
|
|
18
|
+
* PTY's size on attach and whenever anyone changes it (a `size` frame), so
|
|
19
|
+
* a pane knows when it is out of step, and the pane in use wins: it says
|
|
20
|
+
* its own size when it is focused, when the page is shown again, and
|
|
21
|
+
* before the first keystroke after the PTY's size changed under it. A
|
|
22
|
+
* hidden pane never resizes the PTY, nor does one under 2x2. The cells
|
|
23
|
+
* are measured again once the theme's fonts have loaded (a web font
|
|
24
|
+
* arriving after the first fit would leave the cell width the fallback
|
|
25
|
+
* font's) and after the theme changes.
|
|
26
|
+
*
|
|
16
27
|
* Nothing here keeps a session's state: the list is GET /api/sessions,
|
|
17
28
|
* asked again every few seconds while the page is shown.
|
|
18
29
|
*
|
|
19
|
-
* Start agent (AG-0.2
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* can't, before Start (agent-start.js readiness).
|
|
30
|
+
* Start agent (AG-0.2): the runtime line says what will run, or why it
|
|
31
|
+
* can't, before Start (agent-start.js readiness). Starting, stopping and
|
|
32
|
+
* resuming an agent never ask for the admin password: it is for sudo, and
|
|
33
|
+
* no agent action runs sudo (LK-0).
|
|
24
34
|
*/
|
|
25
|
-
import {
|
|
26
|
-
checkUnlock,
|
|
27
|
-
onLockChange,
|
|
28
|
-
requireUnlock,
|
|
29
|
-
unlockWith,
|
|
30
|
-
} from "/topbar.js";
|
|
31
35
|
import { fillStartForm } from "/agent-start.js";
|
|
32
36
|
|
|
33
37
|
const t = (key, vars) => window.denShell?.t?.(key, vars) ?? key;
|
|
@@ -57,6 +61,7 @@ const st = {
|
|
|
57
61
|
timer: null,
|
|
58
62
|
xterm: null,
|
|
59
63
|
csrf: null,
|
|
64
|
+
docWired: false,
|
|
60
65
|
};
|
|
61
66
|
|
|
62
67
|
// ── the URL ─────────────────────────────────────────────────────────────
|
|
@@ -336,6 +341,100 @@ function send(p, obj) {
|
|
|
336
341
|
if (p.sock?.readyState === WebSocket.OPEN) p.sock.send(JSON.stringify(obj));
|
|
337
342
|
}
|
|
338
343
|
|
|
344
|
+
/** The smallest size a pane ever asks for (a pane not laid out yet). */
|
|
345
|
+
const MIN_COLS = 2;
|
|
346
|
+
const MIN_ROWS = 2;
|
|
347
|
+
|
|
348
|
+
/** Shown: the page is visible and the pane has a box. */
|
|
349
|
+
function paneShown(p) {
|
|
350
|
+
return (
|
|
351
|
+
!document.hidden &&
|
|
352
|
+
p.el.isConnected &&
|
|
353
|
+
p.el.offsetWidth > 0 &&
|
|
354
|
+
p.el.offsetHeight > 0
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Fit the terminal to its pane (its own cols and rows; nothing is sent). */
|
|
359
|
+
function refit(p) {
|
|
360
|
+
const box = p.el.querySelector(".term");
|
|
361
|
+
if (!p.fit || !p.el.isConnected || !box || box.clientWidth <= 0) return;
|
|
362
|
+
try {
|
|
363
|
+
p.fit.fit();
|
|
364
|
+
} catch {
|
|
365
|
+
/* not laid out yet */
|
|
366
|
+
}
|
|
367
|
+
markSize(p);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** The pane's size and the PTY's, on the pane (data-cols, data-pty-cols). */
|
|
371
|
+
function markSize(p) {
|
|
372
|
+
if (!p.term) return;
|
|
373
|
+
p.el.dataset.cols = String(p.term.cols);
|
|
374
|
+
p.el.dataset.rows = String(p.term.rows);
|
|
375
|
+
if (p.pty) {
|
|
376
|
+
p.el.dataset.ptyCols = String(p.pty.cols);
|
|
377
|
+
p.el.dataset.ptyRows = String(p.pty.rows);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Whether this pane must say its size before a keystroke: the PTY is known
|
|
383
|
+
* to have another size, or the host never says the PTY's size (an older
|
|
384
|
+
* session host that outlived an upgrade sends no size frames), so another
|
|
385
|
+
* viewer may have resized it unseen. Saying the same size again is free:
|
|
386
|
+
* the PTY only redraws when the size really changes.
|
|
387
|
+
*/
|
|
388
|
+
function outOfStep(p) {
|
|
389
|
+
if (!p.term) return false;
|
|
390
|
+
if (!p.heardSize) return true;
|
|
391
|
+
return !!p.pty && (p.pty.cols !== p.term.cols || p.pty.rows !== p.term.rows);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Say this pane's size to the session (the PTY takes it: last writer
|
|
396
|
+
* wins). Never from a hidden pane, never under 2x2.
|
|
397
|
+
*/
|
|
398
|
+
function sendSize(p) {
|
|
399
|
+
const term = p.term;
|
|
400
|
+
if (!term || p.sock?.readyState !== WebSocket.OPEN || !paneShown(p))
|
|
401
|
+
return false;
|
|
402
|
+
if (term.cols < MIN_COLS || term.rows < MIN_ROWS) return false;
|
|
403
|
+
send(p, { type: "resize", cols: term.cols, rows: term.rows });
|
|
404
|
+
// What the PTY will be: the server's frame follows (or another
|
|
405
|
+
// viewer's, if it resized after).
|
|
406
|
+
p.pty = { cols: term.cols, rows: term.rows };
|
|
407
|
+
markSize(p);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** This pane is in use: fit it and make the PTY its size. */
|
|
412
|
+
function claimSize(p) {
|
|
413
|
+
refit(p);
|
|
414
|
+
sendSize(p);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Measure the cells again (xterm measures them once, when the font is
|
|
419
|
+
* set) and refit: a web font that arrived after the first fit, or a theme
|
|
420
|
+
* whose --mono is another font. A change the PTY hears via onResize.
|
|
421
|
+
*/
|
|
422
|
+
function remeasure(p) {
|
|
423
|
+
const term = p.term;
|
|
424
|
+
if (!term) return;
|
|
425
|
+
const family = cssVar("--mono") ?? "monospace";
|
|
426
|
+
// Setting the same family is a no-op in xterm: set another first (never
|
|
427
|
+
// drawn: the theme's family is back before the refit).
|
|
428
|
+
if (term.options["fontFamily"] === family)
|
|
429
|
+
Object.assign(term.options, { fontFamily: `${family}, monospace` });
|
|
430
|
+
Object.assign(term.options, { fontFamily: family });
|
|
431
|
+
refit(p);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function remeasureAll() {
|
|
435
|
+
for (const p of st.panes.values()) remeasure(p);
|
|
436
|
+
}
|
|
437
|
+
|
|
339
438
|
async function connect(p) {
|
|
340
439
|
p.lost = false;
|
|
341
440
|
paintBar(p);
|
|
@@ -357,8 +456,10 @@ async function connect(p) {
|
|
|
357
456
|
let opened = false;
|
|
358
457
|
sock.onopen = () => {
|
|
359
458
|
opened = true;
|
|
360
|
-
|
|
361
|
-
|
|
459
|
+
p.pty = null;
|
|
460
|
+
p.heardSize = false;
|
|
461
|
+
refit(p);
|
|
462
|
+
sendSize(p);
|
|
362
463
|
};
|
|
363
464
|
sock.onmessage = (e) => {
|
|
364
465
|
if (typeof e.data === "string") {
|
|
@@ -368,7 +469,13 @@ async function connect(p) {
|
|
|
368
469
|
} catch {
|
|
369
470
|
return;
|
|
370
471
|
}
|
|
371
|
-
if (msg?.type === "
|
|
472
|
+
if (msg?.type === "size") {
|
|
473
|
+
if (Number.isInteger(msg.cols) && Number.isInteger(msg.rows)) {
|
|
474
|
+
p.heardSize = true;
|
|
475
|
+
p.pty = { cols: msg.cols, rows: msg.rows };
|
|
476
|
+
markSize(p);
|
|
477
|
+
}
|
|
478
|
+
} else if (msg?.type === "session") {
|
|
372
479
|
p.label = msg.label;
|
|
373
480
|
p.workspace = msg.workspace;
|
|
374
481
|
p.exited = !!msg.exited;
|
|
@@ -415,7 +522,16 @@ async function openPane(id) {
|
|
|
415
522
|
el.className = "pane";
|
|
416
523
|
el.dataset.id = id;
|
|
417
524
|
el.innerHTML = `<div class="bar2"></div><div class="term"></div>`;
|
|
418
|
-
const p = {
|
|
525
|
+
const p = {
|
|
526
|
+
id,
|
|
527
|
+
el,
|
|
528
|
+
term: null,
|
|
529
|
+
fit: null,
|
|
530
|
+
sock: null,
|
|
531
|
+
closed: false,
|
|
532
|
+
// The PTY's size as last heard (the server's size frame), or null.
|
|
533
|
+
pty: null,
|
|
534
|
+
};
|
|
419
535
|
st.panes.set(id, p);
|
|
420
536
|
st.open.push(id);
|
|
421
537
|
paintBar(p);
|
|
@@ -448,19 +564,21 @@ async function openPane(id) {
|
|
|
448
564
|
p.fit = fit;
|
|
449
565
|
const enc = new TextEncoder();
|
|
450
566
|
term.onData((d) => {
|
|
451
|
-
if (p.sock?.readyState
|
|
567
|
+
if (p.sock?.readyState !== WebSocket.OPEN) return;
|
|
568
|
+
// The one typing wins: another viewer resized the PTY since.
|
|
569
|
+
if (outOfStep(p)) claimSize(p);
|
|
570
|
+
p.sock.send(enc.encode(d));
|
|
452
571
|
});
|
|
453
|
-
term.onResize((
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
} catch {
|
|
458
|
-
/* not laid out yet */
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
p.ro = new ResizeObserver(() => refit());
|
|
572
|
+
term.onResize(() => sendSize(p));
|
|
573
|
+
// Focused (a click, the window coming back): this pane's size.
|
|
574
|
+
el.addEventListener("focusin", () => claimSize(p));
|
|
575
|
+
p.ro = new ResizeObserver(() => refit(p));
|
|
462
576
|
p.ro.observe(el.querySelector(".term"));
|
|
463
|
-
refit();
|
|
577
|
+
refit(p);
|
|
578
|
+
// The theme's font may still be loading: measure again once it has.
|
|
579
|
+
document.fonts?.ready?.then(() => {
|
|
580
|
+
if (!p.closed) remeasure(p);
|
|
581
|
+
});
|
|
464
582
|
void connect(p);
|
|
465
583
|
}
|
|
466
584
|
|
|
@@ -489,7 +607,7 @@ function closePane(id, { quiet = false } = {}) {
|
|
|
489
607
|
|
|
490
608
|
async function stopAgent(p, button) {
|
|
491
609
|
const r = rowOf(p.id);
|
|
492
|
-
if (!r?.agent
|
|
610
|
+
if (!r?.agent) return;
|
|
493
611
|
button.disabled = true;
|
|
494
612
|
try {
|
|
495
613
|
await api(
|
|
@@ -505,7 +623,54 @@ async function stopAgent(p, button) {
|
|
|
505
623
|
|
|
506
624
|
// ── Start agent ─────────────────────────────────────────────────────────
|
|
507
625
|
|
|
508
|
-
|
|
626
|
+
// Personas and runtimes, per workspace (AG-0.5: a workspace's own runtime
|
|
627
|
+
// paths win), asked once per page each.
|
|
628
|
+
const personasAsked = new Map();
|
|
629
|
+
|
|
630
|
+
function personasFor(ws) {
|
|
631
|
+
const key = ws || "";
|
|
632
|
+
if (!personasAsked.has(key))
|
|
633
|
+
personasAsked.set(
|
|
634
|
+
key,
|
|
635
|
+
api(
|
|
636
|
+
"GET",
|
|
637
|
+
`/api/personas${ws ? `?workspace=${encodeURIComponent(ws)}` : ""}`,
|
|
638
|
+
).catch((err) => {
|
|
639
|
+
personasAsked.delete(key);
|
|
640
|
+
throw err;
|
|
641
|
+
}),
|
|
642
|
+
);
|
|
643
|
+
return personasAsked.get(key);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Fill the form for the workspace picked; false when the ask failed. */
|
|
647
|
+
async function fillForWorkspace(ws) {
|
|
648
|
+
let data;
|
|
649
|
+
try {
|
|
650
|
+
data = await personasFor(ws);
|
|
651
|
+
} catch (err) {
|
|
652
|
+
window.denShell?.errorToast?.(err);
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
const form = st.root.querySelector("#ag-form");
|
|
656
|
+
const keep = [
|
|
657
|
+
form.querySelector("#ag-persona").value,
|
|
658
|
+
form.querySelector("#ag-runtime").value,
|
|
659
|
+
];
|
|
660
|
+
fillStartForm(form, data, t);
|
|
661
|
+
// The persona and runtime picked stay, where the new list still has them.
|
|
662
|
+
const ps = form.querySelector("#ag-persona");
|
|
663
|
+
if (keep[0] && [...ps.options].some((o) => o.value === keep[0])) {
|
|
664
|
+
ps.value = keep[0];
|
|
665
|
+
ps.onchange?.();
|
|
666
|
+
}
|
|
667
|
+
const rs = form.querySelector("#ag-runtime");
|
|
668
|
+
if ([...rs.options].some((o) => o.value === keep[1] && !o.disabled)) {
|
|
669
|
+
rs.value = keep[1];
|
|
670
|
+
rs.onchange?.();
|
|
671
|
+
}
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
509
674
|
|
|
510
675
|
async function openStart() {
|
|
511
676
|
const dlg = st.root.querySelector("#ag-dlg");
|
|
@@ -516,78 +681,16 @@ async function openStart() {
|
|
|
516
681
|
.map((w) => `<option value="${esc(w)}">${esc(w)}</option>`)
|
|
517
682
|
.join("");
|
|
518
683
|
st.root.querySelector("#ag-ws-l").hidden = shown.length <= 1;
|
|
519
|
-
|
|
520
|
-
personasAsked = null;
|
|
521
|
-
throw err;
|
|
522
|
-
});
|
|
523
|
-
let data;
|
|
524
|
-
try {
|
|
525
|
-
data = await personasAsked;
|
|
526
|
-
} catch (err) {
|
|
527
|
-
window.denShell?.errorToast?.(err);
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
fillStartForm(st.root.querySelector("#ag-form"), data, t);
|
|
531
|
-
// LK-1: the lock before the modal. Locked, its password is asked here.
|
|
532
|
-
paintInlineLock(!(await checkUnlock()));
|
|
684
|
+
if (!(await fillForWorkspace(wsSel.value || shown[0]))) return;
|
|
533
685
|
if (dlg.open) return;
|
|
534
686
|
if (typeof dlg.showModal === "function") dlg.showModal();
|
|
535
687
|
else dlg.setAttribute("open", "");
|
|
536
|
-
st.root.querySelector("#ag-lock:not([hidden]) input")?.focus();
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/** Show (or hide) the dialog's inline password field (LK-1). */
|
|
540
|
-
function paintInlineLock(locked) {
|
|
541
|
-
const box = st.root?.querySelector("#ag-lock");
|
|
542
|
-
if (!box) return;
|
|
543
|
-
box.hidden = !locked;
|
|
544
|
-
const err = box.querySelector("#ag-lock-err");
|
|
545
|
-
if (!locked) {
|
|
546
|
-
box.querySelector("#ag-lock-pw").value = "";
|
|
547
|
-
err.hidden = true;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* The lock, for Start: the dialog's own field when it shows one (unlock,
|
|
553
|
-
* then go on), else the shell's rule (its popover is in the top layer).
|
|
554
|
-
*/
|
|
555
|
-
async function unlockForStart(go) {
|
|
556
|
-
const box = st.root.querySelector("#ag-lock");
|
|
557
|
-
if (box.hidden) return requireUnlock();
|
|
558
|
-
if (await checkUnlock()) {
|
|
559
|
-
paintInlineLock(false);
|
|
560
|
-
return true;
|
|
561
|
-
}
|
|
562
|
-
const input = box.querySelector("#ag-lock-pw");
|
|
563
|
-
const err = box.querySelector("#ag-lock-err");
|
|
564
|
-
if (!input.value) {
|
|
565
|
-
err.textContent = t("lock.title");
|
|
566
|
-
err.hidden = false;
|
|
567
|
-
input.focus();
|
|
568
|
-
return false;
|
|
569
|
-
}
|
|
570
|
-
go.disabled = true;
|
|
571
|
-
go.textContent = t("lock.checking");
|
|
572
|
-
const r = await unlockWith(input.value);
|
|
573
|
-
go.disabled = false;
|
|
574
|
-
go.textContent = t("ag.go");
|
|
575
|
-
if (r.ok) {
|
|
576
|
-
paintInlineLock(false);
|
|
577
|
-
return true;
|
|
578
|
-
}
|
|
579
|
-
err.textContent = r.error;
|
|
580
|
-
err.hidden = false;
|
|
581
|
-
input.value = "";
|
|
582
|
-
input.focus();
|
|
583
|
-
return false;
|
|
584
688
|
}
|
|
585
689
|
|
|
586
690
|
async function submitStart(form) {
|
|
587
691
|
const dlg = st.root.querySelector("#ag-dlg");
|
|
588
692
|
const go = st.root.querySelector("#ag-go");
|
|
589
693
|
if (go.disabled) return;
|
|
590
|
-
if (!(await unlockForStart(go))) return;
|
|
591
694
|
const ws = form.querySelector("#ag-ws").value || viewWorkspaces()[0];
|
|
592
695
|
const persona = form.querySelector("#ag-persona").value;
|
|
593
696
|
const runtime = form.querySelector("#ag-runtime").value;
|
|
@@ -615,6 +718,40 @@ async function submitStart(form) {
|
|
|
615
718
|
}
|
|
616
719
|
}
|
|
617
720
|
|
|
721
|
+
/**
|
|
722
|
+
* Once per document: a font that loads later (the theme's --mono as a web
|
|
723
|
+
* font) and a theme applied without a reload (its sheet, then
|
|
724
|
+
* den:appearance) measure the cells again; the page shown again makes
|
|
725
|
+
* each pane say its size (another viewer may have taken the PTY while it
|
|
726
|
+
* was hidden).
|
|
727
|
+
*/
|
|
728
|
+
function wireDocument() {
|
|
729
|
+
st.docWired = true;
|
|
730
|
+
document.fonts?.addEventListener?.("loadingdone", remeasureAll);
|
|
731
|
+
window.addEventListener("den:appearance", () => {
|
|
732
|
+
for (const p of st.panes.values())
|
|
733
|
+
if (p.term) p.term.options.theme = termTheme();
|
|
734
|
+
remeasureAll();
|
|
735
|
+
});
|
|
736
|
+
document.addEventListener(
|
|
737
|
+
"load",
|
|
738
|
+
(e) => {
|
|
739
|
+
if (e.target?.id === "theme-css") {
|
|
740
|
+
for (const p of st.panes.values())
|
|
741
|
+
if (p.term) p.term.options.theme = termTheme();
|
|
742
|
+
remeasureAll();
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
true,
|
|
746
|
+
);
|
|
747
|
+
// Shown again: each pane says its size (another viewer may have taken
|
|
748
|
+
// the PTY while this page was hidden).
|
|
749
|
+
document.addEventListener("visibilitychange", () => {
|
|
750
|
+
if (document.hidden) return;
|
|
751
|
+
for (const p of st.panes.values()) claimSize(p);
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
618
755
|
// ── mount ───────────────────────────────────────────────────────────────
|
|
619
756
|
|
|
620
757
|
function translateStatic(root) {
|
|
@@ -630,8 +767,6 @@ function translateStatic(root) {
|
|
|
630
767
|
set("#ag-r-t", "ag.runtime");
|
|
631
768
|
set("#ag-cancel", "ag.cancel");
|
|
632
769
|
set("#ag-go", "ag.go");
|
|
633
|
-
set("#ag-lock-t", "lock.pw");
|
|
634
|
-
set("#ag-lock-note", "lock.title");
|
|
635
770
|
const h = root.querySelector("#ag-dlg-h");
|
|
636
771
|
if (h) h.textContent = h.textContent.replace(/^\+\s*/, "");
|
|
637
772
|
}
|
|
@@ -667,9 +802,8 @@ export async function mountAgentsPage(root) {
|
|
|
667
802
|
root.querySelector("#ag-start").addEventListener("click", () => {
|
|
668
803
|
void openStart();
|
|
669
804
|
});
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (unlocked) paintInlineLock(false);
|
|
805
|
+
root.querySelector("#ag-ws").addEventListener("change", (e) => {
|
|
806
|
+
void fillForWorkspace(e.currentTarget.value);
|
|
673
807
|
});
|
|
674
808
|
root.querySelector("#ag-cancel").addEventListener("click", () => {
|
|
675
809
|
root.querySelector("#ag-dlg").close?.();
|
|
@@ -695,6 +829,7 @@ export async function mountAgentsPage(root) {
|
|
|
695
829
|
attributes: true,
|
|
696
830
|
attributeFilter: ["data-mode", "data-theme", "class"],
|
|
697
831
|
});
|
|
832
|
+
if (!st.docWired) wireDocument();
|
|
698
833
|
window.addEventListener("pagehide", () => {
|
|
699
834
|
for (const p of st.panes.values()) {
|
|
700
835
|
p.closed = true;
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Every call is the server's agent routes (agent-routes.ts), which are the
|
|
9
9
|
* CLI's own operations: nothing here keeps an agent's state. Each action
|
|
10
|
-
* goes through denShell.api() (a refusal is its error toast)
|
|
11
|
-
*
|
|
10
|
+
* goes through denShell.api() (a refusal is its error toast). None asks
|
|
11
|
+
* for the admin password: it is for sudo, and no agent action runs sudo
|
|
12
|
+
* (LK-0). A row with a
|
|
12
13
|
* session has Open (its terminal on the Agents page, AG-4a-term), and the
|
|
13
14
|
* header links to the Agents page for the workspace.
|
|
14
15
|
*
|
|
15
16
|
* The page's table styles (table.svc, .tablewrap, .tbl-*) and the shell's
|
|
16
17
|
* (.den-sec, .den-btn, .den-dot) are reused as they are.
|
|
17
18
|
*/
|
|
18
|
-
import { requireUnlock } from "/topbar.js";
|
|
19
19
|
import { fillStartForm } from "/agent-start.js";
|
|
20
20
|
|
|
21
21
|
const t = (key, vars) => window.denShell?.t?.(key, vars) ?? key;
|
|
@@ -217,7 +217,6 @@ async function openForm(sec) {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
async function act(ws, verb, id, button) {
|
|
220
|
-
if (!(await requireUnlock())) return;
|
|
221
220
|
button.disabled = true;
|
|
222
221
|
try {
|
|
223
222
|
await api("POST", `${base(ws)}/${encodeURIComponent(id)}/${verb}`);
|
|
@@ -231,7 +230,6 @@ async function act(ws, verb, id, button) {
|
|
|
231
230
|
}
|
|
232
231
|
|
|
233
232
|
async function start(ws, form) {
|
|
234
|
-
if (!(await requireUnlock())) return;
|
|
235
233
|
const go = form.querySelector("#agent-start-go");
|
|
236
234
|
const persona = form.querySelector('select[name="persona"]').value;
|
|
237
235
|
const runtime = form.querySelector('select[name="runtime"]').value;
|
|
@@ -298,7 +298,17 @@
|
|
|
298
298
|
font-size: 12.5px;
|
|
299
299
|
}
|
|
300
300
|
dialog.ag-dlg {
|
|
301
|
-
|
|
301
|
+
/* The approved mock's Start agent dialog (AG-0.5), measured on
|
|
302
|
+
den-shell.html: .dlg.wide, min(600px, 100%) of content in a
|
|
303
|
+
content-box, so 600 + 2 x 18 padding + 2 x 1 border = 638
|
|
304
|
+
outside at 1440. Here the box is border-box: 638, and never
|
|
305
|
+
wider than the viewport less 16px a side (at 375 the mock's
|
|
306
|
+
box is 381 and overflows; this one is 343). */
|
|
307
|
+
box-sizing: border-box;
|
|
308
|
+
width: min(638px, calc(100vw - 32px));
|
|
309
|
+
max-width: none;
|
|
310
|
+
max-height: calc(100vh - 32px);
|
|
311
|
+
overflow-y: auto;
|
|
302
312
|
background: var(--raised);
|
|
303
313
|
color: var(--text);
|
|
304
314
|
border: 1px solid var(--border-strong);
|
|
@@ -324,8 +334,7 @@
|
|
|
324
334
|
}
|
|
325
335
|
.ag-dlg .help[hidden],
|
|
326
336
|
.ag-dlg .ready[hidden],
|
|
327
|
-
.ag-dlg .err[hidden]
|
|
328
|
-
.ag-dlg .ag-lock[hidden] {
|
|
337
|
+
.ag-dlg .err[hidden] {
|
|
329
338
|
display: none;
|
|
330
339
|
}
|
|
331
340
|
/* AG-0.2: the runtime it will run, or why it can't, before Start
|
|
@@ -342,20 +351,6 @@
|
|
|
342
351
|
.ag-dlg .ready .mono {
|
|
343
352
|
font-family: var(--mono);
|
|
344
353
|
}
|
|
345
|
-
/* LK-1: locked, the password is asked here, at the top of the dialog,
|
|
346
|
-
and Start unlocks then starts. */
|
|
347
|
-
.ag-dlg .ag-lock {
|
|
348
|
-
display: grid;
|
|
349
|
-
gap: 6px;
|
|
350
|
-
padding: 10px 12px;
|
|
351
|
-
border: 1px solid
|
|
352
|
-
color-mix(in srgb, var(--warn) 45%, var(--border));
|
|
353
|
-
border-radius: var(--radius-button);
|
|
354
|
-
background: color-mix(in srgb, var(--warn) 8%, var(--raised));
|
|
355
|
-
}
|
|
356
|
-
.ag-dlg .ag-lock .help {
|
|
357
|
-
margin: 0;
|
|
358
|
-
}
|
|
359
354
|
.ag-dlg .err {
|
|
360
355
|
margin: 0;
|
|
361
356
|
font: 12.5px/1.45 var(--sans);
|
|
@@ -447,17 +442,6 @@
|
|
|
447
442
|
<dialog class="ag-dlg" id="ag-dlg" aria-labelledby="ag-dlg-h">
|
|
448
443
|
<form method="dialog" id="ag-form">
|
|
449
444
|
<h2 id="ag-dlg-h">Start an agent</h2>
|
|
450
|
-
<div class="ag-lock" id="ag-lock" hidden>
|
|
451
|
-
<label
|
|
452
|
-
><span id="ag-lock-t">Admin password</span>
|
|
453
|
-
<input
|
|
454
|
-
type="password"
|
|
455
|
-
id="ag-lock-pw"
|
|
456
|
-
autocomplete="current-password"
|
|
457
|
-
/></label>
|
|
458
|
-
<p class="help" id="ag-lock-note"></p>
|
|
459
|
-
<p class="err" id="ag-lock-err" role="alert" hidden></p>
|
|
460
|
-
</div>
|
|
461
445
|
<label id="ag-ws-l"
|
|
462
446
|
><span id="ag-ws-t">Workspace</span>
|
|
463
447
|
<select name="ws" id="ag-ws"></select
|
|
@@ -156,6 +156,7 @@ export function applyHealth(m, results) {
|
|
|
156
156
|
check: h.check ?? null,
|
|
157
157
|
error: h.error ?? null,
|
|
158
158
|
status: h.status ?? null,
|
|
159
|
+
redirect: h.redirect ?? null,
|
|
159
160
|
ms:
|
|
160
161
|
typeof h.ms === "number"
|
|
161
162
|
? h.ms
|
|
@@ -232,6 +233,7 @@ export function repoRollup(m, repo) {
|
|
|
232
233
|
service: s.name,
|
|
233
234
|
ok: s.health.ok,
|
|
234
235
|
isEntrypoint: s.entry,
|
|
236
|
+
redirect: !!s.health.redirect,
|
|
235
237
|
})),
|
|
236
238
|
);
|
|
237
239
|
}
|
|
@@ -30,12 +30,11 @@ const escT = (key, vars) =>
|
|
|
30
30
|
);
|
|
31
31
|
|
|
32
32
|
export {
|
|
33
|
-
|
|
34
|
-
onLockChange,
|
|
33
|
+
askSudo,
|
|
35
34
|
problemCards,
|
|
36
35
|
problemFix,
|
|
37
|
-
requireUnlock,
|
|
38
36
|
setServiceRunner,
|
|
37
|
+
sudoRun,
|
|
39
38
|
takePendingAction,
|
|
40
39
|
} from "./topbar.js";
|
|
41
40
|
export { buildRow } from "./health-fit.js";
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
export const STATE_RANK = { down: 0, degraded: 1, ok: 2, unknown: 3 };
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* @typedef {{ service: string, ok: boolean | null | undefined, isEntrypoint?: boolean, awaiting?: boolean }} HealthSignal
|
|
30
|
+
* @typedef {{ service: string, ok: boolean | null | undefined, isEntrypoint?: boolean, awaiting?: boolean, redirect?: boolean }} HealthSignal
|
|
31
31
|
* @typedef {"down" | "degraded" | "ok" | "unknown"} RepoState
|
|
32
|
-
* @typedef {{ state: RepoState, entry: string | null, down: string[], checked: number, unchecked: number, awaiting: number }} RepoRollup
|
|
32
|
+
* @typedef {{ state: RepoState, entry: string | null, down: string[], checked: number, unchecked: number, awaiting: number, redirected?: number }} RepoRollup
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -60,6 +60,11 @@ export function rollupRepo(repo, signals) {
|
|
|
60
60
|
const answered = signals.filter((s) => !s.awaiting);
|
|
61
61
|
const entry = entrySignal(repo, answered);
|
|
62
62
|
const checked = answered.filter((s) => s.ok === true || s.ok === false);
|
|
63
|
+
// A redirect (ok null, `redirect`) answered: not down, not up, and not
|
|
64
|
+
// "not checked" either; counted on its own.
|
|
65
|
+
const redirected = answered.filter(
|
|
66
|
+
(s) => s.redirect && s.ok !== true && s.ok !== false,
|
|
67
|
+
).length;
|
|
63
68
|
const down = awaiting
|
|
64
69
|
? []
|
|
65
70
|
: checked.filter((s) => s.ok === false).map((s) => s.service);
|
|
@@ -77,8 +82,9 @@ export function rollupRepo(repo, signals) {
|
|
|
77
82
|
entry: entry ? entry.service : null,
|
|
78
83
|
down,
|
|
79
84
|
checked: checked.length,
|
|
80
|
-
unchecked: answered.length - checked.length,
|
|
85
|
+
unchecked: answered.length - checked.length - redirected,
|
|
81
86
|
awaiting,
|
|
87
|
+
...(redirected ? { redirected } : {}),
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -99,16 +105,22 @@ export function rollupRows(rows) {
|
|
|
99
105
|
|
|
100
106
|
/**
|
|
101
107
|
* "down (web down)", "degraded (mailpit down)", "healthy",
|
|
102
|
-
* "healthy, 25 not checked", "unknown (not
|
|
108
|
+
* "healthy, 25 not checked", "healthy, 1 redirect", "unknown (not
|
|
109
|
+
* checked)", "unknown (1 redirect)".
|
|
103
110
|
* @param {RepoRollup} r
|
|
104
111
|
* @returns {string}
|
|
105
112
|
*/
|
|
106
113
|
export function describeRollup(r) {
|
|
107
|
-
const
|
|
114
|
+
const extra = [
|
|
115
|
+
r.redirected ? `${r.redirected} redirect` : "",
|
|
116
|
+
r.unchecked > 0 ? `${r.unchecked} not checked` : "",
|
|
117
|
+
]
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join(", ");
|
|
108
120
|
if (r.awaiting) return "unknown (still checking)";
|
|
109
|
-
if (r.state === "unknown")
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
if (r.state === "unknown")
|
|
122
|
+
return r.redirected ? `unknown (${extra})` : "unknown (not checked)";
|
|
123
|
+
if (r.state === "ok") return extra ? `healthy, ${extra}` : "healthy";
|
|
112
124
|
return `${r.state} (${r.down.join(", ")} down)`;
|
|
113
125
|
}
|
|
114
126
|
|