@monster-orange/den 0.4.27 → 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 +47 -0
- package/docs/distributions.md +73 -2
- 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 +13 -0
- package/src/shared/den-config.ts +42 -1
- package/src/shared/hub.ts +10 -0
- package/src/ui/cli/agent-ops.ts +144 -0
- package/src/ui/cli/agents-command.ts +127 -1
- 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 +56 -2
- package/src/ui/server/hub-connect.ts +69 -3
- 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 +39 -0
- package/src/ui/server/public/i18n/ja.js +37 -0
- package/src/ui/server/public/settings-registry.js +37 -1
- package/src/ui/server/public/shell.js +200 -0
- 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 +16 -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)",
|
|
@@ -550,6 +562,10 @@ export const REGEX_ROUTES: Readonly<Record<string, readonly string[]>> = {
|
|
|
550
562
|
"agent-routes.ts:RUNTIME_PATH": ["/api/agents/runtimes/:id/path"],
|
|
551
563
|
"agent-routes.ts:PERSONA_PATH": ["/api/personas/:id"],
|
|
552
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
|
+
],
|
|
553
569
|
"agent-routes.ts:CONVERSATIONS_PATH": [
|
|
554
570
|
"/api/workspaces/:ws/conversations",
|
|
555
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.
|