@monster-orange/den 0.4.7 → 0.4.8

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +120 -1
  4. package/package.json +1 -1
  5. package/schemas/den.plugin.v1.json +3 -2
  6. package/src/shared/agents/den-claude.ts +19 -0
  7. package/src/shared/agents/personas.ts +11 -1
  8. package/src/shared/agents/records.ts +114 -0
  9. package/src/shared/agents/runtime.ts +12 -0
  10. package/src/shared/den-config.ts +30 -0
  11. package/src/shared/plugins/manifest.ts +3 -0
  12. package/src/shared/usage/paths.ts +16 -0
  13. package/src/shared/usage/report.ts +283 -0
  14. package/src/shared/usage/sources.ts +127 -0
  15. package/src/shared/usage/transcript.ts +262 -0
  16. package/src/ui/cli/agent-ops.ts +488 -50
  17. package/src/ui/cli/agents-command.ts +120 -14
  18. package/src/ui/cli/index.ts +13 -1
  19. package/src/ui/cli/usage-command.ts +256 -0
  20. package/src/ui/mcp/api.ts +10 -1
  21. package/src/ui/mcp/server.ts +34 -0
  22. package/src/ui/mcp/tools.ts +50 -0
  23. package/src/ui/server/agent-resume-grant.ts +53 -0
  24. package/src/ui/server/agent-routes.ts +230 -9
  25. package/src/ui/server/audit.ts +68 -1
  26. package/src/ui/server/index.ts +16 -5
  27. package/src/ui/server/public/agents-page.js +333 -11
  28. package/src/ui/server/public/agents-section.js +17 -3
  29. package/src/ui/server/public/agents.html +44 -0
  30. package/src/ui/server/public/i18n/es.js +28 -0
  31. package/src/ui/server/public/i18n/ja.js +25 -0
  32. package/src/ui/server/public/shell.js +66 -0
  33. package/src/ui/server/request-guard.ts +21 -1
  34. package/src/ui/server/route-classes.ts +23 -0
  35. package/src/ui/server/session-routes.ts +52 -4
  36. package/src/ui/server/sudo-store.ts +36 -7
@@ -31,6 +31,15 @@
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.
34
43
  */
35
44
  import { fillStartForm } from "/agent-start.js";
36
45
 
@@ -45,6 +54,9 @@ const esc = (s) =>
45
54
  );
46
55
  const api = (method, url, body) => window.denShell.api(method, url, body);
47
56
  const CLI = "__DEN_CLI__";
57
+ const TITLE = "__DEN_TITLE__";
58
+ /** How long Remove's Undo stays offered (AG-14.1b). */
59
+ export const UNDO_MS = 10_000;
48
60
 
49
61
  /** The most panes at once (the prototype's 2x2). */
50
62
  export const MAX_PANES = 4;
