@monster-orange/den 0.4.26 → 0.4.28
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 +88 -0
- package/docs/distributions.md +145 -20
- package/package.json +1 -1
- package/src/shared/agents/claude-helpers.ts +623 -0
- package/src/shared/agents/den-claude.ts +54 -0
- package/src/shared/agents/helpers.ts +175 -0
- package/src/shared/agents/runtime.ts +15 -2
- package/src/shared/den-config.ts +42 -1
- package/src/shared/hub.ts +314 -82
- package/src/shared/secrets/onepassword.ts +52 -15
- package/src/ui/cli/agent-ops.ts +161 -3
- package/src/ui/cli/agents-command.ts +127 -1
- package/src/ui/cli/doctor-command.ts +11 -2
- package/src/ui/mcp/api.ts +8 -2
- package/src/ui/mcp/server.ts +13 -1
- package/src/ui/mcp/tools.ts +50 -0
- package/src/ui/server/agent-routes.ts +133 -1
- package/src/ui/server/hub-connect.ts +499 -0
- package/src/ui/server/index.ts +7 -1
- package/src/ui/server/public/agent-helpers.js +311 -0
- package/src/ui/server/public/agents-page.js +76 -9
- package/src/ui/server/public/i18n/es.js +41 -0
- package/src/ui/server/public/i18n/ja.js +39 -0
- package/src/ui/server/public/settings-registry.js +232 -33
- package/src/ui/server/public/settings.html +23 -0
- package/src/ui/server/public/shell.js +201 -0
- package/src/ui/server/public/topbar.js +5 -1
- package/src/ui/server/public/transcript-view.js +202 -0
- package/src/ui/server/request-guard.ts +3 -1
- package/src/ui/server/route-classes.ts +33 -0
- package/src/ui/server/session-routes.ts +71 -1
- package/src/ui/server/ui-assets.ts +3 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A read-only transcript view, shared (D32: a helper's transcript; later
|
|
3
|
+
* an agent's own). Items are { at?, kind: prompt|text|tool|result|note,
|
|
4
|
+
* tool?, text, error? }, as the server summarizes them: tool calls in a few
|
|
5
|
+
* words, their results under them, never thinking.
|
|
6
|
+
*
|
|
7
|
+
* openTranscript({ title, subtitle?, note?, load, opener? }) shows it in a
|
|
8
|
+
* modal <dialog> of its own (made once, kept in <body>): labelled, focus on
|
|
9
|
+
* its heading, Escape or Close gives focus back to the opener. `load()`
|
|
10
|
+
* resolves { items, omitted } (or rejects with an Error to show).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const t = (key, vars) => window.denShell?.t?.(key, vars) ?? key;
|
|
14
|
+
const esc = (s) =>
|
|
15
|
+
String(s ?? "").replace(
|
|
16
|
+
/[&<>"']/g,
|
|
17
|
+
(c) =>
|
|
18
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
19
|
+
c
|
|
20
|
+
],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const time = (ms) => {
|
|
24
|
+
if (typeof ms !== "number") return "";
|
|
25
|
+
try {
|
|
26
|
+
return new Date(ms).toLocaleTimeString([], {
|
|
27
|
+
hour: "2-digit",
|
|
28
|
+
minute: "2-digit",
|
|
29
|
+
second: "2-digit",
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** The items as a list (no dialog): for a page that shows them in place. */
|
|
37
|
+
export function transcriptHtml(items, { omitted = 0 } = {}) {
|
|
38
|
+
const rows = (items ?? []).map((i) => {
|
|
39
|
+
const at = i.at
|
|
40
|
+
? `<time class="tx-at" datetime="${esc(new Date(i.at).toISOString())}">${esc(time(i.at))}</time>`
|
|
41
|
+
: "";
|
|
42
|
+
if (i.kind === "tool")
|
|
43
|
+
return `<li class="tx-i tx-tool">${at}<span class="tx-k">${esc(i.tool || "tool")}</span><span class="tx-t mono">${esc(i.text)}</span></li>`;
|
|
44
|
+
if (i.kind === "result")
|
|
45
|
+
return `<li class="tx-i tx-res${i.error ? " err" : ""}">${at}<span class="tx-k" aria-label="${esc(t(i.error ? "tx.error" : "tx.result"))}">${i.error ? "✗" : "←"}</span><span class="tx-t mono">${esc(i.text)}</span></li>`;
|
|
46
|
+
if (i.kind === "prompt")
|
|
47
|
+
return `<li class="tx-i tx-prompt">${at}<span class="tx-k">${esc(t("tx.asked"))}</span><span class="tx-t">${esc(i.text)}</span></li>`;
|
|
48
|
+
if (i.kind === "note")
|
|
49
|
+
return `<li class="tx-i tx-note"><span class="tx-t">${esc(i.text)}</span></li>`;
|
|
50
|
+
return `<li class="tx-i tx-text">${at}<span class="tx-t">${esc(i.text)}</span></li>`;
|
|
51
|
+
});
|
|
52
|
+
return (
|
|
53
|
+
(omitted
|
|
54
|
+
? `<p class="tx-omit">${esc(t("tx.omitted", { n: omitted }))}</p>`
|
|
55
|
+
: "") +
|
|
56
|
+
(rows.length
|
|
57
|
+
? `<ol class="tx-list">${rows.join("")}</ol>`
|
|
58
|
+
: `<p class="tx-empty">${esc(t("tx.empty"))}</p>`)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const CSS = `
|
|
63
|
+
.tx-dlg{font-family:var(--sans);box-shadow:var(--shadow);width:min(860px,calc(100vw - 32px));max-height:calc(100vh - 48px);padding:0;border:1px solid var(--border-strong);border-radius:10px;background:var(--raised, var(--surface));color:var(--text)}
|
|
64
|
+
.tx-dlg::backdrop{background:rgba(0,0,0,.35)}
|
|
65
|
+
.tx-hd{display:flex;gap:8px;align-items:baseline;padding:12px 16px;border-bottom:1px solid var(--border)}
|
|
66
|
+
.tx-hd h2{font-size:15px;margin:0;outline:none;min-width:0;overflow-wrap:anywhere}
|
|
67
|
+
.tx-sub{color:var(--muted);font-size:12px;min-width:0;overflow-wrap:anywhere}
|
|
68
|
+
.tx-hd .x{margin-left:auto;flex:none}
|
|
69
|
+
.tx-body{padding:8px 16px 4px;overflow:auto;max-height:calc(100vh - 200px)}
|
|
70
|
+
.tx-note{margin:0;padding:8px 16px 12px;color:var(--muted);font-size:12px;border-top:1px solid var(--border)}
|
|
71
|
+
.tx-list{list-style:none;margin:0;padding:0}
|
|
72
|
+
.tx-i{display:grid;grid-template-columns:auto auto 1fr;gap:4px 8px;padding:4px 0;font-size:13px;align-items:baseline;border-bottom:1px dashed var(--border)}
|
|
73
|
+
.tx-i:last-child{border-bottom:0}
|
|
74
|
+
.tx-at{color:var(--faint);font-size:11px;font-variant-numeric:tabular-nums}
|
|
75
|
+
.tx-k{color:var(--muted);font-weight:600;font-size:12px}
|
|
76
|
+
.tx-t{grid-column:3;min-width:0;overflow-wrap:anywhere;white-space:pre-wrap}
|
|
77
|
+
.tx-text .tx-t,.tx-prompt .tx-t,.tx-note .tx-t{grid-column:2 / 4}
|
|
78
|
+
.tx-text:not(:has(.tx-at)) .tx-t,.tx-note .tx-t{grid-column:1 / 4}
|
|
79
|
+
.tx-t.mono{font-family:var(--mono);font-size:12px}
|
|
80
|
+
.tx-res .tx-t{color:var(--muted)}
|
|
81
|
+
.tx-res.err .tx-k,.tx-res.err .tx-t{color:var(--bad)}
|
|
82
|
+
.tx-omit,.tx-empty{color:var(--muted);font-size:12px}
|
|
83
|
+
@media (max-width:520px){.tx-i{grid-template-columns:auto 1fr}.tx-at{display:none}.tx-t{grid-column:2}.tx-text .tx-t,.tx-prompt .tx-t{grid-column:1 / 3}}
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
let dlg = null;
|
|
87
|
+
let opener = null;
|
|
88
|
+
let openerKey = "";
|
|
89
|
+
|
|
90
|
+
const q = (v) =>
|
|
91
|
+
typeof CSS !== "undefined" && CSS.escape
|
|
92
|
+
? CSS.escape(String(v))
|
|
93
|
+
: String(v).replace(/["\\]/g, "\\$&");
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* How to find `el` again once a page redraws it (the Agents page's poll
|
|
97
|
+
* replaces its rows and bars): a helper's row, an agent's "All helpers",
|
|
98
|
+
* a pane's Helpers button. "" for anything else.
|
|
99
|
+
*/
|
|
100
|
+
export function refindKey(el) {
|
|
101
|
+
if (!el || !el.matches) return "";
|
|
102
|
+
if (el.matches("button.agh[data-helper]"))
|
|
103
|
+
return `button.agh[data-helper="${q(el.dataset.helper)}"][data-agent="${q(el.dataset.agent)}"]`;
|
|
104
|
+
const g = el.closest?.(".aghs[data-helpers-of]");
|
|
105
|
+
if (g && el.matches(".aghs-all"))
|
|
106
|
+
return `.aghs[data-helpers-of="${q(g.dataset.helpersOf)}"][data-ws="${q(g.dataset.ws)}"] .aghs-all`;
|
|
107
|
+
const pane = el.closest?.(".pane[data-id]");
|
|
108
|
+
if (pane && el.matches('[data-act="helpers"]'))
|
|
109
|
+
return `.pane[data-id="${q(pane.dataset.id)}"] [data-act="helpers"]`;
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Focus `el`, or, once it is gone, what `key` finds now (refindKey). */
|
|
114
|
+
export function focusBack(el, key) {
|
|
115
|
+
if (el?.isConnected) return void el.focus?.();
|
|
116
|
+
if (!key) return;
|
|
117
|
+
const now = Array.from(document.querySelectorAll(key)).find(
|
|
118
|
+
(x) => !x.closest("dialog"),
|
|
119
|
+
);
|
|
120
|
+
now?.focus?.();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function dialog() {
|
|
124
|
+
if (dlg && dlg.isConnected) return dlg;
|
|
125
|
+
if (!document.getElementById("tx-css")) {
|
|
126
|
+
const s = document.createElement("style");
|
|
127
|
+
s.id = "tx-css";
|
|
128
|
+
s.textContent = CSS;
|
|
129
|
+
document.head.append(s);
|
|
130
|
+
}
|
|
131
|
+
dlg = document.createElement("dialog");
|
|
132
|
+
dlg.className = "tx-dlg";
|
|
133
|
+
dlg.id = "tx-dlg";
|
|
134
|
+
dlg.setAttribute("aria-labelledby", "tx-h");
|
|
135
|
+
dlg.innerHTML =
|
|
136
|
+
`<div class="tx-hd"><h2 id="tx-h" tabindex="-1"></h2><span class="tx-sub" id="tx-sub"></span>` +
|
|
137
|
+
`<button type="button" class="den-btn sm x" data-tx-close></button></div>` +
|
|
138
|
+
`<div class="tx-body" id="tx-body" aria-live="polite"></div>` +
|
|
139
|
+
`<p class="tx-note" id="tx-note" hidden></p>`;
|
|
140
|
+
dlg.querySelector("[data-tx-close]").addEventListener("click", () => close());
|
|
141
|
+
dlg.addEventListener("close", () => {
|
|
142
|
+
const o = opener;
|
|
143
|
+
const k = openerKey;
|
|
144
|
+
opener = null;
|
|
145
|
+
openerKey = "";
|
|
146
|
+
focusBack(o, k);
|
|
147
|
+
});
|
|
148
|
+
document.body.append(dlg);
|
|
149
|
+
return dlg;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function close() {
|
|
153
|
+
if (!dlg) return;
|
|
154
|
+
if (typeof dlg.close === "function" && dlg.open) dlg.close();
|
|
155
|
+
else {
|
|
156
|
+
dlg.removeAttribute("open");
|
|
157
|
+
dlg.dispatchEvent(new Event("close"));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Show a transcript (see the file's comment). Resolves once it is drawn. */
|
|
162
|
+
export async function openTranscript({
|
|
163
|
+
title,
|
|
164
|
+
subtitle = "",
|
|
165
|
+
note = "",
|
|
166
|
+
load,
|
|
167
|
+
opener: from = null,
|
|
168
|
+
}) {
|
|
169
|
+
const d = dialog();
|
|
170
|
+
opener = from ?? document.activeElement;
|
|
171
|
+
openerKey = refindKey(opener);
|
|
172
|
+
d.querySelector("#tx-h").textContent = title;
|
|
173
|
+
d.querySelector("#tx-sub").textContent = subtitle;
|
|
174
|
+
const x = d.querySelector("[data-tx-close]");
|
|
175
|
+
x.textContent = t("tx.close");
|
|
176
|
+
const n = d.querySelector("#tx-note");
|
|
177
|
+
n.textContent = note;
|
|
178
|
+
n.hidden = !note;
|
|
179
|
+
const body = d.querySelector("#tx-body");
|
|
180
|
+
body.setAttribute("aria-busy", "true");
|
|
181
|
+
body.innerHTML = `<p class="tx-empty">${esc(t("tx.loading"))}</p>`;
|
|
182
|
+
if (!d.open) {
|
|
183
|
+
if (typeof d.showModal === "function") d.showModal();
|
|
184
|
+
else d.setAttribute("open", "");
|
|
185
|
+
}
|
|
186
|
+
d.querySelector("#tx-h").focus();
|
|
187
|
+
try {
|
|
188
|
+
const r = await load();
|
|
189
|
+
body.innerHTML = transcriptHtml(r?.items, { omitted: r?.omitted ?? 0 });
|
|
190
|
+
// The newest lines are what a person opens it for.
|
|
191
|
+
body.scrollTop = body.scrollHeight;
|
|
192
|
+
} catch (e) {
|
|
193
|
+
body.innerHTML = `<p class="tx-empty" role="alert">${esc(t("tx.failed", { err: e?.message || String(e) }))}</p>`;
|
|
194
|
+
} finally {
|
|
195
|
+
body.removeAttribute("aria-busy");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Close the transcript view, if open (tests, page changes). */
|
|
200
|
+
export function closeTranscript() {
|
|
201
|
+
close();
|
|
202
|
+
}
|
|
@@ -707,7 +707,9 @@ export function agentRefusal(
|
|
|
707
707
|
AGENT_READ_ONLY.some((r) => pathMatches(r, pathname)))
|
|
708
708
|
)
|
|
709
709
|
return `${p.actor} can't use ${pathname}: signing in, web.secret and the admin password are a person's.`;
|
|
710
|
-
|
|
710
|
+
// D32's helpers count is the list's, no session's: read-grade.
|
|
711
|
+
const m =
|
|
712
|
+
pathname === "/api/sessions/helpers" ? null : SESSION_ROUTE.exec(pathname);
|
|
711
713
|
if (m) {
|
|
712
714
|
let id = m[1];
|
|
713
715
|
try {
|
|
@@ -172,6 +172,18 @@ export const ROUTES: readonly RouteEntry[] = [
|
|
|
172
172
|
"/api/workspaces/:ws/agents/:id/paths",
|
|
173
173
|
"which paths an agent printed exist in its workspace (stat only, AG-4b)",
|
|
174
174
|
),
|
|
175
|
+
R(
|
|
176
|
+
"/api/workspaces/:ws/agents/:id/helpers",
|
|
177
|
+
"D32: the helpers an agent started in its own session, read from the runtime's files (never written)",
|
|
178
|
+
),
|
|
179
|
+
R(
|
|
180
|
+
"/api/workspaces/:ws/agents/:id/helpers/:helper",
|
|
181
|
+
"D32: one helper's transcript, tool calls summarized, read-only (no stop or steer)",
|
|
182
|
+
),
|
|
183
|
+
R(
|
|
184
|
+
"/api/sessions/helpers",
|
|
185
|
+
"D32: how many helpers the agents have running (a count, for the rail's badge)",
|
|
186
|
+
),
|
|
175
187
|
R(
|
|
176
188
|
"/api/sessions",
|
|
177
189
|
"lists the sessions and agents (names and states, never their output)",
|
|
@@ -392,6 +404,20 @@ export const ROUTES: readonly RouteEntry[] = [
|
|
|
392
404
|
"/api/hub",
|
|
393
405
|
"HUB-1.5: Den Hub: its URL, connected or not, the team and the install's label (never the token)",
|
|
394
406
|
),
|
|
407
|
+
M(
|
|
408
|
+
["POST"],
|
|
409
|
+
"/api/hub/connect",
|
|
410
|
+
"HUB-1.7: start Connect to Hub: a PKCE verifier and single-use state kept here; answers Hub's /connect URL",
|
|
411
|
+
),
|
|
412
|
+
M(
|
|
413
|
+
["POST"],
|
|
414
|
+
"/api/hub/disconnect",
|
|
415
|
+
"HUB-1.7: revoke this install's Hub token on Hub, then remove it here",
|
|
416
|
+
),
|
|
417
|
+
R(
|
|
418
|
+
"/hub/callback",
|
|
419
|
+
"HUB-1.7: Hub sends the person back here. Sign-in free: the session cookie is SameSite=Strict, so Hub's redirect doesn't carry it. It acts only for a state this server issued to a signed-in Connect (single-use, 10 min), with the PKCE verifier only this server holds; anything else changes nothing",
|
|
420
|
+
),
|
|
395
421
|
M(
|
|
396
422
|
["PUT"],
|
|
397
423
|
"/api/agents/rotation",
|
|
@@ -488,6 +514,9 @@ export const SIGNIN_FREE: readonly string[] = [
|
|
|
488
514
|
// SEC-2a: approve this computer with a code, from the signed-out page.
|
|
489
515
|
"/api/signin/code",
|
|
490
516
|
"/api/signin/code/:id",
|
|
517
|
+
// HUB-1.7: Hub's redirect back (no session cookie on it): the state
|
|
518
|
+
// this server issued is the proof (hub-connect.ts).
|
|
519
|
+
"/hub/callback",
|
|
491
520
|
];
|
|
492
521
|
|
|
493
522
|
/**
|
|
@@ -533,6 +562,10 @@ export const REGEX_ROUTES: Readonly<Record<string, readonly string[]>> = {
|
|
|
533
562
|
"agent-routes.ts:RUNTIME_PATH": ["/api/agents/runtimes/:id/path"],
|
|
534
563
|
"agent-routes.ts:PERSONA_PATH": ["/api/personas/:id"],
|
|
535
564
|
"agent-routes.ts:PLAIN_PATH": ["/api/workspaces/:ws/sessions"],
|
|
565
|
+
"agent-routes.ts:HELPERS_PATH": [
|
|
566
|
+
"/api/workspaces/:ws/agents/:id/helpers",
|
|
567
|
+
"/api/workspaces/:ws/agents/:id/helpers/:helper",
|
|
568
|
+
],
|
|
536
569
|
"agent-routes.ts:CONVERSATIONS_PATH": [
|
|
537
570
|
"/api/workspaces/:ws/conversations",
|
|
538
571
|
"/api/workspaces/:ws/conversations/:id/import",
|
|
@@ -64,7 +64,16 @@ import { parseSessionName } from "../../shared/sessions/names.js";
|
|
|
64
64
|
import { sessionPaths } from "../../shared/sessions/paths.js";
|
|
65
65
|
import { loadPty, type PtyLoad } from "../../shared/sessions/pty.js";
|
|
66
66
|
import type { SessionInfo } from "../../shared/sessions/types.js";
|
|
67
|
-
import {
|
|
67
|
+
import type { HelperSettings } from "../../shared/agents/helpers.js";
|
|
68
|
+
import { readAgentsConfig } from "../../shared/den-config.js";
|
|
69
|
+
import {
|
|
70
|
+
agentRows,
|
|
71
|
+
isStoppedState,
|
|
72
|
+
newerOf,
|
|
73
|
+
noteEnded,
|
|
74
|
+
rowHelpers,
|
|
75
|
+
type AgentRow,
|
|
76
|
+
} from "../cli/agent-ops.js";
|
|
68
77
|
import type { AgentRecord, Override } from "../../shared/agents/records.js";
|
|
69
78
|
import { state as sessionState } from "../cli/sessions-command.js";
|
|
70
79
|
import { recordAction } from "./audit.js";
|
|
@@ -91,6 +100,8 @@ export interface SessionRouteDeps {
|
|
|
91
100
|
waitingAgents?: () => ReadonlySet<string>;
|
|
92
101
|
/** D28: each agent's effective rotation settings (one resolver per list). */
|
|
93
102
|
rotationPolicy?: () => (rec: AgentRecord) => RotationPolicy | undefined;
|
|
103
|
+
/** D32: agents.helpers (default: the agents config's, else its defaults). */
|
|
104
|
+
helperSettings?: () => HelperSettings | undefined;
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
interface Resolved {
|
|
@@ -102,6 +113,7 @@ interface Resolved {
|
|
|
102
113
|
pty: () => PtyLoad;
|
|
103
114
|
waitingAgents: () => ReadonlySet<string>;
|
|
104
115
|
rotationPolicy?: SessionRouteDeps["rotationPolicy"];
|
|
116
|
+
helperSettings: () => HelperSettings | undefined;
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
/** A workspace name as `?ws=` may give it. */
|
|
@@ -122,6 +134,15 @@ function resolve(d: SessionRouteDeps = {}): Resolved {
|
|
|
122
134
|
throw e;
|
|
123
135
|
}
|
|
124
136
|
}),
|
|
137
|
+
helperSettings:
|
|
138
|
+
d.helperSettings ??
|
|
139
|
+
(() => {
|
|
140
|
+
try {
|
|
141
|
+
return readAgentsConfig().helpers;
|
|
142
|
+
} catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}),
|
|
125
146
|
activeWorkspaces: d.activeWorkspaces ?? (() => getActiveWorkspaces()),
|
|
126
147
|
workspaceExists:
|
|
127
148
|
d.workspaceExists ??
|
|
@@ -209,6 +230,8 @@ export interface ListedSession {
|
|
|
209
230
|
rotation?: RotationInfo;
|
|
210
231
|
/** D28: the last relaunch from a handoff (the page toasts one Den did by itself). */
|
|
211
232
|
lastRotation?: RotationEntry;
|
|
233
|
+
/** D32: its helpers (counts and the recent ones). */
|
|
234
|
+
helpers?: AgentRow["helpers"];
|
|
212
235
|
}
|
|
213
236
|
|
|
214
237
|
/** The list, from the host's sessions and the agent records. */
|
|
@@ -216,6 +239,7 @@ export function listedSessions(
|
|
|
216
239
|
sessions: SessionInfo[],
|
|
217
240
|
d: Pick<Resolved, "stateDir" | "dist" | "activeWorkspaces"> &
|
|
218
241
|
Partial<Pick<Resolved, "waitingAgents">> &
|
|
242
|
+
Partial<Pick<Resolved, "helperSettings">> &
|
|
219
243
|
Pick<SessionRouteDeps, "rotationPolicy">,
|
|
220
244
|
/** A workspace asked for by name (`?ws=`), listed even when inactive. */
|
|
221
245
|
also?: string,
|
|
@@ -237,6 +261,7 @@ export function listedSessions(
|
|
|
237
261
|
const out: ListedSession[] = [];
|
|
238
262
|
const agentSessions = new Set<string>();
|
|
239
263
|
noteEnded(d.stateDir(), mine, records);
|
|
264
|
+
const helperSettings = d.helperSettings?.();
|
|
240
265
|
for (const r of agentRows(
|
|
241
266
|
mine,
|
|
242
267
|
records,
|
|
@@ -283,6 +308,11 @@ export function listedSessions(
|
|
|
283
308
|
...(r.lastRotation ? { lastRotation: r.lastRotation } : {}),
|
|
284
309
|
...(revoked.has(rec.id) && s && !s.exited ? { tokenRevoked: true } : {}),
|
|
285
310
|
...(waiting.has(rec.id) && s && !s.exited ? { waiting: true } : {}),
|
|
311
|
+
// D32: the helpers it started in its own session.
|
|
312
|
+
...(() => {
|
|
313
|
+
const h = rowHelpers(rec, !isStoppedState(r.state), helperSettings);
|
|
314
|
+
return h ? { helpers: h } : {};
|
|
315
|
+
})(),
|
|
286
316
|
});
|
|
287
317
|
}
|
|
288
318
|
for (const s of mine) {
|
|
@@ -311,6 +341,9 @@ export function listedSessions(
|
|
|
311
341
|
);
|
|
312
342
|
}
|
|
313
343
|
|
|
344
|
+
/** D32: the helpers count, under /api/sessions/ but no session's route. */
|
|
345
|
+
export const SESSION_HELPERS_PATH = "/api/sessions/helpers";
|
|
346
|
+
|
|
314
347
|
/**
|
|
315
348
|
* `GET /api/sessions`; false when `pathname` isn't it. `ioAllowed` is what
|
|
316
349
|
* the request checks say about this browser: beyond loopback without a
|
|
@@ -323,6 +356,8 @@ export async function handleSessionList(
|
|
|
323
356
|
ioAllowed: boolean,
|
|
324
357
|
deps: SessionRouteDeps = {},
|
|
325
358
|
): Promise<boolean> {
|
|
359
|
+
if (pathname === "/api/sessions/helpers")
|
|
360
|
+
return helpersRunning(req, res, deps);
|
|
326
361
|
if (pathname !== "/api/sessions") return false;
|
|
327
362
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
328
363
|
res.setHeader("Allow", "GET");
|
|
@@ -660,3 +695,38 @@ export async function handleSessionUpgrade(
|
|
|
660
695
|
detach();
|
|
661
696
|
});
|
|
662
697
|
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* D32: `GET /api/sessions/helpers`: how many helpers the active
|
|
701
|
+
* workspaces' agents have running now, `{ running }` (the rail's Agents
|
|
702
|
+
* badge on every page). Names and counts only, never a transcript.
|
|
703
|
+
*/
|
|
704
|
+
async function helpersRunning(
|
|
705
|
+
req: http.IncomingMessage,
|
|
706
|
+
res: http.ServerResponse,
|
|
707
|
+
deps: SessionRouteDeps,
|
|
708
|
+
): Promise<true> {
|
|
709
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
710
|
+
res.setHeader("Allow", "GET");
|
|
711
|
+
sendJson(res, { ok: false, error: "method not allowed" }, 405);
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
const d = resolve(deps);
|
|
715
|
+
let sessions: SessionInfo[] = [];
|
|
716
|
+
try {
|
|
717
|
+
const c = await d.connectExisting();
|
|
718
|
+
if (c)
|
|
719
|
+
try {
|
|
720
|
+
sessions = await c.list();
|
|
721
|
+
} finally {
|
|
722
|
+
c.close();
|
|
723
|
+
}
|
|
724
|
+
} catch {
|
|
725
|
+
/* no host: nothing runs, so no helper runs */
|
|
726
|
+
}
|
|
727
|
+
let running = 0;
|
|
728
|
+
for (const r of listedSessions(sessions, d))
|
|
729
|
+
running += r.helpers?.running ?? 0;
|
|
730
|
+
sendJson(res, { running });
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
@@ -34,6 +34,9 @@ export const UI_MODULES: readonly string[] = [
|
|
|
34
34
|
"/error-page.js",
|
|
35
35
|
"/agents-section.js",
|
|
36
36
|
"/agents-page.js",
|
|
37
|
+
// D32: an agent's helpers, and the shared read-only transcript view.
|
|
38
|
+
"/agent-helpers.js",
|
|
39
|
+
"/transcript-view.js",
|
|
37
40
|
// The Start agent form's choices, shared by both (AG-10.0).
|
|
38
41
|
"/agent-start.js",
|
|
39
42
|
// AG-10.4-10.7: the persona picker, editor and Settings › Personas.
|