@monster-orange/den 0.4.13 → 0.4.14

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.
@@ -29,6 +29,18 @@
29
29
  * instruction is typed into
30
30
  * the session once it is
31
31
  * ready, never an argument
32
+ * GET /api/workspaces/<ws>/conversations[?runtime=<r>] Bring in
33
+ * (AG-17.1): each runtime's
34
+ * own conversations in the
35
+ * workspace's folder, newest
36
+ * first, with which runtimes
37
+ * can bring one in
38
+ * POST /api/workspaces/<ws>/conversations/<id>/import { persona,
39
+ * runtime?, id? }: the
40
+ * runtime's resume of it, as
41
+ * a Den agent; 409
42
+ * open_elsewhere when it was
43
+ * written in the last minute
32
44
  * POST /api/workspaces/<ws>/sessions a plain session: the login
33
45
  * shell in the workspace's
34
46
  * folder (AG-10.1)
@@ -86,12 +98,14 @@ import {
86
98
  } from "../../shared/den-config.js";
87
99
  import {
88
100
  readWorkspacesFile,
101
+ repoFolders,
89
102
  resolveWorkspaceDir,
90
103
  } from "../../shared/environment-manager/workspaces.js";
91
104
  import { getStateDir } from "../../shared/paths.js";
92
105
  import { HostError } from "../../shared/sessions/client.js";