@@ -88,11 +100,17 @@ function writeUrl() {
88
100
 
89
101
  // ── the list ────────────────────────────────────────────────────────────
90
102
 
91
- /** The workspaces to show: ?ws= (when active), else the scope bar's view. */
103
+ /** ?ws=: the one workspace the page was asked for, or null. */
104
+ const urlWorkspace = () => new URLSearchParams(location.search).get("ws");
105
+
106
+ /**
107
+ * The workspaces to show: ?ws= (always that one, even when it isn't active
108
+ * or isn't known: never All in its place), else the scope bar's view.
109
+ */
92
110
  function viewWorkspaces() {
93
111
  const all = st.data?.workspaces ?? [];
94
- const ws = new URLSearchParams(location.search).get("ws");
95
- if (ws && all.includes(ws)) return [ws];
112
+ const ws = urlWorkspace();
113
+ if (ws) return [ws];
96
114
  if (st.scope && st.scope !== "all" && all.includes(st.scope))
97
115
  return [st.scope];
98
116
  return all;
@@ -112,7 +130,9 @@ function stateText(state) {
112
130
  return m[1] === undefined
113
131
  ? t("ag.state.exited")
114
132
  : t("ag.state.exitedCode", { code: m[1] });
115
- return ["running", "stopping", "stopped", "ended"].includes(state)
133
+ return ["running", "stopping", "stopped", "ended", "interrupted"].includes(
134
+ state,
135
+ )
116
136
  ? t(`ag.state.${state}`)
117
137
  : state;
118
138
  }
@@ -127,7 +147,73 @@ function paneName(r) {
127
147
  return r.kind === "agent" ? r.displayName || r.agent || r.label : r.label;
128
148
  }
129
149
 
150
+ /** An agent that isn't running: stopped, interrupted or exited (AG-14). */
151
+ export function isStopped(r) {
152
+ return r.kind === "agent" && r.state !== "running" && r.state !== "stopping";
153
+ }
154
+
155
+ /** "19:52" today, else "Nov 6, 19:52" (the browser's own format). */
156
+ function when(iso) {
157
+ const d = new Date(iso);
158
+ if (Number.isNaN(d.getTime())) return "";
159
+ const time = d.toLocaleTimeString([], {
160
+ hour: "2-digit",
161
+ minute: "2-digit",
162
+ hour12: false,
163
+ });
164
+ const today = new Date().toDateString() === d.toDateString();
165
+ return today
166
+ ? time
167
+ : `${d.toLocaleDateString([], { month: "short", day: "numeric" })}, ${time}`;
168
+ }
169
+
170
+ /** Why a stopped agent stopped (AG-14.1). */
171
+ export function stopReason(r) {
172
+ if (r.state === "interrupted")
173
+ return t("ag.why.interrupted", { name: TITLE });
174
+ if (r.state === "stopped")
175
+ return t("ag.why.stopped", { time: when(r.stoppedAt ?? "") }).trim();
176
+ const m = /^exited(?: \((-?\d+)\))?$/.exec(r.state);
177
+ if (m)
178
+ return m[1] === undefined
179
+ ? t("ag.why.exited")
180
+ : t("ag.why.exitedCode", { code: m[1] });
181
+ return stateText(r.state);
182
+ }
183
+
184
+ /** A stopped agent's row: its button (it opens an ended pane) and actions. */
185
+ function stoppedHtml(r) {
186
+ const open = st.open.includes(r.id);
187
+ const name = displayName(r);
188
+ const nm = `${esc(name)}${r.persona && r.persona !== r.label ? ` <span class="id">${esc(r.label)}</span>` : ""}`;
189
+ const meta = [r.runtime, r.cwd].filter(Boolean).join(" · ");
190
+ const who = esc(r.agent);
191
+ const resume = r.resumable
192
+ ? `<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>`
193
+ : "";
194
+ 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>`;
195
+ 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>`;
196
+ const cant =
197
+ !r.resumable && r.noResume
198
+ ? `<span class="agcant" data-no-resume>${esc(r.noResume.startsWith(`${r.runtime} `) ? t("ag.noResume", { runtime: r.runtime }) : r.noResume)}</span>`
199
+ : "";
200
+ return (
201
+ `<div class="agst" data-agent="${who}" data-state="${esc(r.state)}" data-ws="${esc(r.workspace)}">` +
202
+ `<button type="button" class="agr" data-id="${esc(r.id)}" data-kind="agent" data-state="${esc(r.state)}"` +
203
+ ` aria-pressed="${open}"${r.attachable ? "" : ' aria-disabled="true"'}` +
204
+ ` aria-label="${esc(t("ag.openName", { id: `${r.workspace} / ${name}` }))}">` +
205
+ `<span class="den-dot ${stateTone(r)}" aria-hidden="true"></span>` +
206
+ `<span class="nm">${nm}</span><span class="st">${esc(stateText(r.state))}</span>` +
207
+ (meta ? `<span class="meta">${esc(meta)}</span>` : "") +
208
+ `<span class="why">${esc(stopReason(r))}</span>` +
209
+ `</button>` +
210
+ `<div class="agacts">${cant}${resume}${fresh}${remove}</div>` +
211
+ `</div>`
212
+ );
213
+ }
214
+
130
215
  function rowHtml(r) {
216
+ if (isStopped(r)) return stoppedHtml(r);
131
217
  const open = st.open.includes(r.id);
132
218
  const nm =
133
219
  r.kind === "agent"
@@ -169,10 +255,40 @@ function paintList() {
169
255
  return;
170
256
  }
171
257
  const shown = viewWorkspaces();
258
+ // ?ws= names a workspace that isn't active: say so, and offer the switch.
259
+ const asked = st.data.requested;
260
+ if (asked && !asked.active)
261
+ notes.push(
262
+ `<div class="agnote" data-note="inactive" role="status">` +
263
+ esc(t("ag.inactive", { ws: asked.workspace })) +
264
+ (asked.known
265
+ ? ` · <button type="button" class="lnk" data-act="switch-ws">${esc(t("ag.switchWs", { ws: asked.workspace }))}</button>`
266
+ : ` ${esc(t("ag.unknownWs"))}`) +
267
+ `</div>`,
268
+ );
269
+ // AG-14.3: one line after a restart, for the agents it interrupted.
270
+ const hit = visibleRows().filter((r) => r.state === "interrupted");
271
+ if (hit.length)
272
+ notes.push(
273
+ `<div class="agnote" data-note="interrupted" role="status">` +
274
+ `${esc(t(hit.length === 1 ? "ag.interrupted.one" : "ag.interrupted", { n: hit.length, name: TITLE }))}` +
275
+ (hit.some((r) => r.resumable)
276
+ ? ` · <button type="button" class="lnk" data-act="resume-all">${esc(t("ag.resumeAll"))}</button>`
277
+ : "") +
278
+ ` · <button type="button" class="lnk" data-act="review">${esc(t("ag.review"))}</button></div>`,
279
+ );
172
280
  const groups = shown
173
281
  .map((ws) => ({
174
282
  ws,
175
- rows: st.data.sessions.filter((s) => s.workspace === ws),
283
+ // Running ones (and plain sessions) first, then the stopped agents.
284
+ rows: st.data.sessions
285
+ .filter((s) => s.workspace === ws)
286
+ .map((s, i) => ({ s, i }))
287
+ .sort(
288
+ (a, b) =>
289
+ Number(isStopped(a.s)) - Number(isStopped(b.s)) || a.i - b.i,
290
+ )
291
+ .map((x) => x.s),
176
292
  }))
177
293
  .filter((g) => g.rows.length > 0);
178
294
  const body = groups.length
@@ -186,6 +302,11 @@ function paintList() {
186
302
  .join("")
187
303
  : `<div class="none">${esc(t("ag.none.scope", { scope: st.scopeLabel }))}</div>`;
188
304
  el.innerHTML = notes.join("") + body;
305
+ const clear = st.root.querySelector("#ag-clear");
306
+ if (clear) clear.hidden = !visibleRows().some(isStopped);
307
+ // No such workspace: nothing to start an agent in.
308
+ const start = st.root.querySelector("#ag-start");
309
+ if (start) start.hidden = st.data.requested?.known === false;
189
310
  }
190
311
 
191
312
  function visibleRows() {
@@ -219,7 +340,7 @@ async function loadScope() {
219
340
  }
220
341
 
221
342
  function scopeLabelFor(shown) {
222
- const ws = new URLSearchParams(location.search).get("ws");
343
+ const ws = urlWorkspace();
223
344
  if (ws && shown.length === 1 && shown[0] === ws) return ws;
224
345
  return st.scope === "all" ? t("ag.all") : st.scope;
225
346
  }
@@ -227,7 +348,11 @@ function scopeLabelFor(shown) {
227
348
  async function refresh() {
228
349
  clearTimeout(st.timer);
229
350
  try {
230
- const r = await fetch("/api/sessions", { headers: { accept: "json" } });
351
+ const ws = urlWorkspace();
352
+ const r = await fetch(
353
+ ws ? `/api/sessions?ws=${encodeURIComponent(ws)}` : "/api/sessions",
354
+ { headers: { accept: "json" } },
355
+ );
231
356
  if (r.status === 401) {
232
357
  st.signedOut = true;
233
358
  } else if (r.ok) {
@@ -325,6 +450,11 @@ function paintBar(p) {
325
450
  r?.kind === "agent" && (r.state === "running" || r.state === "stopping")
326
451
  ? `<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
452
  : "";
453
+ // The mock's pane bar: Resume (primary) on an ended agent that can.
454
+ const resume =
455
+ r && isStopped(r) && r.resumable
456
+ ? `<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>`
457
+ : "";
328
458
  const reconnect = p.lost
329
459
  ? `<button type="button" class="den-btn sm" data-act="reconnect">${esc(t("ag.reconnect"))}</button>`
330
460
  : "";
@@ -334,7 +464,7 @@ function paintBar(p) {
334
464
  (r?.tokenRevoked
335
465
  ? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
336
466
  : "") +
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>`;
467
+ `<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
468
  }
339
469
 
340
470
  function send(p, obj) {
@@ -610,17 +740,185 @@ async function stopAgent(p, button) {
610
740
  if (!r?.agent) return;
611
741
  button.disabled = true;
612
742
  try {
613
- await api(
743
+ const out = await api(
614
744
  "POST",
615
745
  `/api/workspaces/${encodeURIComponent(r.workspace)}/agents/${encodeURIComponent(r.agent)}/stop`,
616
746
  );
617
- window.denShell?.toast?.(t("ag.stopped", { id: r.agent }));
747
+ window.denShell?.toast?.(
748
+ t(out?.stillStopping ? "ag.stillStopping" : "ag.stopped", {
749
+ id: r.agent,
750
+ }),
751
+ );
618
752
  } catch (err) {
619
753
  window.denShell?.errorToast?.(err);
620
754
  }
621
755
  await refresh();
622
756
  }
623
757
 
758
+ // ── Stopped agents (AG-14) ──────────────────────────────────────────────
759
+
760
+ const agentPath = (r, verb) =>
761
+ `/api/workspaces/${encodeURIComponent(r.workspace)}/agents/${encodeURIComponent(r.agent)}/${verb}`;
762
+
763
+ /** Resume (the same conversation) or Start fresh; its pane opens on it. */
764
+ async function restartRow(r, verb, button) {
765
+ if (button) button.disabled = true;
766
+ try {
767
+ const out = await api("POST", agentPath(r, verb));
768
+ window.denShell?.toast?.(
769
+ t(
770
+ verb !== "resume"
771
+ ? "ag.startedFresh"
772
+ : out?.fresh
773
+ ? "ag.resumedFresh"
774
+ : "ag.resumed",
775
+ { id: r.agent },
776
+ ),
777
+ );
778
+ if (st.panes.has(r.id)) closePane(r.id, { quiet: true });
779
+ await refresh();
780
+ if (out?.agent?.sessionId) await openPane(out.agent.sessionId);
781
+ } catch (err) {
782
+ window.denShell?.errorToast?.(err);
783
+ await refresh();
784
+ }
785
+ }
786
+
787
+ /** Undo: the removed agents come back (POST /api/agents/restore). */
788
+ async function undoRemove(ids) {
789
+ try {
790
+ const out = await api("POST", "/api/agents/restore", { agents: ids });
791
+ const back = out?.restored ?? [];
792
+ if (back.length)
793
+ window.denShell?.toast?.(t("ag.restored", { ids: back.join(", ") }));
794
+ } catch (err) {
795
+ window.denShell?.errorToast?.(err);
796
+ }
797
+ await refresh();
798
+ }
799
+
800
+ /** The removal's toast, with Undo for UNDO_MS. */
801
+ function removedToast(msg, ids) {
802
+ const shell = window.denShell;
803
+ if (shell?.toastAction)
804
+ shell.toastAction(msg, t("ag.undo"), () => void undoRemove(ids), UNDO_MS);
805
+ else shell?.toast?.(msg);
806
+ }
807
+
808
+ /** Remove: one click, no confirm (AG-14.1b). */
809
+ async function removeRow(r, button) {
810
+ if (button) button.disabled = true;
811
+ try {
812
+ const out = await api("POST", agentPath(r, "remove"));
813
+ if (st.panes.has(r.id)) closePane(r.id, { quiet: true });
814
+ const ids = (out?.removed ?? []).map((x) => x.agent);
815
+ await refresh();
816
+ removedToast(
817
+ t("ag.removed", { id: r.agent }),
818
+ ids.length ? ids : [r.agent],
819
+ );
820
+ } catch (err) {
821
+ window.denShell?.errorToast?.(err);
822
+ await refresh();
823
+ }
824
+ }
825
+
826
+ /** Clear stopped: every stopped agent in view (AG-14.1b). */
827
+ async function clearStopped(button) {
828
+ const workspaces = [
829
+ ...new Set(
830
+ visibleRows()
831
+ .filter(isStopped)
832
+ .map((r) => r.workspace),
833
+ ),
834
+ ];
835
+ if (!workspaces.length) return;
836
+ button.disabled = true;
837
+ try {
838
+ const out = await api("POST", "/api/agents/clear-stopped", { workspaces });
839
+ const ids = (out?.removed ?? []).map((x) => x.agent);
840
+ for (const r of visibleRows())
841
+ if (ids.includes(r.agent) && st.panes.has(r.id))
842
+ closePane(r.id, { quiet: true });
843
+ await refresh();
844
+ if (ids.length)
845
+ removedToast(
846
+ t(ids.length === 1 ? "ag.removedMany.one" : "ag.removedMany", {
847
+ n: ids.length,
848
+ }),
849
+ ids,
850
+ );
851
+ } catch (err) {
852
+ window.denShell?.errorToast?.(err);
853
+ await refresh();
854
+ } finally {
855
+ button.disabled = false;
856
+ }
857
+ }
858
+
859
+ /** Resume all: every interrupted agent in view that can resume (AG-14.3). */
860
+ async function resumeAll(button) {
861
+ const workspaces = [
862
+ ...new Set(
863
+ visibleRows()
864
+ .filter((r) => r.state === "interrupted")
865
+ .map((r) => r.workspace),
866
+ ),
867
+ ];
868
+ if (!workspaces.length) return;
869
+ button.disabled = true;
870
+ try {
871
+ const out = await window.denShell.api(
872
+ "POST",
873
+ "/api/agents/resume-interrupted",
874
+ { workspaces },
875
+ );
876
+ const n = out?.resumed?.length ?? 0;
877
+ window.denShell?.toast?.(
878
+ t(n === 1 ? "ag.resumedAll.one" : "ag.resumedAll", { n }),
879
+ );
880
+ } catch (err) {
881
+ window.denShell?.errorToast?.(err);
882
+ }
883
+ await refresh();
884
+ }
885
+
886
+ /**
887
+ * Switch to ?ws='s workspace: make it active (as Local Dev's Activate
888
+ * does), then view it (POST /api/scope, the top bar's Workspace switch),
889
+ * then reload, as that switch does.
890
+ */
891
+ async function switchWorkspace(button) {
892
+ const ws = st.data?.requested?.workspace;
893
+ if (!ws) return;
894
+ button.disabled = true;
895
+ try {
896
+ await api("POST", "/api/workspaces", { name: ws, action: "activate" });
897
+ await api("POST", "/api/scope", { scope: ws });
898
+ } catch (err) {
899
+ window.denShell?.errorToast?.(err);
900
+ button.disabled = false;
901
+ return;
902
+ }
903
+ location.reload();
904
+ }
905
+
906
+ /** Review: the first interrupted agent, scrolled to and focused. */
907
+ function review() {
908
+ const first = st.root.querySelector('#agl .agst[data-state="interrupted"]');
909
+ if (!first) return;
910
+ for (const el of st.root.querySelectorAll("#agl .agst[data-review]"))
911
+ el.removeAttribute("data-review");
912
+ for (const el of st.root.querySelectorAll(
913
+ '#agl .agst[data-state="interrupted"]',
914
+ ))
915
+ el.setAttribute("data-review", "");
916
+ first.scrollIntoView({ block: "nearest" });
917
+ (
918
+ first.querySelector('[data-act="resume"]') ?? first.querySelector(".agr")
919
+ )?.focus();
920
+ }
921
+
624
922
  // ── Start agent ─────────────────────────────────────────────────────────
625
923
 
626
924
  // Personas and runtimes, per workspace (AG-0.5: a workspace's own runtime
@@ -761,6 +1059,7 @@ function translateStatic(root) {
761
1059
  };
762
1060
  set("#ag-h", "n.agents");
763
1061
  set("#ag-start", "ag.start");
1062
+ set("#ag-clear", "ag.clear");
764
1063
  set("#ag-dlg-h", "ag.start");
765
1064
  set("#ag-ws-t", "ag.ws");
766
1065
  set("#ag-p-t", "ag.persona");
@@ -782,6 +1081,23 @@ export async function mountAgentsPage(root) {
782
1081
  const startAsked = new URLSearchParams(location.search).get("start") === "1";
783
1082
 
784
1083
  root.querySelector("#agl").addEventListener("click", (e) => {
1084
+ const act = e.target.closest("button[data-act]");
1085
+ if (act) {
1086
+ const which = act.dataset.act;
1087
+ if (which === "resume-all") return void resumeAll(act);
1088
+ if (which === "review") return review();
1089
+ if (which === "switch-ws") return void switchWorkspace(act);
1090
+ const agent = act.closest(".agst")?.dataset.agent;
1091
+ const ws = act.closest(".agst")?.dataset.ws;
1092
+ const r = visibleRows().find(
1093
+ (x) => x.agent === agent && x.workspace === ws,
1094
+ );
1095
+ if (!r) return;
1096
+ if (which === "resume" || which === "fresh")
1097
+ void restartRow(r, which, act);
1098
+ else if (which === "remove") void removeRow(r, act);
1099
+ return;
1100
+ }
785
1101
  const b = e.target.closest(".agr");
786
1102
  if (!b) return;
787
1103
  if (b.getAttribute("aria-disabled") === "true") return;
@@ -797,11 +1113,17 @@ export async function mountAgentsPage(root) {
797
1113
  if (!p) return;
798
1114
  if (b.dataset.act === "close") closePane(id);
799
1115
  else if (b.dataset.act === "stop") void stopAgent(p, b);
800
- else if (b.dataset.act === "reconnect") void connect(p);
1116
+ else if (b.dataset.act === "resume") {
1117
+ const r = rowOf(id);
1118
+ if (r) void restartRow(r, "resume", b);
1119
+ } else if (b.dataset.act === "reconnect") void connect(p);
801
1120
  });
802
1121
  root.querySelector("#ag-start").addEventListener("click", () => {
803
1122
  void openStart();
804
1123
  });
1124
+ root.querySelector("#ag-clear")?.addEventListener("click", (e) => {
1125
+ void clearStopped(e.currentTarget);
1126
+ });
805
1127
  root.querySelector("#ag-ws").addEventListener("change", (e) => {
806
1128
  void fillForWorkspace(e.currentTarget.value);
807
1129
  });
@@ -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(state)
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("POST", `${base(ws)}/${encodeURIComponent(id)}/${verb}`);
224
+ const out = await api(
225
+ "POST",
226
+ `${base(ws)}/${encodeURIComponent(id)}/${verb}`,
227
+ );
223
228
  window.denShell?.toast?.(
224
- t(verb === "stop" ? "ag.stopped" : "ag.resumed", { id }),
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,35 @@
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.remove": "Quitar",
418
+ "ag.removeName": "Quitar {id}",
419
+ "ag.removed": "{id} quitado",
420
+ "ag.removedMany": "{n} agentes detenidos quitados",
421
+ "ag.removedMany.one": "{n} agente detenido quitado",
422
+ "ag.undo": "Deshacer",
423
+ "ag.restored": "De vuelta en la lista: {ids}",
424
+ "ag.clear": "Quitar detenidos",
425
+ "ag.noResume": "{runtime} no puede reanudar una conversación",
426
+ "ag.interrupted": "{n} agentes se interrumpieron al reiniciarse {name}",
427
+ "ag.interrupted.one": "{n} agente se interrumpió al reiniciarse {name}",
428
+ "ag.resumeAll": "Reanudar todos",
429
+ "ag.review": "Revisar",
430
+ "ag.resumedAll": "{n} agentes reanudados",
431
+ "ag.resumedAll.one": "{n} agente reanudado",
404
432
  // The Agents tab (AG-4a-term, agents-page.js).
405
433
  "n.agents": "Agentes",
406
434
  "kind.agent": "Agente",
@@ -387,7 +387,32 @@
387
387
  "ag.cancel": "キャンセル",
388
388
  "ag.started": "エージェント {id} を開始しました",
389
389
  "ag.stopped": "エージェント {id} を停止しました",
390
+ "ag.stillStopping": "{id} はまだ停止中です",
391
+ "ag.resumedFresh": "{id}: まだ再開するものがないため、新しく開始しました",
392
+ "ag.inactive": "ワークスペース {ws} はアクティブではありません",
393
+ "ag.switchWs": "{ws} に切り替え",
394
+ "ag.unknownWs": "(その名前のワークスペースはありません)",
390
395
  "ag.resumed": "エージェント {id} を再開しました",
396
+ "ag.state.interrupted": "中断",
397
+ "ag.why.stopped": "{time} に停止しました",
398
+ "ag.why.interrupted": "中断: {name} が再起動しました",
399
+ "ag.why.exited": "終了しました",
400
+ "ag.why.exitedCode": "終了しました(コード {code})",
401
+ "ag.fresh": "新しく始める",
402
+ "ag.freshName": "{id} を新しく始める",
403
+ "ag.startedFresh": "{id} を新しく始めました",
404
+ "ag.remove": "削除",
405
+ "ag.removeName": "{id} を削除",
406
+ "ag.removed": "{id} を削除しました",
407
+ "ag.removedMany": "停止中のエージェント {n} 件を削除しました",
408
+ "ag.undo": "元に戻す",
409
+ "ag.restored": "一覧に戻しました: {ids}",
410
+ "ag.clear": "停止中を削除",
411
+ "ag.noResume": "{runtime} は会話を再開できません",
412
+ "ag.interrupted": "{name} の再起動でエージェント {n} 件が中断されました",
413
+ "ag.resumeAll": "すべて再開",
414
+ "ag.review": "確認",
415
+ "ag.resumedAll": "エージェント {n} 件を再開しました",
391
416
  // The Agents tab (AG-4a-term, agents-page.js).
392
417
  "n.agents": "エージェント",
393
418
  "kind.agent": "エージェント",