@monster-orange/den 0.4.22 → 0.4.24
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 +73 -0
- package/docs/distributions.md +91 -30
- package/package.json +1 -1
- package/service/install.sh +5 -3
- package/src/shared/agents/guardrails/rotation-hook.ts +5 -0
- package/src/shared/agents/rotate-settings.ts +309 -0
- package/src/shared/agents/rotation.ts +110 -5
- package/src/shared/den-config.ts +27 -12
- package/src/ui/cli/agent-ops.ts +514 -15
- package/src/ui/cli/agents-command.ts +169 -3
- package/src/ui/cli/doctor-command.ts +24 -1
- package/src/ui/cli/doctor-where.ts +59 -13
- package/src/ui/cli/server-agent-ops.ts +2 -2
- package/src/ui/cli/web-command.ts +32 -5
- package/src/ui/cli/web-service.ts +160 -0
- package/src/ui/server/agent-routes.ts +161 -5
- package/src/ui/server/index.ts +13 -5
- package/src/ui/server/public/agents-page.js +214 -22
- package/src/ui/server/public/agents.html +32 -0
- package/src/ui/server/public/combobox.js +150 -0
- package/src/ui/server/public/error-page.js +26 -14
- package/src/ui/server/public/i18n/es.js +47 -4
- package/src/ui/server/public/i18n/ja.js +43 -4
- package/src/ui/server/public/settings-registry.js +318 -0
- package/src/ui/server/public/settings.html +166 -4
- package/src/ui/server/public/shell.js +62 -16
- package/src/ui/server/public/svc-panel.js +54 -15
- package/src/ui/server/route-classes.ts +21 -0
- package/src/ui/server/session-routes.ts +23 -5
- package/src/ui/server/ui-assets.ts +2 -0
|
@@ -67,6 +67,9 @@ const S = {
|
|
|
67
67
|
testing: new Set(),
|
|
68
68
|
/** The open panel: { id, el, opener, scrim, timer }. */
|
|
69
69
|
open: null,
|
|
70
|
+
/** D27: a pointer is down inside the panel: repaint after it is up. */
|
|
71
|
+
pressed: false,
|
|
72
|
+
syncLater: false,
|
|
70
73
|
};
|
|
71
74
|
|
|
72
75
|
/** Start or Restart from here: "starting…" until health says, or the timeout. */
|
|
@@ -228,14 +231,21 @@ function testsHtml(s) {
|
|
|
228
231
|
return `<span class="den-svp-${ok ? "ok" : "bad"}">${esc(t(ok ? "svp.testsOk" : "svp.testsBad", { t: at }))}</span>`;
|
|
229
232
|
}
|
|
230
233
|
|
|
231
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* Visit: a real link, styled as a button (D27). A native target=_blank link
|
|
236
|
+
* is never popup-blocked (Safari blocked window.open from the delegated
|
|
237
|
+
* click) and works with no script. Off (down, starting): no href,
|
|
238
|
+
* aria-disabled, and a click says why (visit()).
|
|
239
|
+
*/
|
|
232
240
|
export function visitHtml(s, cls, label) {
|
|
233
241
|
if (!s.url) return "";
|
|
234
|
-
const on = visitable(s);
|
|
242
|
+
const on = visitable(s) && /^https?:\/\//i.test(s.url);
|
|
235
243
|
return (
|
|
236
|
-
`<
|
|
237
|
-
|
|
238
|
-
|
|
244
|
+
`<a class="${cls}" data-visit="${esc(s.id)}" data-url="${esc(s.url)}"` +
|
|
245
|
+
(on
|
|
246
|
+
? ` href="${esc(s.url)}" target="_blank" rel="noopener noreferrer"`
|
|
247
|
+
: ` role="link" tabindex="0" aria-disabled="true"`) +
|
|
248
|
+
` aria-label="${esc(t("act.visitX", { s: s.id }))}" title="${esc(on ? s.url : t("svp.notRunning"))}">${label}</a>`
|
|
239
249
|
);
|
|
240
250
|
}
|
|
241
251
|
|
|
@@ -335,6 +345,12 @@ function sync() {
|
|
|
335
345
|
publishUrls();
|
|
336
346
|
const o = S.open;
|
|
337
347
|
if (!o) return;
|
|
348
|
+
// D27: never repaint under a press: the button pressed would be replaced
|
|
349
|
+
// between pointerdown and click, and the click would land on the panel.
|
|
350
|
+
if (S.pressed) {
|
|
351
|
+
S.syncLater = true;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
338
354
|
const a = document.activeElement;
|
|
339
355
|
const mine = a && o.el.contains(a) && a !== o.el;
|
|
340
356
|
const key = mine
|
|
@@ -451,15 +467,20 @@ export function panelOpen() {
|
|
|
451
467
|
const env = () => S.deps.currentEnv?.() ?? "local";
|
|
452
468
|
const runs = () => window.denShell?.runs;
|
|
453
469
|
|
|
454
|
-
/**
|
|
470
|
+
/**
|
|
471
|
+
* Visit, clicked: a live link opens by itself (the browser follows it, so
|
|
472
|
+
* true: leave the click alone); an off one says why (false: the caller
|
|
473
|
+
* prevents the click).
|
|
474
|
+
*/
|
|
455
475
|
export function visit(btn) {
|
|
456
|
-
if (
|
|
476
|
+
if (
|
|
477
|
+
btn.getAttribute("aria-disabled") === "true" ||
|
|
478
|
+
!btn.hasAttribute("href")
|
|
479
|
+
) {
|
|
457
480
|
window.denShell?.toast?.(t("svp.notRunning"));
|
|
458
|
-
return;
|
|
481
|
+
return false;
|
|
459
482
|
}
|
|
460
|
-
|
|
461
|
-
if (!url || !/^https?:\/\//i.test(url)) return;
|
|
462
|
-
window.open(url, "_blank", "noopener");
|
|
483
|
+
return true;
|
|
463
484
|
}
|
|
464
485
|
|
|
465
486
|
/** A stream into a runs-dock tab: Start, Stop, Restart (exec), Logs, tests. */
|
|
@@ -635,6 +656,27 @@ export function initServicePanel(deps = {}) {
|
|
|
635
656
|
sync();
|
|
636
657
|
});
|
|
637
658
|
window.addEventListener("den:pins", () => sync());
|
|
659
|
+
// D27: a press inside the open panel holds its repaint until it ends.
|
|
660
|
+
document.addEventListener(
|
|
661
|
+
"pointerdown",
|
|
662
|
+
(e) => {
|
|
663
|
+
if (S.open?.el.contains(e.target)) S.pressed = true;
|
|
664
|
+
},
|
|
665
|
+
true,
|
|
666
|
+
);
|
|
667
|
+
const released = () => {
|
|
668
|
+
if (!S.pressed) return;
|
|
669
|
+
S.pressed = false;
|
|
670
|
+
// After the click this press makes, then the repaint it held.
|
|
671
|
+
setTimeout(() => {
|
|
672
|
+
if (S.syncLater) {
|
|
673
|
+
S.syncLater = false;
|
|
674
|
+
sync();
|
|
675
|
+
}
|
|
676
|
+
}, 0);
|
|
677
|
+
};
|
|
678
|
+
document.addEventListener("pointerup", released, true);
|
|
679
|
+
document.addEventListener("pointercancel", released, true);
|
|
638
680
|
document.addEventListener(
|
|
639
681
|
"click",
|
|
640
682
|
(e) => {
|
|
@@ -643,10 +685,7 @@ export function initServicePanel(deps = {}) {
|
|
|
643
685
|
// The panel's Visit here; the rail's ↗ is shell.js's (the same visit()).
|
|
644
686
|
const v = tgt.closest?.("[data-visit]");
|
|
645
687
|
if (v) {
|
|
646
|
-
if (o?.el.contains(v))
|
|
647
|
-
e.preventDefault();
|
|
648
|
-
visit(v);
|
|
649
|
-
}
|
|
688
|
+
if (o?.el.contains(v) && !visit(v)) e.preventDefault();
|
|
650
689
|
return;
|
|
651
690
|
}
|
|
652
691
|
const opener = tgt.closest?.("[data-svpanel-open]");
|
|
@@ -374,6 +374,25 @@ export const ROUTES: readonly RouteEntry[] = [
|
|
|
374
374
|
"forgets every stopped agent of the workspaces named (AG-14.1b)",
|
|
375
375
|
),
|
|
376
376
|
M(["POST"], "/api/agents/restore", "Undo: brings removed agents back"),
|
|
377
|
+
M(
|
|
378
|
+
["POST"],
|
|
379
|
+
"/api/workspaces/:ws/agents/:id/keep",
|
|
380
|
+
"D28 Keep this conversation: resets its rotation count (and, asked, sets its override to never); a person's only",
|
|
381
|
+
),
|
|
382
|
+
S(
|
|
383
|
+
["POST"],
|
|
384
|
+
"/api/workspaces/:ws/agents/:id/handoff",
|
|
385
|
+
"D28 Write handoff: types the handoff prompt into the agent's running session",
|
|
386
|
+
),
|
|
387
|
+
R(
|
|
388
|
+
"/api/agents/rotation",
|
|
389
|
+
"D28: rotation settings, the overrides and an agent's effective values",
|
|
390
|
+
),
|
|
391
|
+
M(
|
|
392
|
+
["PUT"],
|
|
393
|
+
"/api/agents/rotation",
|
|
394
|
+
"D28: sets or clears one rotation setting at one scope; a person's only",
|
|
395
|
+
),
|
|
377
396
|
S(
|
|
378
397
|
["POST"],
|
|
379
398
|
"/api/workspaces/:ws/agents/:id/fresh",
|
|
@@ -504,6 +523,8 @@ export const REGEX_ROUTES: Readonly<Record<string, readonly string[]>> = {
|
|
|
504
523
|
"/api/workspaces/:ws/agents/:id/name",
|
|
505
524
|
"/api/workspaces/:ws/agents/:id/paths",
|
|
506
525
|
"/api/workspaces/:ws/agents/:id/persona",
|
|
526
|
+
"/api/workspaces/:ws/agents/:id/keep",
|
|
527
|
+
"/api/workspaces/:ws/agents/:id/handoff",
|
|
507
528
|
],
|
|
508
529
|
"agent-routes.ts:RUNTIME_PATH": ["/api/agents/runtimes/:id/path"],
|
|
509
530
|
"agent-routes.ts:PERSONA_PATH": ["/api/personas/:id"],
|
|
@@ -50,7 +50,11 @@ import { revokedAgentIds } from "../../shared/actor-token.js";
|
|
|
50
50
|
import { listRecords } from "../../shared/agents/records.js";
|
|
51
51
|
import { softNewlineSeq } from "../../shared/agents/catalog.js";
|
|
52
52
|
import { getRuntime } from "../../shared/agents/runtimes.js";
|
|
53
|
-
import type {
|
|
53
|
+
import type {
|
|
54
|
+
RotationEntry,
|
|
55
|
+
RotationInfo,
|
|
56
|
+
RotationPolicy,
|
|
57
|
+
} from "../../shared/agents/rotation.js";
|
|
54
58
|
import {
|
|
55
59
|
connectHost,
|
|
56
60
|
noHost,
|
|
@@ -61,7 +65,7 @@ import { sessionPaths } from "../../shared/sessions/paths.js";
|
|
|
61
65
|
import { loadPty, type PtyLoad } from "../../shared/sessions/pty.js";
|
|
62
66
|
import type { SessionInfo } from "../../shared/sessions/types.js";
|
|
63
67
|
import { agentRows, newerOf, noteEnded } from "../cli/agent-ops.js";
|
|
64
|
-
import type { Override } from "../../shared/agents/records.js";
|
|
68
|
+
import type { AgentRecord, Override } from "../../shared/agents/records.js";
|
|
65
69
|
import { state as sessionState } from "../cli/sessions-command.js";
|
|
66
70
|
import { recordAction } from "./audit.js";
|
|
67
71
|
import { sendJson } from "./http-helpers.js";
|
|
@@ -85,6 +89,8 @@ export interface SessionRouteDeps {
|
|
|
85
89
|
pty?: () => PtyLoad;
|
|
86
90
|
/** The agent ids with an approval waiting for the person (AG-6a, ON-3). */
|
|
87
91
|
waitingAgents?: () => ReadonlySet<string>;
|
|
92
|
+
/** D28: each agent's effective rotation settings (one resolver per list). */
|
|
93
|
+
rotationPolicy?: () => (rec: AgentRecord) => RotationPolicy | undefined;
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
interface Resolved {
|
|
@@ -95,6 +101,7 @@ interface Resolved {
|
|
|
95
101
|
workspaceExists: (ws: string) => boolean;
|
|
96
102
|
pty: () => PtyLoad;
|
|
97
103
|
waitingAgents: () => ReadonlySet<string>;
|
|
104
|
+
rotationPolicy?: SessionRouteDeps["rotationPolicy"];
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
/** A workspace name as `?ws=` may give it. */
|
|
@@ -125,6 +132,7 @@ function resolve(d: SessionRouteDeps = {}): Resolved {
|
|
|
125
132
|
)),
|
|
126
133
|
pty: d.pty ?? (() => loadPty()),
|
|
127
134
|
waitingAgents: d.waitingAgents ?? (() => new Set<string>()),
|
|
135
|
+
...(d.rotationPolicy ? { rotationPolicy: d.rotationPolicy } : {}),
|
|
128
136
|
};
|
|
129
137
|
}
|
|
130
138
|
|
|
@@ -199,13 +207,16 @@ export interface ListedSession {
|
|
|
199
207
|
* Relaunch from handoff is its action, not Resume.
|
|
200
208
|
*/
|
|
201
209
|
rotation?: RotationInfo;
|
|
210
|
+
/** D28: the last relaunch from a handoff (the page toasts one Den did by itself). */
|
|
211
|
+
lastRotation?: RotationEntry;
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
/** The list, from the host's sessions and the agent records. */
|
|
205
215
|
export function listedSessions(
|
|
206
216
|
sessions: SessionInfo[],
|
|
207
217
|
d: Pick<Resolved, "stateDir" | "dist" | "activeWorkspaces"> &
|
|
208
|
-
Partial<Pick<Resolved, "waitingAgents"
|
|
218
|
+
Partial<Pick<Resolved, "waitingAgents">> &
|
|
219
|
+
Pick<SessionRouteDeps, "rotationPolicy">,
|
|
209
220
|
/** A workspace asked for by name (`?ws=`), listed even when inactive. */
|
|
210
221
|
also?: string,
|
|
211
222
|
): ListedSession[] {
|
|
@@ -226,7 +237,12 @@ export function listedSessions(
|
|
|
226
237
|
const out: ListedSession[] = [];
|
|
227
238
|
const agentSessions = new Set<string>();
|
|
228
239
|
noteEnded(d.stateDir(), mine, records);
|
|
229
|
-
for (const r of agentRows(
|
|
240
|
+
for (const r of agentRows(
|
|
241
|
+
mine,
|
|
242
|
+
records,
|
|
243
|
+
d.stateDir(),
|
|
244
|
+
d.rotationPolicy?.(),
|
|
245
|
+
)) {
|
|
230
246
|
const rec = records.find((x) => x.id === r.agent)!;
|
|
231
247
|
const s = r.session;
|
|
232
248
|
if (s) agentSessions.add(s.ref.id);
|
|
@@ -262,7 +278,9 @@ export function listedSessions(
|
|
|
262
278
|
...(r.stoppedAt ? { stoppedAt: r.stoppedAt } : {}),
|
|
263
279
|
...(r.endedAt ? { endedAt: r.endedAt } : {}),
|
|
264
280
|
...(r.interruptedAt ? { interruptedAt: r.interruptedAt } : {}),
|
|
265
|
-
|
|
281
|
+
// D28: always (its effective settings show), due or not.
|
|
282
|
+
...(r.rotation ? { rotation: r.rotation } : {}),
|
|
283
|
+
...(r.lastRotation ? { lastRotation: r.lastRotation } : {}),
|
|
266
284
|
...(revoked.has(rec.id) && s && !s.exited ? { tokenRevoked: true } : {}),
|
|
267
285
|
...(waiting.has(rec.id) && s && !s.exited ? { waiting: true } : {}),
|
|
268
286
|
});
|
|
@@ -52,6 +52,8 @@ export const UI_MODULES: readonly string[] = [
|
|
|
52
52
|
"/server-agent-page.js",
|
|
53
53
|
// 0.4.18: the service panel (rail pins, Graph ⋯, Services names).
|
|
54
54
|
"/svc-panel.js",
|
|
55
|
+
// D30: the shared searchable picker (Settings › Agents › Rotation).
|
|
56
|
+
"/combobox.js",
|
|
55
57
|
// 0.4.18: where a service's live log comes from (Logs page and panel).
|
|
56
58
|
"/log-source.js",
|
|
57
59
|
];
|