@monster-orange/den 0.4.7 → 0.4.9
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 +174 -0
- package/README.md +7 -7
- package/docs/distributions.md +243 -1
- package/package.json +1 -1
- package/schemas/den.plugin.v1.json +3 -2
- package/src/shared/agents/den-claude.ts +57 -3
- package/src/shared/agents/guardrails/hooks.json +23 -0
- package/src/shared/agents/guardrails/instructions.md +23 -0
- package/src/shared/agents/guardrails/rotation-hook.ts +127 -0
- package/src/shared/agents/guardrails/stop-hook.ts +48 -2
- package/src/shared/agents/guardrails.ts +38 -7
- package/src/shared/agents/handoff.ts +118 -0
- package/src/shared/agents/personas.ts +11 -1
- package/src/shared/agents/records.ts +122 -1
- package/src/shared/agents/rotation.ts +362 -0
- package/src/shared/agents/runtime.ts +27 -0
- package/src/shared/den-config.ts +72 -1
- package/src/shared/git/gh-pr-state.ts +32 -0
- package/src/shared/plugins/manifest.ts +3 -0
- package/src/shared/usage/paths.ts +16 -0
- package/src/shared/usage/report.ts +377 -0
- package/src/shared/usage/sources.ts +127 -0
- package/src/shared/usage/transcript.ts +319 -0
- package/src/ui/cli/agent-ops.ts +678 -51
- package/src/ui/cli/agents-command.ts +192 -22
- package/src/ui/cli/index.ts +13 -1
- package/src/ui/cli/usage-command.ts +323 -0
- package/src/ui/mcp/api.ts +10 -1
- package/src/ui/mcp/server.ts +34 -0
- package/src/ui/mcp/tools.ts +50 -0
- package/src/ui/server/agent-resume-grant.ts +53 -0
- package/src/ui/server/agent-routes.ts +247 -9
- package/src/ui/server/audit.ts +68 -1
- package/src/ui/server/index.ts +16 -5
- package/src/ui/server/public/agents-page.js +400 -13
- package/src/ui/server/public/agents-section.js +17 -3
- package/src/ui/server/public/agents.html +44 -0
- package/src/ui/server/public/i18n/es.js +40 -0
- package/src/ui/server/public/i18n/ja.js +36 -0
- package/src/ui/server/public/shell.js +79 -0
- package/src/ui/server/request-guard.ts +21 -1
- package/src/ui/server/route-classes.ts +29 -0
- package/src/ui/server/session-routes.ts +60 -5
- package/src/ui/server/sudo-store.ts +36 -7
|
@@ -31,6 +31,24 @@
|
|
|
31
31
|
* can't, before Start (agent-start.js readiness). Starting, stopping and
|
|
32
32
|
* resuming an agent never ask for the admin password: it is for sudo, and
|
|
33
33
|
* no agent action runs sudo (LK-0).
|
|
34
|
+
*
|
|
35
|
+
* Stopped agents (AG-14) stay listed under their workspace, below the
|
|
36
|
+
* running ones, each with why it stopped ("Stopped by you 19:52",
|
|
37
|
+
* "Interrupted: Den restarted", "Exited (code 1)") and its actions:
|
|
38
|
+
* Resume (the same conversation; its pane opens), Start fresh, Remove (one
|
|
39
|
+
* click, and Undo in the toast for 10 s). A runtime that can't resume
|
|
40
|
+
* shows only Start fresh, saying so. Clear stopped removes every stopped
|
|
41
|
+
* agent in view. After a restart, one line says how many were interrupted,
|
|
42
|
+
* with Resume all and Review.
|
|
43
|
+
*
|
|
44
|
+
* Session rotation (den#4): an agent whose session rotated (after
|
|
45
|
+
* agents.rotate.after_compactions compactions, or its pull request merged)
|
|
46
|
+
* and whose handoff.md passes the check reads "Ready to relaunch (3
|
|
47
|
+
* compactions)" or "(PR merged)", with Relaunch from handoff as its primary
|
|
48
|
+
* action, then Start fresh and Remove; Resume is not offered. With the
|
|
49
|
+
* handoff missing or failing the check, the row says so and keeps Resume
|
|
50
|
+
* and Start fresh. A running one says "Handoff due" or "Ready to relaunch"
|
|
51
|
+
* as a tag.
|
|
34
52
|
*/
|
|
35
53
|
import { fillStartForm } from "/agent-start.js";
|
|
36
54
|
|
|
@@ -45,6 +63,9 @@ const esc = (s) =>
|
|
|
45
63
|
);
|
|
46
64
|
const api = (method, url, body) => window.denShell.api(method, url, body);
|
|
47
65
|
const CLI = "__DEN_CLI__";
|
|
66
|
+
const TITLE = "__DEN_TITLE__";
|
|
67
|
+
/** How long Remove's Undo stays offered (AG-14.1b). */
|
|
68
|
+
export const UNDO_MS = 10_000;
|
|
48
69
|
|
|
49
70
|
/** The most panes at once (the prototype's 2x2). */
|
|
50
71
|
export const MAX_PANES = 4;
|
|
@@ -88,11 +109,17 @@ function writeUrl() {
|
|
|
88
109
|
|
|
89
110
|
// ── the list ────────────────────────────────────────────────────────────
|
|
90
111
|
|
|
91
|
-
/**
|
|
112
|
+
/** ?ws=: the one workspace the page was asked for, or null. */
|
|
113
|
+
const urlWorkspace = () => new URLSearchParams(location.search).get("ws");
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The workspaces to show: ?ws= (always that one, even when it isn't active
|
|
117
|
+
* or isn't known: never All in its place), else the scope bar's view.
|
|
118
|
+
*/
|
|
92
119
|
function viewWorkspaces() {
|
|
93
120
|
const all = st.data?.workspaces ?? [];
|
|
94
|
-
const ws =
|
|
95
|
-
if (ws
|
|
121
|
+
const ws = urlWorkspace();
|
|
122
|
+
if (ws) return [ws];
|
|
96
123
|
if (st.scope && st.scope !== "all" && all.includes(st.scope))
|
|
97
124
|
return [st.scope];
|
|
98
125
|
return all;
|
|
@@ -112,7 +139,9 @@ function stateText(state) {
|
|
|
112
139
|
return m[1] === undefined
|
|
113
140
|
? t("ag.state.exited")
|
|
114
141
|
: t("ag.state.exitedCode", { code: m[1] });
|
|
115
|
-
return ["running", "stopping", "stopped", "ended"].includes(
|
|
142
|
+
return ["running", "stopping", "stopped", "ended", "interrupted"].includes(
|
|
143
|
+
state,
|
|
144
|
+
)
|
|
116
145
|
? t(`ag.state.${state}`)
|
|
117
146
|
: state;
|
|
118
147
|
}
|
|
@@ -127,7 +156,111 @@ function paneName(r) {
|
|
|
127
156
|
return r.kind === "agent" ? r.displayName || r.agent || r.label : r.label;
|
|
128
157
|
}
|
|
129
158
|
|
|
159
|
+
/** An agent that isn't running: stopped, interrupted or exited (AG-14). */
|
|
160
|
+
export function isStopped(r) {
|
|
161
|
+
return r.kind === "agent" && r.state !== "running" && r.state !== "stopping";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** "19:52" today, else "Nov 6, 19:52" (the browser's own format). */
|
|
165
|
+
function when(iso) {
|
|
166
|
+
const d = new Date(iso);
|
|
167
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
168
|
+
const time = d.toLocaleTimeString([], {
|
|
169
|
+
hour: "2-digit",
|
|
170
|
+
minute: "2-digit",
|
|
171
|
+
hour12: false,
|
|
172
|
+
});
|
|
173
|
+
const today = new Date().toDateString() === d.toDateString();
|
|
174
|
+
return today
|
|
175
|
+
? time
|
|
176
|
+
: `${d.toLocaleDateString([], { month: "short", day: "numeric" })}, ${time}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Why a stopped agent stopped (AG-14.1). */
|
|
180
|
+
export function stopReason(r) {
|
|
181
|
+
if (r.state === "interrupted")
|
|
182
|
+
return t("ag.why.interrupted", { name: TITLE });
|
|
183
|
+
if (r.state === "stopped")
|
|
184
|
+
return t("ag.why.stopped", { time: when(r.stoppedAt ?? "") }).trim();
|
|
185
|
+
const m = /^exited(?: \((-?\d+)\))?$/.exec(r.state);
|
|
186
|
+
if (m)
|
|
187
|
+
return m[1] === undefined
|
|
188
|
+
? t("ag.why.exited")
|
|
189
|
+
: t("ag.why.exitedCode", { code: m[1] });
|
|
190
|
+
return stateText(r.state);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Why it rotates, in words: "3 compactions", "PR merged" (den#4). */
|
|
194
|
+
function rotationWhy(rot) {
|
|
195
|
+
if (rot.due === "pr-merged") return t("ag.rot.merged");
|
|
196
|
+
return t(
|
|
197
|
+
rot.compactions === 1 ? "ag.rot.compactions.one" : "ag.rot.compactions",
|
|
198
|
+
{
|
|
199
|
+
n: rot.compactions,
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Its rotation, when one is due: "ready" or "due" (den#4). */
|
|
205
|
+
function rotationOf(r) {
|
|
206
|
+
if (!r.rotation?.due) return "";
|
|
207
|
+
return r.rotation.ready ? "ready" : "due";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A stopped agent's row: its button (it opens an ended pane) and actions. */
|
|
211
|
+
function stoppedHtml(r) {
|
|
212
|
+
const open = st.open.includes(r.id);
|
|
213
|
+
const name = displayName(r);
|
|
214
|
+
const nm = `${esc(name)}${r.persona && r.persona !== r.label ? ` <span class="id">${esc(r.label)}</span>` : ""}`;
|
|
215
|
+
const meta = [r.runtime, r.cwd].filter(Boolean).join(" · ");
|
|
216
|
+
const who = esc(r.agent);
|
|
217
|
+
const rot = rotationOf(r);
|
|
218
|
+
const relaunch =
|
|
219
|
+
rot === "ready"
|
|
220
|
+
? `<button type="button" class="den-btn sm primary" data-act="relaunch" aria-label="${esc(t("ag.relaunchName", { id: r.agent }))}">${esc(t("ag.relaunch"))}</button>`
|
|
221
|
+
: "";
|
|
222
|
+
const resume = r.resumable
|
|
223
|
+
? `<button type="button" class="den-btn sm primary" data-act="resume" aria-label="${esc(t("ag.resumeName", { id: r.agent }))}">${esc(t("ag.resume"))}</button>`
|
|
224
|
+
: "";
|
|
225
|
+
const fresh = `<button type="button" class="den-btn sm" data-act="fresh" aria-label="${esc(t("ag.freshName", { id: r.agent }))}">${esc(t("ag.fresh"))}</button>`;
|
|
226
|
+
const remove = `<button type="button" class="den-btn sm" data-act="remove" aria-label="${esc(t("ag.removeName", { id: r.agent }))}">${esc(t("ag.remove"))}</button>`;
|
|
227
|
+
// A rotation due without a handoff that passes: say so (den#4).
|
|
228
|
+
const handoff =
|
|
229
|
+
rot === "due"
|
|
230
|
+
? `<span class="agcant" data-handoff="${esc(r.rotation.handoff)}">${esc(
|
|
231
|
+
t(
|
|
232
|
+
r.rotation.handoff === "missing"
|
|
233
|
+
? "ag.rot.noHandoff"
|
|
234
|
+
: "ag.rot.badHandoff",
|
|
235
|
+
{ why: rotationWhy(r.rotation) },
|
|
236
|
+
),
|
|
237
|
+
)}</span>`
|
|
238
|
+
: "";
|
|
239
|
+
const cant =
|
|
240
|
+
!r.resumable && r.noResume && rot !== "ready"
|
|
241
|
+
? `<span class="agcant" data-no-resume>${esc(r.noResume.startsWith(`${r.runtime} `) ? t("ag.noResume", { runtime: r.runtime }) : r.noResume)}</span>`
|
|
242
|
+
: "";
|
|
243
|
+
const stText =
|
|
244
|
+
rot === "ready"
|
|
245
|
+
? t("ag.rot.ready", { why: rotationWhy(r.rotation) })
|
|
246
|
+
: stateText(r.state);
|
|
247
|
+
return (
|
|
248
|
+
`<div class="agst" data-agent="${who}" data-state="${esc(r.state)}" data-ws="${esc(r.workspace)}"${rot ? ` data-rotation="${rot}"` : ""}>` +
|
|
249
|
+
`<button type="button" class="agr" data-id="${esc(r.id)}" data-kind="agent" data-state="${esc(r.state)}"` +
|
|
250
|
+
` aria-pressed="${open}"${r.attachable ? "" : ' aria-disabled="true"'}` +
|
|
251
|
+
` aria-label="${esc(t("ag.openName", { id: `${r.workspace} / ${name}` }))}">` +
|
|
252
|
+
`<span class="den-dot ${stateTone(r)}" aria-hidden="true"></span>` +
|
|
253
|
+
`<span class="nm">${nm}</span><span class="st">${esc(stText)}</span>` +
|
|
254
|
+
(meta ? `<span class="meta">${esc(meta)}</span>` : "") +
|
|
255
|
+
`<span class="why">${esc(stopReason(r))}</span>` +
|
|
256
|
+
`</button>` +
|
|
257
|
+
`<div class="agacts">${handoff}${cant}${relaunch}${resume}${fresh}${remove}</div>` +
|
|
258
|
+
`</div>`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
130
262
|
function rowHtml(r) {
|
|
263
|
+
if (isStopped(r)) return stoppedHtml(r);
|
|
131
264
|
const open = st.open.includes(r.id);
|
|
132
265
|
const nm =
|
|
133
266
|
r.kind === "agent"
|
|
@@ -137,9 +270,21 @@ function rowHtml(r) {
|
|
|
137
270
|
? `<span class="wbdg">${esc(t("ag.waiting"))}</span>`
|
|
138
271
|
: `<span class="st">${esc(stateText(r.state))}</span>`;
|
|
139
272
|
const meta = [r.runtime, r.cwd].filter(Boolean).join(" · ");
|
|
140
|
-
const
|
|
141
|
-
|
|
273
|
+
const rot = rotationOf(r);
|
|
274
|
+
const rotTag = rot
|
|
275
|
+
? `<span class="tagx warn" data-rotation="${rot}">${esc(
|
|
276
|
+
t(rot === "ready" ? "ag.rot.ready" : "ag.rot.due", {
|
|
277
|
+
why: rotationWhy(r.rotation),
|
|
278
|
+
}),
|
|
279
|
+
)}</span>`
|
|
142
280
|
: "";
|
|
281
|
+
const revokedTag = r.tokenRevoked
|
|
282
|
+
? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
|
|
283
|
+
: "";
|
|
284
|
+
const tags =
|
|
285
|
+
rotTag || revokedTag
|
|
286
|
+
? `<span class="agtags">${revokedTag}${rotTag}</span>`
|
|
287
|
+
: "";
|
|
143
288
|
return (
|
|
144
289
|
`<button type="button" class="agr" data-id="${esc(r.id)}" data-kind="${esc(r.kind)}" data-state="${esc(r.state)}"` +
|
|
145
290
|
` aria-pressed="${open}"${r.attachable ? "" : ' aria-disabled="true"'}` +
|
|
@@ -169,10 +314,40 @@ function paintList() {
|
|
|
169
314
|
return;
|
|
170
315
|
}
|
|
171
316
|
const shown = viewWorkspaces();
|
|
317
|
+
// ?ws= names a workspace that isn't active: say so, and offer the switch.
|
|
318
|
+
const asked = st.data.requested;
|
|
319
|
+
if (asked && !asked.active)
|
|
320
|
+
notes.push(
|
|
321
|
+
`<div class="agnote" data-note="inactive" role="status">` +
|
|
322
|
+
esc(t("ag.inactive", { ws: asked.workspace })) +
|
|
323
|
+
(asked.known
|
|
324
|
+
? ` · <button type="button" class="lnk" data-act="switch-ws">${esc(t("ag.switchWs", { ws: asked.workspace }))}</button>`
|
|
325
|
+
: ` ${esc(t("ag.unknownWs"))}`) +
|
|
326
|
+
`</div>`,
|
|
327
|
+
);
|
|
328
|
+
// AG-14.3: one line after a restart, for the agents it interrupted.
|
|
329
|
+
const hit = visibleRows().filter((r) => r.state === "interrupted");
|
|
330
|
+
if (hit.length)
|
|
331
|
+
notes.push(
|
|
332
|
+
`<div class="agnote" data-note="interrupted" role="status">` +
|
|
333
|
+
`${esc(t(hit.length === 1 ? "ag.interrupted.one" : "ag.interrupted", { n: hit.length, name: TITLE }))}` +
|
|
334
|
+
(hit.some((r) => r.resumable)
|
|
335
|
+
? ` · <button type="button" class="lnk" data-act="resume-all">${esc(t("ag.resumeAll"))}</button>`
|
|
336
|
+
: "") +
|
|
337
|
+
` · <button type="button" class="lnk" data-act="review">${esc(t("ag.review"))}</button></div>`,
|
|
338
|
+
);
|
|
172
339
|
const groups = shown
|
|
173
340
|
.map((ws) => ({
|
|
174
341
|
ws,
|
|
175
|
-
|
|
342
|
+
// Running ones (and plain sessions) first, then the stopped agents.
|
|
343
|
+
rows: st.data.sessions
|
|
344
|
+
.filter((s) => s.workspace === ws)
|
|
345
|
+
.map((s, i) => ({ s, i }))
|
|
346
|
+
.sort(
|
|
347
|
+
(a, b) =>
|
|
348
|
+
Number(isStopped(a.s)) - Number(isStopped(b.s)) || a.i - b.i,
|
|
349
|
+
)
|
|
350
|
+
.map((x) => x.s),
|
|
176
351
|
}))
|
|
177
352
|
.filter((g) => g.rows.length > 0);
|
|
178
353
|
const body = groups.length
|
|
@@ -186,6 +361,11 @@ function paintList() {
|
|
|
186
361
|
.join("")
|
|
187
362
|
: `<div class="none">${esc(t("ag.none.scope", { scope: st.scopeLabel }))}</div>`;
|
|
188
363
|
el.innerHTML = notes.join("") + body;
|
|
364
|
+
const clear = st.root.querySelector("#ag-clear");
|
|
365
|
+
if (clear) clear.hidden = !visibleRows().some(isStopped);
|
|
366
|
+
// No such workspace: nothing to start an agent in.
|
|
367
|
+
const start = st.root.querySelector("#ag-start");
|
|
368
|
+
if (start) start.hidden = st.data.requested?.known === false;
|
|
189
369
|
}
|
|
190
370
|
|
|
191
371
|
function visibleRows() {
|
|
@@ -219,7 +399,7 @@ async function loadScope() {
|
|
|
219
399
|
}
|
|
220
400
|
|
|
221
401
|
function scopeLabelFor(shown) {
|
|
222
|
-
const ws =
|
|
402
|
+
const ws = urlWorkspace();
|
|
223
403
|
if (ws && shown.length === 1 && shown[0] === ws) return ws;
|
|
224
404
|
return st.scope === "all" ? t("ag.all") : st.scope;
|
|
225
405
|
}
|
|
@@ -227,7 +407,11 @@ function scopeLabelFor(shown) {
|
|
|
227
407
|
async function refresh() {
|
|
228
408
|
clearTimeout(st.timer);
|
|
229
409
|
try {
|
|
230
|
-
const
|
|
410
|
+
const ws = urlWorkspace();
|
|
411
|
+
const r = await fetch(
|
|
412
|
+
ws ? `/api/sessions?ws=${encodeURIComponent(ws)}` : "/api/sessions",
|
|
413
|
+
{ headers: { accept: "json" } },
|
|
414
|
+
);
|
|
231
415
|
if (r.status === 401) {
|
|
232
416
|
st.signedOut = true;
|
|
233
417
|
} else if (r.ok) {
|
|
@@ -325,6 +509,11 @@ function paintBar(p) {
|
|
|
325
509
|
r?.kind === "agent" && (r.state === "running" || r.state === "stopping")
|
|
326
510
|
? `<button type="button" class="den-btn sm" data-act="stop" aria-label="${esc(t("ag.stopName", { id: r.agent }))}">${esc(t("ag.stop"))}</button>`
|
|
327
511
|
: "";
|
|
512
|
+
// The mock's pane bar: Resume (primary) on an ended agent that can.
|
|
513
|
+
const resume =
|
|
514
|
+
r && isStopped(r) && r.resumable
|
|
515
|
+
? `<button type="button" class="den-btn sm primary" data-act="resume" aria-label="${esc(t("ag.resumeName", { id: r.agent }))}">${esc(t("ag.resume"))}</button>`
|
|
516
|
+
: "";
|
|
328
517
|
const reconnect = p.lost
|
|
329
518
|
? `<button type="button" class="den-btn sm" data-act="reconnect">${esc(t("ag.reconnect"))}</button>`
|
|
330
519
|
: "";
|
|
@@ -334,7 +523,7 @@ function paintBar(p) {
|
|
|
334
523
|
(r?.tokenRevoked
|
|
335
524
|
? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
|
|
336
525
|
: "") +
|
|
337
|
-
`<span class="x">${reconnect}${stop}<button type="button" class="den-btn sm" data-act="close" aria-label="${esc(t("ag.detachName", { id: name }))}" title="${esc(t("ag.detach"))}">×</button></span>`;
|
|
526
|
+
`<span class="x">${reconnect}${stop}${resume}<button type="button" class="den-btn sm" data-act="close" aria-label="${esc(t("ag.detachName", { id: name }))}" title="${esc(t("ag.detach"))}">×</button></span>`;
|
|
338
527
|
}
|
|
339
528
|
|
|
340
529
|
function send(p, obj) {
|
|
@@ -610,17 +799,191 @@ async function stopAgent(p, button) {
|
|
|
610
799
|
if (!r?.agent) return;
|
|
611
800
|
button.disabled = true;
|
|
612
801
|
try {
|
|
613
|
-
await api(
|
|
802
|
+
const out = await api(
|
|
614
803
|
"POST",
|
|
615
804
|
`/api/workspaces/${encodeURIComponent(r.workspace)}/agents/${encodeURIComponent(r.agent)}/stop`,
|
|
616
805
|
);
|
|
617
|
-
window.denShell?.toast?.(
|
|
806
|
+
window.denShell?.toast?.(
|
|
807
|
+
t(out?.stillStopping ? "ag.stillStopping" : "ag.stopped", {
|
|
808
|
+
id: r.agent,
|
|
809
|
+
}),
|
|
810
|
+
);
|
|
811
|
+
} catch (err) {
|
|
812
|
+
window.denShell?.errorToast?.(err);
|
|
813
|
+
}
|
|
814
|
+
await refresh();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ── Stopped agents (AG-14) ──────────────────────────────────────────────
|
|
818
|
+
|
|
819
|
+
const agentPath = (r, verb) =>
|
|
820
|
+
`/api/workspaces/${encodeURIComponent(r.workspace)}/agents/${encodeURIComponent(r.agent)}/${verb}`;
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Resume (the same conversation), Start fresh, or Relaunch from handoff
|
|
824
|
+
* (den#4: a new conversation that starts from handoff.md); its pane opens
|
|
825
|
+
* on it.
|
|
826
|
+
*/
|
|
827
|
+
async function restartRow(r, verb, button) {
|
|
828
|
+
if (button) button.disabled = true;
|
|
829
|
+
try {
|
|
830
|
+
const out = await api("POST", agentPath(r, verb));
|
|
831
|
+
window.denShell?.toast?.(
|
|
832
|
+
t(
|
|
833
|
+
verb === "relaunch"
|
|
834
|
+
? "ag.relaunched"
|
|
835
|
+
: verb !== "resume"
|
|
836
|
+
? "ag.startedFresh"
|
|
837
|
+
: out?.fresh
|
|
838
|
+
? "ag.resumedFresh"
|
|
839
|
+
: "ag.resumed",
|
|
840
|
+
{ id: r.agent },
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
if (st.panes.has(r.id)) closePane(r.id, { quiet: true });
|
|
844
|
+
await refresh();
|
|
845
|
+
if (out?.agent?.sessionId) await openPane(out.agent.sessionId);
|
|
846
|
+
} catch (err) {
|
|
847
|
+
window.denShell?.errorToast?.(err);
|
|
848
|
+
await refresh();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Undo: the removed agents come back (POST /api/agents/restore). */
|
|
853
|
+
async function undoRemove(ids) {
|
|
854
|
+
try {
|
|
855
|
+
const out = await api("POST", "/api/agents/restore", { agents: ids });
|
|
856
|
+
const back = out?.restored ?? [];
|
|
857
|
+
if (back.length)
|
|
858
|
+
window.denShell?.toast?.(t("ag.restored", { ids: back.join(", ") }));
|
|
859
|
+
} catch (err) {
|
|
860
|
+
window.denShell?.errorToast?.(err);
|
|
861
|
+
}
|
|
862
|
+
await refresh();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** The removal's toast, with Undo for UNDO_MS. */
|
|
866
|
+
function removedToast(msg, ids) {
|
|
867
|
+
const shell = window.denShell;
|
|
868
|
+
if (shell?.toastAction)
|
|
869
|
+
shell.toastAction(msg, t("ag.undo"), () => void undoRemove(ids), UNDO_MS);
|
|
870
|
+
else shell?.toast?.(msg);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** Remove: one click, no confirm (AG-14.1b). */
|
|
874
|
+
async function removeRow(r, button) {
|
|
875
|
+
if (button) button.disabled = true;
|
|
876
|
+
try {
|
|
877
|
+
const out = await api("POST", agentPath(r, "remove"));
|
|
878
|
+
if (st.panes.has(r.id)) closePane(r.id, { quiet: true });
|
|
879
|
+
const ids = (out?.removed ?? []).map((x) => x.agent);
|
|
880
|
+
await refresh();
|
|
881
|
+
removedToast(
|
|
882
|
+
t("ag.removed", { id: r.agent }),
|
|
883
|
+
ids.length ? ids : [r.agent],
|
|
884
|
+
);
|
|
885
|
+
} catch (err) {
|
|
886
|
+
window.denShell?.errorToast?.(err);
|
|
887
|
+
await refresh();
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/** Clear stopped: every stopped agent in view (AG-14.1b). */
|
|
892
|
+
async function clearStopped(button) {
|
|
893
|
+
const workspaces = [
|
|
894
|
+
...new Set(
|
|
895
|
+
visibleRows()
|
|
896
|
+
.filter(isStopped)
|
|
897
|
+
.map((r) => r.workspace),
|
|
898
|
+
),
|
|
899
|
+
];
|
|
900
|
+
if (!workspaces.length) return;
|
|
901
|
+
button.disabled = true;
|
|
902
|
+
try {
|
|
903
|
+
const out = await api("POST", "/api/agents/clear-stopped", { workspaces });
|
|
904
|
+
const ids = (out?.removed ?? []).map((x) => x.agent);
|
|
905
|
+
for (const r of visibleRows())
|
|
906
|
+
if (ids.includes(r.agent) && st.panes.has(r.id))
|
|
907
|
+
closePane(r.id, { quiet: true });
|
|
908
|
+
await refresh();
|
|
909
|
+
if (ids.length)
|
|
910
|
+
removedToast(
|
|
911
|
+
t(ids.length === 1 ? "ag.removedMany.one" : "ag.removedMany", {
|
|
912
|
+
n: ids.length,
|
|
913
|
+
}),
|
|
914
|
+
ids,
|
|
915
|
+
);
|
|
916
|
+
} catch (err) {
|
|
917
|
+
window.denShell?.errorToast?.(err);
|
|
918
|
+
await refresh();
|
|
919
|
+
} finally {
|
|
920
|
+
button.disabled = false;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** Resume all: every interrupted agent in view that can resume (AG-14.3). */
|
|
925
|
+
async function resumeAll(button) {
|
|
926
|
+
const workspaces = [
|
|
927
|
+
...new Set(
|
|
928
|
+
visibleRows()
|
|
929
|
+
.filter((r) => r.state === "interrupted")
|
|
930
|
+
.map((r) => r.workspace),
|
|
931
|
+
),
|
|
932
|
+
];
|
|
933
|
+
if (!workspaces.length) return;
|
|
934
|
+
button.disabled = true;
|
|
935
|
+
try {
|
|
936
|
+
const out = await window.denShell.api(
|
|
937
|
+
"POST",
|
|
938
|
+
"/api/agents/resume-interrupted",
|
|
939
|
+
{ workspaces },
|
|
940
|
+
);
|
|
941
|
+
const n = out?.resumed?.length ?? 0;
|
|
942
|
+
window.denShell?.toast?.(
|
|
943
|
+
t(n === 1 ? "ag.resumedAll.one" : "ag.resumedAll", { n }),
|
|
944
|
+
);
|
|
618
945
|
} catch (err) {
|
|
619
946
|
window.denShell?.errorToast?.(err);
|
|
620
947
|
}
|
|
621
948
|
await refresh();
|
|
622
949
|
}
|
|
623
950
|
|
|
951
|
+
/**
|
|
952
|
+
* Switch to ?ws='s workspace: make it active (as Local Dev's Activate
|
|
953
|
+
* does), then view it (POST /api/scope, the top bar's Workspace switch),
|
|
954
|
+
* then reload, as that switch does.
|
|
955
|
+
*/
|
|
956
|
+
async function switchWorkspace(button) {
|
|
957
|
+
const ws = st.data?.requested?.workspace;
|
|
958
|
+
if (!ws) return;
|
|
959
|
+
button.disabled = true;
|
|
960
|
+
try {
|
|
961
|
+
await api("POST", "/api/workspaces", { name: ws, action: "activate" });
|
|
962
|
+
await api("POST", "/api/scope", { scope: ws });
|
|
963
|
+
} catch (err) {
|
|
964
|
+
window.denShell?.errorToast?.(err);
|
|
965
|
+
button.disabled = false;
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
location.reload();
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** Review: the first interrupted agent, scrolled to and focused. */
|
|
972
|
+
function review() {
|
|
973
|
+
const first = st.root.querySelector('#agl .agst[data-state="interrupted"]');
|
|
974
|
+
if (!first) return;
|
|
975
|
+
for (const el of st.root.querySelectorAll("#agl .agst[data-review]"))
|
|
976
|
+
el.removeAttribute("data-review");
|
|
977
|
+
for (const el of st.root.querySelectorAll(
|
|
978
|
+
'#agl .agst[data-state="interrupted"]',
|
|
979
|
+
))
|
|
980
|
+
el.setAttribute("data-review", "");
|
|
981
|
+
first.scrollIntoView({ block: "nearest" });
|
|
982
|
+
(
|
|
983
|
+
first.querySelector('[data-act="resume"]') ?? first.querySelector(".agr")
|
|
984
|
+
)?.focus();
|
|
985
|
+
}
|
|
986
|
+
|
|
624
987
|
// ── Start agent ─────────────────────────────────────────────────────────
|
|
625
988
|
|
|
626
989
|
// Personas and runtimes, per workspace (AG-0.5: a workspace's own runtime
|
|
@@ -761,6 +1124,7 @@ function translateStatic(root) {
|
|
|
761
1124
|
};
|
|
762
1125
|
set("#ag-h", "n.agents");
|
|
763
1126
|
set("#ag-start", "ag.start");
|
|
1127
|
+
set("#ag-clear", "ag.clear");
|
|
764
1128
|
set("#ag-dlg-h", "ag.start");
|
|
765
1129
|
set("#ag-ws-t", "ag.ws");
|
|
766
1130
|
set("#ag-p-t", "ag.persona");
|
|
@@ -782,6 +1146,23 @@ export async function mountAgentsPage(root) {
|
|
|
782
1146
|
const startAsked = new URLSearchParams(location.search).get("start") === "1";
|
|
783
1147
|
|
|
784
1148
|
root.querySelector("#agl").addEventListener("click", (e) => {
|
|
1149
|
+
const act = e.target.closest("button[data-act]");
|
|
1150
|
+
if (act) {
|
|
1151
|
+
const which = act.dataset.act;
|
|
1152
|
+
if (which === "resume-all") return void resumeAll(act);
|
|
1153
|
+
if (which === "review") return review();
|
|
1154
|
+
if (which === "switch-ws") return void switchWorkspace(act);
|
|
1155
|
+
const agent = act.closest(".agst")?.dataset.agent;
|
|
1156
|
+
const ws = act.closest(".agst")?.dataset.ws;
|
|
1157
|
+
const r = visibleRows().find(
|
|
1158
|
+
(x) => x.agent === agent && x.workspace === ws,
|
|
1159
|
+
);
|
|
1160
|
+
if (!r) return;
|
|
1161
|
+
if (which === "resume" || which === "fresh" || which === "relaunch")
|
|
1162
|
+
void restartRow(r, which, act);
|
|
1163
|
+
else if (which === "remove") void removeRow(r, act);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
785
1166
|
const b = e.target.closest(".agr");
|
|
786
1167
|
if (!b) return;
|
|
787
1168
|
if (b.getAttribute("aria-disabled") === "true") return;
|
|
@@ -797,11 +1178,17 @@ export async function mountAgentsPage(root) {
|
|
|
797
1178
|
if (!p) return;
|
|
798
1179
|
if (b.dataset.act === "close") closePane(id);
|
|
799
1180
|
else if (b.dataset.act === "stop") void stopAgent(p, b);
|
|
800
|
-
else if (b.dataset.act === "
|
|
1181
|
+
else if (b.dataset.act === "resume") {
|
|
1182
|
+
const r = rowOf(id);
|
|
1183
|
+
if (r) void restartRow(r, "resume", b);
|
|
1184
|
+
} else if (b.dataset.act === "reconnect") void connect(p);
|
|
801
1185
|
});
|
|
802
1186
|
root.querySelector("#ag-start").addEventListener("click", () => {
|
|
803
1187
|
void openStart();
|
|
804
1188
|
});
|
|
1189
|
+
root.querySelector("#ag-clear")?.addEventListener("click", (e) => {
|
|
1190
|
+
void clearStopped(e.currentTarget);
|
|
1191
|
+
});
|
|
805
1192
|
root.querySelector("#ag-ws").addEventListener("change", (e) => {
|
|
806
1193
|
void fillForWorkspace(e.currentTarget.value);
|
|
807
1194
|
});
|
|
@@ -79,7 +79,9 @@ function stateText(state) {
|
|
|
79
79
|
return m[1] === undefined
|
|
80
80
|
? t("ag.state.exited")
|
|
81
81
|
: t("ag.state.exitedCode", { code: m[1] });
|
|
82
|
-
return ["running", "stopping", "stopped", "ended"].includes(
|
|
82
|
+
return ["running", "stopping", "stopped", "ended", "interrupted"].includes(
|
|
83
|
+
state,
|
|
84
|
+
)
|
|
83
85
|
? t(`ag.state.${state}`)
|
|
84
86
|
: state;
|
|
85
87
|
}
|
|
@@ -219,9 +221,21 @@ async function openForm(sec) {
|
|
|
219
221
|
async function act(ws, verb, id, button) {
|
|
220
222
|
button.disabled = true;
|
|
221
223
|
try {
|
|
222
|
-
await api(
|
|
224
|
+
const out = await api(
|
|
225
|
+
"POST",
|
|
226
|
+
`${base(ws)}/${encodeURIComponent(id)}/${verb}`,
|
|
227
|
+
);
|
|
223
228
|
window.denShell?.toast?.(
|
|
224
|
-
t(
|
|
229
|
+
t(
|
|
230
|
+
verb === "stop"
|
|
231
|
+
? out?.stillStopping
|
|
232
|
+
? "ag.stillStopping"
|
|
233
|
+
: "ag.stopped"
|
|
234
|
+
: out?.fresh
|
|
235
|
+
? "ag.resumedFresh"
|
|
236
|
+
: "ag.resumed",
|
|
237
|
+
{ id },
|
|
238
|
+
),
|
|
225
239
|
);
|
|
226
240
|
} catch (err) {
|
|
227
241
|
window.denShell?.errorToast?.(err);
|
|
@@ -85,6 +85,9 @@
|
|
|
85
85
|
display: flex;
|
|
86
86
|
gap: 8px;
|
|
87
87
|
}
|
|
88
|
+
.ph .act [hidden] {
|
|
89
|
+
display: none;
|
|
90
|
+
}
|
|
88
91
|
.mono {
|
|
89
92
|
font-family: var(--mono);
|
|
90
93
|
}
|
|
@@ -163,6 +166,44 @@
|
|
|
163
166
|
overflow: hidden;
|
|
164
167
|
text-overflow: ellipsis;
|
|
165
168
|
}
|
|
169
|
+
/* AG-14: a stopped agent's row. Its why is the row's .meta line; its
|
|
170
|
+
actions are the mock's .btn.sm (den-btn sm), starting under the name
|
|
171
|
+
(the row's 12px padding, its 2px border, the 8px dot with its 5px
|
|
172
|
+
margins (.den-dot), the 8px gap: 40px). */
|
|
173
|
+
.agr .why {
|
|
174
|
+
grid-column: 2 / 4;
|
|
175
|
+
font: 11.5px var(--mono);
|
|
176
|
+
color: var(--faint);
|
|
177
|
+
white-space: nowrap;
|
|
178
|
+
overflow: hidden;
|
|
179
|
+
text-overflow: ellipsis;
|
|
180
|
+
}
|
|
181
|
+
.agst .agacts {
|
|
182
|
+
display: flex;
|
|
183
|
+
flex-wrap: wrap;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: 6px;
|
|
186
|
+
padding: 0 12px 8px 40px;
|
|
187
|
+
}
|
|
188
|
+
.agst .agcant {
|
|
189
|
+
flex-basis: 100%;
|
|
190
|
+
font: 12px/1.4 var(--sans);
|
|
191
|
+
color: var(--faint);
|
|
192
|
+
}
|
|
193
|
+
.agst[data-review] > .agr {
|
|
194
|
+
border-left-color: var(--warn);
|
|
195
|
+
}
|
|
196
|
+
/* The mock's .lnk: a button that reads as a link. */
|
|
197
|
+
.lnk {
|
|
198
|
+
color: var(--accent-text);
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
font: inherit;
|
|
201
|
+
font-size: 12.5px;
|
|
202
|
+
text-decoration: underline;
|
|
203
|
+
background: none;
|
|
204
|
+
border: 0;
|
|
205
|
+
padding: 0;
|
|
206
|
+
}
|
|
166
207
|
.wbdg {
|
|
167
208
|
font: 10.5px var(--mono);
|
|
168
209
|
color: var(--warn);
|
|
@@ -424,6 +465,9 @@
|
|
|
424
465
|
</div>
|
|
425
466
|
</div>
|
|
426
467
|
<div class="act">
|
|
468
|
+
<button type="button" class="den-btn" id="ag-clear" hidden>
|
|
469
|
+
Clear stopped
|
|
470
|
+
</button>
|
|
427
471
|
<button type="button" class="den-btn primary" id="ag-start">
|
|
428
472
|
+ Start agent
|
|
429
473
|
</button>
|
|
@@ -400,7 +400,47 @@
|
|
|
400
400
|
"ag.cancel": "Cancelar",
|
|
401
401
|
"ag.started": "Agente {id} iniciado",
|
|
402
402
|
"ag.stopped": "Agente {id} detenido",
|
|
403
|
+
"ag.stillStopping": "{id} todavía se está deteniendo",
|
|
404
|
+
"ag.resumedFresh": "{id}: aún no hay nada que reanudar; se inició de nuevo",
|
|
405
|
+
"ag.inactive": "El espacio de trabajo {ws} no está activo",
|
|
406
|
+
"ag.switchWs": "Cambiar a {ws}",
|
|
407
|
+
"ag.unknownWs": "(ningún espacio de trabajo tiene ese nombre)",
|
|
403
408
|
"ag.resumed": "Agente {id} reanudado",
|
|
409
|
+
"ag.state.interrupted": "interrumpido",
|
|
410
|
+
"ag.why.stopped": "Lo detuviste a las {time}",
|
|
411
|
+
"ag.why.interrupted": "Interrumpido: {name} se reinició",
|
|
412
|
+
"ag.why.exited": "Salió",
|
|
413
|
+
"ag.why.exitedCode": "Salió (código {code})",
|
|
414
|
+
"ag.fresh": "Empezar de cero",
|
|
415
|
+
"ag.freshName": "Empezar {id} de cero",
|
|
416
|
+
"ag.startedFresh": "{id} empezó de cero",
|
|
417
|
+
"ag.rot.ready": "Listo para relanzar ({why})",
|
|
418
|
+
"ag.rot.due": "Falta el traspaso ({why})",
|
|
419
|
+
"ag.rot.compactions": "{n} compactaciones",
|
|
420
|
+
"ag.rot.compactions.one": "{n} compactación",
|
|
421
|
+
"ag.rot.merged": "PR fusionado",
|
|
422
|
+
"ag.rot.noHandoff":
|
|
423
|
+
"Su sesión rotó ({why}), pero no escribió handoff.md. Reanúdalo para que lo escriba, o empieza de cero.",
|
|
424
|
+
"ag.rot.badHandoff":
|
|
425
|
+
"Su sesión rotó ({why}), pero su handoff.md no está completo. Reanúdalo para arreglarlo, o empieza de cero.",
|
|
426
|
+
"ag.relaunch": "Relanzar desde el traspaso",
|
|
427
|
+
"ag.relaunchName": "Relanzar {id} desde el traspaso",
|
|
428
|
+
"ag.relaunched": "{id} relanzado desde su traspaso",
|
|
429
|
+
"ag.remove": "Quitar",
|
|
430
|
+
"ag.removeName": "Quitar {id}",
|
|
431
|
+
"ag.removed": "{id} quitado",
|
|
432
|
+
"ag.removedMany": "{n} agentes detenidos quitados",
|
|
433
|
+
"ag.removedMany.one": "{n} agente detenido quitado",
|
|
434
|
+
"ag.undo": "Deshacer",
|
|
435
|
+
"ag.restored": "De vuelta en la lista: {ids}",
|
|
436
|
+
"ag.clear": "Quitar detenidos",
|
|
437
|
+
"ag.noResume": "{runtime} no puede reanudar una conversación",
|
|
438
|
+
"ag.interrupted": "{n} agentes se interrumpieron al reiniciarse {name}",
|
|
439
|
+
"ag.interrupted.one": "{n} agente se interrumpió al reiniciarse {name}",
|
|
440
|
+
"ag.resumeAll": "Reanudar todos",
|
|
441
|
+
"ag.review": "Revisar",
|
|
442
|
+
"ag.resumedAll": "{n} agentes reanudados",
|
|
443
|
+
"ag.resumedAll.one": "{n} agente reanudado",
|
|
404
444
|
// The Agents tab (AG-4a-term, agents-page.js).
|
|
405
445
|
"n.agents": "Agentes",
|
|
406
446
|
"kind.agent": "Agente",
|