93
106
  import {
94
107
  AgentError,
108
+ defaultId,
95
109
  agentRecord,
96
110
  checkInstruction,
97
111
  createPersona,
@@ -114,6 +128,8 @@ import {
114
128
  resumeInterrupted,
115
129
  setRuntimePath,
116
130
  resolveAgentsDeps,
131
+ importAgent,
132
+ listConversations,
117
133
  relaunchAgent,
118
134
  resumeAgent,
119
135
  startAgent,
@@ -131,7 +147,10 @@ import {
131
147
  auditActorOf,
132
148
  principalOf,
133
149
  } from "./request-guard.js";
134
- import { agentResumeRefusal } from "./agent-resume-grant.js";
150
+ import {
151
+ agentImportRefusal,
152
+ agentResumeRefusal,
153
+ } from "./agent-resume-grant.js";
135
154
  import { readBody, sendJson } from "./http-helpers.js";
136
155
  import type { Actor } from "../../shared/agent-actor.js";
137
156
 
@@ -148,6 +167,8 @@ const AGENTS_PATH =
148
167
  const RUNTIME_PATH = /^\/api\/agents\/runtimes\/([^/]+)\/path$/;
149
168
  const PERSONA_PATH = /^\/api\/personas\/([^/]+)$/;
150
169
  const PLAIN_PATH = /^\/api\/workspaces\/([^/]+)\/sessions$/;
170
+ const CONVERSATIONS_PATH =
171
+ /^\/api\/workspaces\/([^/]+)\/conversations(?:\/([^/]+)\/import)?$/;
151
172
  /** A start's body: a few names, commands and a first instruction. */
152
173
  const MAX_BODY = 32 * 1024;
153
174
  /** A list of names (a page's workspaces, an Undo's agents). */
@@ -166,6 +187,20 @@ function webDeps(over: AgentsDeps = {}): AgentsResolved {
166
187
  if (!cfg) throw new AgentError(`workspace "${ws}" does not exist`);
167
188
  return resolveWorkspaceDir(ws, cfg, getReposRoot());
168
189
  },
190
+ // AG-17.1: its folder and each by-path repo's, outside it too. A caller
191
+ // that gives its own folder (a test) gives its folders too, or just it.
192
+ ...(over.workspaceDir
193
+ ? {}
194
+ : {
195
+ workspaceFolders: (ws: string) => {
196
+ const cfg = readWorkspacesFile().workspaces[ws];
197
+ if (!cfg) throw new AgentError(`workspace "${ws}" does not exist`);
198
+ return [
199
+ resolveWorkspaceDir(ws, cfg, getReposRoot()),
200
+ ...repoFolders(cfg),
201
+ ];
202
+ },
203
+ }),
169
204
  stdout: NO_TTY,
170
205
  out: () => undefined,
171
206
  err: () => undefined,
@@ -188,6 +223,7 @@ function sendError(res: http.ServerResponse, e: unknown, ws?: string): void {
188
223
  error,
189
224
  ...(e.code && e.code !== NOT_IN_WORKSPACE ? { code: e.code } : {}),
190
225
  ...(e.hints.length ? { hints: e.hints } : {}),
226
+ ...(e.running ? { running: e.running } : {}),
191
227
  },
192
228
  e.status,
193
229
  );
@@ -275,6 +311,8 @@ interface StartBody {
275
311
  permission?: string;
276
312
  allow?: unknown;
277
313
  instruction?: unknown;
314
+ /** AG-10.10: a display name, checked as a rename's is. */
315
+ name?: unknown;
278
316
  }
279
317
 
280
318
  const START_KEYS = [
@@ -286,6 +324,7 @@ const START_KEYS = [
286
324
  "permission",
287
325
  "allow",
288
326
  "instruction",
327
+ "name",
289
328
  ];
290
329
 
291
330
  async function startBody(req: http.IncomingMessage): Promise<StartBody> {
@@ -331,6 +370,8 @@ async function startBody(req: http.IncomingMessage): Promise<StartBody> {
331
370
  }
332
371
  if (o.allow !== undefined && o.allow !== null) out.allow = o.allow;
333
372
  if (o.instruction !== undefined) out.instruction = o.instruction;
373
+ if (o.name !== undefined && o.name !== null && o.name !== "")
374
+ out.name = o.name;
334
375
  return out as StartBody;
335
376
  }
336
377
 
@@ -349,6 +390,7 @@ export async function handleAgentRoutes(
349
390
  const rp = RUNTIME_PATH.exec(pathname);
350
391
  const pp = PERSONA_PATH.exec(pathname);
351
392
  const plain = PLAIN_PATH.exec(pathname);
393
+ const conv = CONVERSATIONS_PATH.exec(pathname);
352
394
  // AG-14's routes over many agents at once.
353
395
  const many =
354
396
  pathname === "/api/agents/clear-stopped" ||
@@ -361,6 +403,7 @@ export async function handleAgentRoutes(
361
403
  !rp &&
362
404
  !pp &&
363
405
  !plain &&
406
+ !conv &&
364
407
  !many
365
408
  )
366
409
  return false;
@@ -369,6 +412,14 @@ export async function handleAgentRoutes(
369
412
 
370
413
  if (pp) return personaRoute(req, res, safeDecode(pp[1]), d);
371
414
  if (plain) return plainSession(req, res, safeDecode(plain[1]), d);
415
+ if (conv)
416
+ return conversations(
417
+ req,
418
+ res,
419
+ safeDecode(conv[1]),
420
+ conv[2] === undefined ? undefined : safeDecode(conv[2]),
421
+ d,
422
+ );
372
423
  if (pathname === "/api/personas" && method === "POST")
373
424
  return personaRoute(req, res, null, d);
374
425
 
@@ -387,6 +438,20 @@ export async function handleAgentRoutes(
387
438
  ...(pathname === "/api/personas"
388
439
  ? { personas: listPersonas(d), ...registries(d) }
389
440
  : {}),
441
+ // AG-10.10: the id a new start of each persona takes here.
442
+ ...(pathname === "/api/personas" && ws
443
+ ? {
444
+ startsAs: Object.fromEntries(
445
+ listPersonas(d).flatMap((p) => {
446
+ try {
447
+ return [[p.id, defaultId(d.stateDir(), p.id, ws)]];
448
+ } catch {
449
+ return []; // no free id: the start says so
450
+ }
451
+ }),
452
+ ),
453
+ }
454
+ : {}),
390
455
  runtimes,
391
456
  ...(ws ? { workspace: ws } : {}),
392
457
  // AG-0.2: the file a person edits to set a runtime's path.
@@ -509,6 +574,7 @@ export async function handleAgentRoutes(
509
574
  ? { permission: body.permission }
510
575
  : {}),
511
576
  ...(body.allow !== undefined ? { allow: body.allow } : {}),
577
+ ...(body.name !== undefined ? { name: body.name } : {}),
512
578
  strict: true,
513
579
  });
514
580
  const ov = s.rec.overrides?.length
@@ -600,6 +666,91 @@ export async function handleAgentRoutes(
600
666
  return true;
601
667
  }
602
668
 
669
+ const IMPORT_KEYS = ["persona", "runtime", "id"];
670
+
671
+ /**
672
+ * Bring in (AG-17.1): GET lists the workspace's conversations, POST
673
+ * …/<id>/import brings one in. A person's, or an agent's opted in through
674
+ * mcp.actions' agent_import inside its scope. Every import is audited.
675
+ */
676
+ async function conversations(
677
+ req: http.IncomingMessage,
678
+ res: http.ServerResponse,
679
+ ws: string,
680
+ session: string | undefined,
681
+ d: AgentsResolved,
682
+ ): Promise<true> {
683
+ const method = req.method ?? "GET";
684
+ const actor = auditActorOf(req);
685
+ const agent = agentPrincipalOf(req);
686
+ const allowed = () => {
687
+ if (agent) {
688
+ const why = agentImportRefusal(agent, ws, mcpActions());
689
+ if (why) throw new AgentError(why, { status: 403 });
690
+ } else guard(actor);
691
+ };
692
+ if (session === undefined) {
693
+ if (method !== "GET" && method !== "HEAD")
694
+ return methodNotAllowed(res, "GET");
695
+ try {
696
+ allowed();
697
+ const runtime =
698
+ new URL(req.url ?? "/", "http://x").searchParams.get("runtime") ??
699
+ undefined;
700
+ const listed = listConversations(d, ws, runtime);
701
+ sendJson(res, { workspace: ws, folder: d.workspaceDir(ws), ...listed });
702
+ } catch (e) {
703
+ sendError(res, e, ws);
704
+ }
705
+ return true;
706
+ }
707
+ if (method !== "POST") return methodNotAllowed(res, "POST");
708
+ let what = `bring in ${session} in ${ws}`;
709
+ const audit = (reason?: string) =>
710
+ recordAction(
711
+ actor,
712
+ {
713
+ verb: "agents",
714
+ detail: what,
715
+ ...(reason ? { outcome: "refused" as const, reason } : {}),
716
+ },
717
+ principalOf(req),
718
+ );
719
+ try {
720
+ allowed();
721
+ const body = await jsonBody(req);
722
+ for (const k of Object.keys(body))
723
+ if (!IMPORT_KEYS.includes(k))
724
+ throw new AgentError(
725
+ `${k}: unknown field (an import takes ${IMPORT_KEYS.join(", ")})`,
726
+ { usage: true },
727
+ );
728
+ const str = (k: string): string | undefined => {
729
+ const v = body[k];
730
+ if (v === undefined || v === null || v === "") return undefined;
731
+ if (typeof v !== "string" || !NAME_RE.test(v))
732
+ throw new AgentError(`${k} must be a name`, { usage: true });
733
+ return v;
734
+ };
735
+ const persona = str("persona");
736
+ if (!persona) throw new AgentError("persona is required", { usage: true });
737
+ const s = await importAgent(d, {
738
+ workspace: ws,
739
+ session,
740
+ persona,
741
+ runtime: str("runtime"),
742
+ id: str("id"),
743
+ });
744
+ what = `bring in ${session} as ${s.rec.id} (${s.rec.persona}, ${s.rec.runtime}) in ${ws}`;
745
+ audit(s.early ? "exited at once" : undefined);
746
+ sendStarted(res, s, d);
747
+ } catch (e) {
748
+ audit(e instanceof Error ? e.message : String(e));
749
+ sendError(res, e, ws);
750
+ }
751
+ return true;
752
+ }
753
+
603
754
  /** The org config's mcp.actions; none when it can't be read. */
604
755
  function mcpActions(): readonly string[] {
605
756
  try {
@@ -1535,8 +1535,99 @@ const sd = {
1535
1535
  intent: "ask",
1536
1536
  extras: [],
1537
1537
  extrasOpen: false,
1538
+ // AG-17.1, Bring in: null for Start; else the workspace's conversations,
1539
+ // the runtimes that can't bring one in, and the one picked.
1540
+ bring: null,
1538
1541
  };
1539
1542
 
1543
+ /** The conversation picked in Bring in, when it can be brought in. */
1544
+ function bringPick() {
1545
+ const c = (sd.bring?.list ?? []).find((x) => x.id === sd.bring.pick);
1546
+ return c && !c.open && !c.agent ? c : null;
1547
+ }
1548
+
1549
+ /** "3 min ago", "2 h ago", or the date. */
1550
+ function agoText(iso) {
1551
+ const ms = Date.now() - Date.parse(iso);
1552
+ if (!Number.isFinite(ms)) return "";
1553
+ const m = Math.round(ms / 60000);
1554
+ if (m < 1) return t("ag.bring.now");
1555
+ if (m < 60) return t("ag.bring.min", { n: m });
1556
+ const h = Math.round(m / 60);
1557
+ if (h < 48) return t("ag.bring.h", { n: h });
1558
+ return iso.slice(0, 10);
1559
+ }
1560
+
1561
+ /** The folder a conversation ran in, inside the workspace's when it is. */
1562
+ function convFolder(c) {
1563
+ const root = sd.bring?.root;
1564
+ if (root && c.cwd === root) return ".";
1565
+ if (root && c.cwd.startsWith(`${root}/`)) return c.cwd.slice(root.length + 1);
1566
+ return c.cwd;
1567
+ }
1568
+
1569
+ function convRowHtml(c) {
1570
+ const taken = c.agent
1571
+ ? ` <span class="tagx">${esc(t("ag.bring.agent", { id: c.agent }))}</span>`
1572
+ : c.open
1573
+ ? ` <span class="tagx warn" title="${esc(t("ag.bring.openTip"))}">${esc(t("ag.bring.open"))}</span>`
1574
+ : "";
1575
+ const off = c.agent || c.open;
1576
+ return (
1577
+ `<button type="button" class="pr" role="radio" data-cid="${esc(c.id)}" aria-checked="${c.id === sd.bring.pick}"${off ? ' aria-disabled="true"' : ""}>` +
1578
+ `<span class="rd" aria-hidden="true"></span><span class="pn pnc"><span class="pt">${esc(c.title || t("ag.bring.untitled"))}</span>${taken}</span><span class="mono">${esc(agoText(c.lastAt))}</span>` +
1579
+ `<span class="pd mono">${esc(convFolder(c))} · ${esc(c.id.slice(0, 8))}</span></button>`
1580
+ );
1581
+ }
1582
+
1583
+ /** Bring in's conversation list and its help line. */
1584
+ function paintConversations() {
1585
+ const f = $d("#ag-conv-f");
1586
+ f.hidden = !sd.bring;
1587
+ if (!sd.bring) return;
1588
+ const list = sd.bring.list;
1589
+ const cant = (sd.bring.runtimes ?? [])
1590
+ .filter((r) => !r.bringIn && r.id !== "generic")
1591
+ .map((r) => r.why);
1592
+ $d("#ag-conv-h").textContent = [
1593
+ list.length ? t("ag.bring.help") : t("ag.bring.none"),
1594
+ // Each its own sentence (they read as one run-on line before).
1595
+ ...cant.map((w) => (/[.!?]$/.test(w) ? w : `${w}.`)),
1596
+ ].join(" ");
1597
+ $d("#ag-conv").innerHTML = list.map(convRowHtml).join("");
1598
+ $d("#ag-conv").hidden = !list.length;
1599
+ }
1600
+
1601
+ /** Ask for the workspace's conversations (Bring in). */
1602
+ async function fillConversations(ws) {
1603
+ if (!sd.bring) return true;
1604
+ try {
1605
+ const r = await api(
1606
+ "GET",
1607
+ `/api/workspaces/${encodeURIComponent(ws)}/conversations`,
1608
+ );
1609
+ sd.bring.list = r?.conversations ?? [];
1610
+ sd.bring.runtimes = r?.runtimes ?? [];
1611
+ sd.bring.root = r?.folder ?? "";
1612
+ } catch (err) {
1613
+ window.denShell?.errorToast?.(err);
1614
+ return false;
1615
+ }
1616
+ if (!sd.bring.list.some((c) => c.id === sd.bring.pick))
1617
+ sd.bring.pick = sd.bring.list.find((c) => !c.open && !c.agent)?.id ?? "";
1618
+ paintConversations();
1619
+ return true;
1620
+ }
1621
+
1622
+ function pickConversation(id) {
1623
+ const c = (sd.bring?.list ?? []).find((x) => x.id === id);
1624
+ if (!c || c.open || c.agent) return;
1625
+ sd.bring.pick = id;
1626
+ for (const b of st.root.querySelectorAll("#ag-conv [data-cid]"))
1627
+ b.setAttribute("aria-checked", String(b.dataset.cid === id));
1628
+ paintRuntime(false);
1629
+ }
1630
+
1540
1631
  const $d = (sel) => st.root.querySelector(sel);
1541
1632
  const curPersona = () =>
1542
1633
  sd.pid === PLAIN
@@ -1563,10 +1654,13 @@ function personaModel() {
1563
1654
  function plistHtml() {
1564
1655
  const ps = sd.data?.personas ?? [];
1565
1656
  const cats = sd.data?.categories ?? [];
1657
+ const rows = personaListHtml(sd.f, ps, cats, (p) =>
1658
+ personaRowHtml(p, p.id === sd.pid, sd.data),
1659
+ );
1660
+ // Bring in continues a conversation: it needs a persona.
1661
+ if (sd.bring) return rows;
1566
1662
  return (
1567
- personaListHtml(sd.f, ps, cats, (p) =>
1568
- personaRowHtml(p, p.id === sd.pid, sd.data),
1569
- ) +
1663
+ rows +
1570
1664
  `<div class="pgh"><span>${esc(t("ag.session"))}</span><span></span></div>` +
1571
1665
  `<button type="button" class="pr" role="radio" data-pid="${esc(PLAIN)}" aria-checked="${sd.pid === PLAIN}"><span class="rd" aria-hidden="true"></span><span class="pn">${esc(t("ag.new.plain"))}</span><span></span><span class="pd">${esc(t("ag.new.plainP"))}</span></button>`
1572
1666
  );
@@ -1612,9 +1706,21 @@ function paintRuntime(reset) {
1612
1706
  const go = $d("#ag-go");
1613
1707
  const ready = $d("#ag-r-ready");
1614
1708
  const plain = sd.pid === PLAIN;
1615
- $d("#ag-r-l").hidden = plain;
1616
- $d("#ag-task-l").hidden = plain;
1617
- $d("#ag-adv").hidden = plain;
1709
+ // Bring in: the conversation's runtime, its own history; no first
1710
+ // instruction, no Advanced (it keeps what the persona gives).
1711
+ $d("#ag-r-l").hidden = plain || !!sd.bring;
1712
+ $d("#ag-task-l").hidden = plain || !!sd.bring;
1713
+ $d("#ag-adv").hidden = plain || !!sd.bring;
1714
+ $d("#ag-name-l").hidden = plain || !!sd.bring;
1715
+ paintStartsAs();
1716
+ if (sd.bring) {
1717
+ $d("#ag-r-miss").hidden = true;
1718
+ ready.hidden = true;
1719
+ const ok = !!bringPick() && !!curPersona();
1720
+ go.disabled = !ok;
1721
+ go.dataset.blocked = ok ? "" : "conversation";
1722
+ return;
1723
+ }
1618
1724
  const fb = $d("#ag-r-fb");
1619
1725
  fb.hidden = true;
1620
1726
  if (plain) {
@@ -1675,7 +1781,10 @@ function paintAdvanced() {
1675
1781
  const models = rtv?.models ?? [];
1676
1782
  const model = sd.model || def.model;
1677
1783
  const first = t("ag.new.modelDef", {
1678
- m: `${def.model || t("pe.rtDefault")} · ${versionLabel(def.model ? def.version : "latest")}`,
1784
+ // No model of its own: just the version, so the label fits the select.
1785
+ m: def.model
1786
+ ? `${def.model} · ${versionLabel(def.version)}`
1787
+ : versionLabel("latest"),
1679
1788
  });
1680
1789
  const verFirst = t("ag.new.verDef", {
1681
1790
  v: versionLabel(
@@ -1749,6 +1858,8 @@ async function fillForWorkspace(ws) {
1749
1858
  window.denShell?.errorToast?.(err);
1750
1859
  return false;
1751
1860
  }
1861
+ if (!(await fillConversations(ws))) return false;
1862
+ if (sd.bring && sd.pid === PLAIN) sd.pid = "";
1752
1863
  const ps = sd.data?.personas ?? [];
1753
1864
  if (sd.pid !== PLAIN && !ps.some((p) => p.id === sd.pid)) {
1754
1865
  // The first persona the list shows (grouped by category).
@@ -1765,9 +1876,19 @@ async function fillForWorkspace(ws) {
1765
1876
  return true;
1766
1877
  }
1767
1878
 
1768
- async function openStart() {
1879
+ /** The dialog's words for Start, or for Bring in (AG-17.1). */
1880
+ function dialogMode(bring) {
1881
+ sd.bring = bring ? { list: [], runtimes: [], root: "", pick: "" } : null;
1882
+ $d("#ag-dlg-h").textContent = t(bring ? "ag.bring.title" : "ag.new.title");
1883
+ $d("#ag-note").textContent = t(bring ? "ag.bring.note" : "ag.new.note");
1884
+ $d("#ag-go").textContent = t(bring ? "ag.bring.go" : "ag.go");
1885
+ $d("#ag-conv-f").hidden = !bring;
1886
+ }
1887
+
1888
+ async function openStart(bring = false) {
1769
1889
  const dlg = $d("#ag-dlg");
1770
1890
  if (!dlg || dlg.open) return;
1891
+ dialogMode(bring);
1771
1892
  const shown = viewWorkspaces();
1772
1893
  const wsSel = $d("#ag-ws");
1773
1894
  wsSel.innerHTML = shown
@@ -1812,21 +1933,48 @@ function startBody() {
1812
1933
  }
1813
1934
  const task = $d("#ag-task").value.trim();
1814
1935
  if (task) body.instruction = task;
1936
+ const name = $d("#ag-name").value.trim();
1937
+ if (name) body.name = name;
1815
1938
  return body;
1816
1939
  }
1817
1940
 
1818
1941
  function showStartError(err) {
1819
1942
  const el = $d("#ag-err");
1820
1943
  el.textContent = err?.message || String(err);
1944
+ // AG-10.10: that agent is running: offer to open it.
1945
+ const running = err?.data?.running;
1946
+ if (running?.sessionId) {
1947
+ const b = document.createElement("button");
1948
+ b.type = "button";
1949
+ b.className = "den-btn sm";
1950
+ b.id = "ag-open-running";
1951
+ b.textContent = t("ag.new.openRunning");
1952
+ b.addEventListener("click", () => {
1953
+ $d("#ag-dlg").close?.();
1954
+ void openPane(running.sessionId);
1955
+ });
1956
+ el.append(" ", b);
1957
+ }
1821
1958
  el.hidden = false;
1822
1959
  }
1823
1960
 
1961
+ /** AG-10.10: the id this start takes, so two of one persona are told apart. */
1962
+ function paintStartsAs() {
1963
+ const el = $d("#ag-as");
1964
+ if (!el) return;
1965
+ const p = sd.pid && sd.pid !== PLAIN && !sd.bring ? curPersona() : null;
1966
+ el.hidden = !p;
1967
+ if (p)
1968
+ el.textContent = t("ag.new.as", { id: sd.data?.startsAs?.[p.id] ?? p.id });
1969
+ }
1970
+
1824
1971
  async function submitStart(form) {
1825
1972
  const dlg = $d("#ag-dlg");
1826
1973
  const go = $d("#ag-go");
1827
1974
  if (go.disabled) return;
1828
1975
  const ws = form.querySelector("#ag-ws").value || viewWorkspaces()[0];
1829
1976
  if (!ws || !sd.pid) return;
1977
+ if (sd.bring) return submitBringIn(ws);
1830
1978
  const bad = sd.extras.find((c) => commandProblem(c));
1831
1979
  if (sd.pid !== PLAIN && bad)
1832
1980
  return showStartError(new Error(t("pe.badCmd", { c: bad })));
@@ -1852,6 +2000,9 @@ async function submitStart(form) {
1852
2000
  startBody(),
1853
2001
  );
1854
2002
  window.denShell?.toast?.(t("ag.started", { id: r?.agent?.id ?? sd.pid }));
2003
+ // AG-10.10: the next start of this persona takes another id.
2004
+ personasAsked.delete(ws);
2005
+ $d("#ag-name").value = "";
1855
2006
  for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
1856
2007
  dlg.close?.();
1857
2008
  await refresh();
@@ -1865,6 +2016,36 @@ async function submitStart(form) {
1865
2016
  }
1866
2017
  }
1867
2018
 
2019
+ /** Bring in (AG-17.1): the runtime's resume of the conversation picked. */
2020
+ async function submitBringIn(ws) {
2021
+ const dlg = $d("#ag-dlg");
2022
+ const go = $d("#ag-go");
2023
+ const c = bringPick();
2024
+ const p = curPersona();
2025
+ if (!c || !p) return;
2026
+ go.disabled = true;
2027
+ go.textContent = t("ag.bring.going");
2028
+ try {
2029
+ const r = await api(
2030
+ "POST",
2031
+ `/api/workspaces/${encodeURIComponent(ws)}/conversations/${encodeURIComponent(c.id)}/import`,
2032
+ { persona: p.id, runtime: c.runtime },
2033
+ );
2034
+ window.denShell?.toast?.(t("ag.bring.done", { id: r?.agent?.id ?? p.id }));
2035
+ for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
2036
+ dlg.close?.();
2037
+ await refresh();
2038
+ if (r?.agent?.sessionId) await openPane(r.agent.sessionId);
2039
+ } catch (err) {
2040
+ showStartError(err);
2041
+ // Still open elsewhere, or taken since: the list says so now.
2042
+ await fillConversations(ws);
2043
+ } finally {
2044
+ go.textContent = t("ag.bring.go");
2045
+ if (dlg.open) paintRuntime(false);
2046
+ }
2047
+ }
2048
+
1868
2049
  /** New persona, Edit or Duplicate from the dialog; back to it after. */
1869
2050
  async function editFromStart(kind) {
1870
2051
  const p = curPersona();
@@ -1892,6 +2073,12 @@ async function editFromStart(kind) {
1892
2073
  function wireStartDialog(root) {
1893
2074
  const form = root.querySelector("#ag-form");
1894
2075
  form.addEventListener("click", (e) => {
2076
+ const cr = e.target.closest("#ag-conv [data-cid]");
2077
+ if (cr) {
2078
+ e.preventDefault();
2079
+ pickConversation(cr.dataset.cid);
2080
+ return;
2081
+ }
1895
2082
  const pr = e.target.closest("#ag-persona [data-pid]");
1896
2083
  if (pr) {
1897
2084
  e.preventDefault();
@@ -2032,6 +2219,8 @@ function translateStatic(root) {
2032
2219
  };
2033
2220
  set("#ag-h", "n.agents");
2034
2221
  set("#ag-start", "ag.start");
2222
+ set("#ag-bring", "ag.bring");
2223
+ set("#ag-conv-t", "ag.bring.conv");
2035
2224
  set("#ag-clear", "ag.clear");
2036
2225
  set("#ag-dlg-h", "ag.new.title");
2037
2226
  set("#ag-ws-t", "ag.ws");
@@ -2041,6 +2230,7 @@ function translateStatic(root) {
2041
2230
  set("#ag-newp", "ag.new.newP");
2042
2231
  set("#ag-what-p", "pe.what");
2043
2232
  set("#ag-task-t", "ag.new.task");
2233
+ set("#ag-name-t", "ag.new.name");
2044
2234
  set("#ag-adv-s", "ag.new.adv");
2045
2235
  set("#ag-note", "ag.new.note");
2046
2236
  const task = root.querySelector("#ag-task");
@@ -2129,6 +2319,9 @@ export async function mountAgentsPage(root) {
2129
2319
  root.querySelector("#ag-start").addEventListener("click", () => {
2130
2320
  void openStart();
2131
2321
  });
2322
+ root.querySelector("#ag-bring")?.addEventListener("click", () => {
2323
+ void openStart(true);
2324
+ });
2132
2325
  root.querySelector("#ag-clear")?.addEventListener("click", (e) => {
2133
2326
  void clearStopped(e.currentTarget);
2134
2327
  });
@@ -642,6 +642,7 @@
642
642
  <button type="button" class="den-btn" id="ag-clear" hidden>
643
643
  Clear stopped
644
644
  </button>
645
+ <button type="button" class="den-btn" id="ag-bring">Bring in</button>
645
646
  <button type="button" class="den-btn primary" id="ag-start">
646
647
  + Start agent
647
648
  </button>
@@ -660,6 +661,18 @@
660
661
  <dialog class="ag-dlg pe-dlg" id="ag-dlg" aria-labelledby="ag-dlg-h">
661
662
  <form method="dialog" id="ag-form" novalidate>
662
663
  <h2 id="ag-dlg-h">Start an agent</h2>
664
+ <div class="fld" id="ag-conv-f" hidden>
665
+ <div class="fldh">
666
+ <span class="k" id="ag-conv-t">Conversation</span>
667
+ </div>
668
+ <p class="helpl" id="ag-conv-h"></p>
669
+ <div
670
+ class="plist"
671
+ id="ag-conv"
672
+ role="radiogroup"
673
+ aria-labelledby="ag-conv-t"
674
+ ></div>
675
+ </div>
663
676
  <div class="fld" id="ag-pfld">
664
677
  <div class="fldh">
665
678
  <span class="k" id="ag-p-t">Persona</span
@@ -712,6 +725,11 @@
712
725
  ><span id="ag-task-t">First instruction (optional)</span>
713
726
  <textarea id="ag-task" rows="4" maxlength="8000"></textarea>
714
727
  </label>
728
+ <label class="fld" id="ag-name-l"
729
+ ><span id="ag-name-t">Name (optional)</span>
730
+ <input id="ag-name" maxlength="40" autocomplete="off" />
731
+ </label>
732
+ <p class="helpl" id="ag-as" role="status"></p>
715
733
  <details id="ag-adv">
716
734
  <summary id="ag-adv-s">Advanced: model and permissions</summary>
717
735
  <div class="adv-body" id="ag-adv-b"></div>
@@ -456,6 +456,9 @@
456
456
  "ag.new.plainP":
457
457
  "Una terminal en la carpeta del espacio de trabajo. Sin agente.",
458
458
  "ag.new.task": "Primera instrucción (opcional)",
459
+ "ag.new.name": "Nombre (opcional)",
460
+ "ag.new.as": "Se inicia como {id}",
461
+ "ag.new.openRunning": "Abrir el que está en ejecución",
459
462
  "ag.new.taskPh":
460
463
  "p. ej., Toma la historia del estado vacío. Sigue el diseño y añade una prueba.",
461
464
  "ag.new.adv": "Avanzado: modelo y permisos",
@@ -465,6 +468,26 @@
465
468
  "ag.new.modelDef": "Predeterminado de la persona ({m})",
466
469
  "ag.new.ver": "Versión",
467
470
  "ag.new.verDef": "Predeterminado de la persona ({v})",
471
+ // AG-17.1, Bring in.
472
+ "ag.bring": "Traer",
473
+ "ag.bring.title": "Traer una conversación",
474
+ "ag.bring.conv": "Conversación",
475
+ "ag.bring.help":
476
+ "Conversaciones iniciadas fuera de Den en la carpeta de este espacio de trabajo o en las de sus repositorios, la más reciente primero.",
477
+ "ag.bring.none":
478
+ "No hay conversaciones iniciadas fuera de Den en la carpeta de este espacio de trabajo ni en las de sus repositorios.",
479
+ "ag.bring.untitled": "Conversación sin título",
480
+ "ag.bring.open": "Abierta en otro lugar",
481
+ "ag.bring.openTip": "Sigue abierta en otra terminal. Ciérrala allí primero",
482
+ "ag.bring.agent": "Agente {id}",
483
+ "ag.bring.note":
484
+ "La continúa en la carpeta donde se ejecutó, con las herramientas y protecciones de Den desde ahora. Sigue en ejecución si cierras el panel. Ciérrala antes en cualquier otro terminal: Den solo la ve abierta si se escribió en el último minuto.",
485
+ "ag.bring.go": "Traer",
486
+ "ag.bring.going": "Trayendo…",
487
+ "ag.bring.done": "Traída como {id}",
488
+ "ag.bring.now": "ahora mismo",
489
+ "ag.bring.min": "hace {n} min",
490
+ "ag.bring.h": "hace {n} h",
468
491
  "ag.new.note":
469
492
  "Se inicia en la carpeta del espacio de trabajo. Sigue en ejecución si cierras el panel.",
470
493
  "ag.new.started": "{p} iniciado en {w}",