@monster-orange/den 0.4.12 → 0.4.13

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 (40) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +146 -10
  4. package/package.json +1 -1
  5. package/src/shared/agents/default-pack/be-dev.md +1 -0
  6. package/src/shared/agents/default-pack/devops.md +1 -0
  7. package/src/shared/agents/default-pack/fe-dev.md +1 -0
  8. package/src/shared/agents/default-pack/orchestrator.md +3 -1
  9. package/src/shared/agents/default-pack/planner.md +1 -0
  10. package/src/shared/agents/default-pack/reviewer.md +1 -0
  11. package/src/shared/agents/default-pack/tester.md +16 -0
  12. package/src/shared/agents/default-pack/ux-designer.md +1 -0
  13. package/src/shared/agents/den-claude.ts +119 -13
  14. package/src/shared/agents/permissions.ts +108 -0
  15. package/src/shared/agents/persona-store.ts +286 -0
  16. package/src/shared/agents/personas.ts +116 -12
  17. package/src/shared/agents/records.ts +28 -0
  18. package/src/shared/agents/roles.ts +288 -0
  19. package/src/shared/agents/runtime.ts +41 -0
  20. package/src/shared/den-config.ts +20 -1
  21. package/src/ui/cli/agent-ops.ts +535 -17
  22. package/src/ui/cli/agents-command.ts +12 -2
  23. package/src/ui/cli/env-probe.ts +4 -1
  24. package/src/ui/mcp/tools.ts +1 -1
  25. package/src/ui/server/agent-routes.ts +260 -21
  26. package/src/ui/server/public/agent-start.js +3 -2
  27. package/src/ui/server/public/agents-page.js +499 -48
  28. package/src/ui/server/public/agents.html +58 -15
  29. package/src/ui/server/public/i18n/es.js +120 -0
  30. package/src/ui/server/public/i18n/ja.js +120 -0
  31. package/src/ui/server/public/persona-ui.css +539 -0
  32. package/src/ui/server/public/persona-ui.js +796 -0
  33. package/src/ui/server/public/settings-page.js +4 -9
  34. package/src/ui/server/public/settings-registry.js +22 -0
  35. package/src/ui/server/public/settings.html +1 -0
  36. package/src/ui/server/public/shell.js +117 -0
  37. package/src/ui/server/public/topbar.js +2 -2
  38. package/src/ui/server/route-classes.ts +18 -0
  39. package/src/ui/server/session-routes.ts +21 -5
  40. package/src/ui/server/ui-assets.ts +3 -0
@@ -27,8 +27,17 @@
27
27
  * Nothing here keeps a session's state: the list is GET /api/sessions,
28
28
  * asked again every few seconds while the page is shown.
29
29
  *
30
- * Start agent (AG-0.2): the runtime line says what will run, or why it
31
- * can't, before Start (agent-start.js readiness). Starting, stopping and
30
+ * Start agent (AG-10.1): a searchable persona list (category and source
31
+ * chips, grouped by category, Plain session pinned last), What's a
32
+ * persona?, Details, + New persona, Duplicate or Edit (persona-ui.js),
33
+ * Runtime and Workspace, a first instruction (typed into the session once
34
+ * it is ready, by the server: never an argument), and Advanced: model and
35
+ * version (AG-12), permission mode and extra allowed commands with the
36
+ * preview of what the runtime is given (AG-13). What differs from the
37
+ * persona tags the session's row (AG-10.9). The runtime line says what
38
+ * will run, or why it can't, before Start (agent-start.js readiness); a
39
+ * persona whose runtime isn't installed starts on the first one that is,
40
+ * and the dialog says so (AG-12.2). Starting, stopping and
32
41
  * resuming an agent never ask for the admin password: it is for sudo, and
33
42
  * no agent action runs sudo (LK-0).
34
43
  *
@@ -74,7 +83,27 @@
74
83
  * and Start fresh. A running one says "Handoff due" or "Ready to relaunch"
75
84
  * as a tag.
76
85
  */
77
- import { fillStartForm } from "/agent-start.js";
86
+ import { readiness, runtimeName, runtimeOptions } from "/agent-start.js";
87
+ import {
88
+ commandProblem,
89
+ filterBarHtml,
90
+ modelOptionsHtml,
91
+ normalizeData,
92
+ openPersonaEditor,
93
+ overrideTagsHtml,
94
+ permBlockHtml,
95
+ personaDetail,
96
+ personaDetailHtml,
97
+ personaListHtml,
98
+ personaName,
99
+ personaRowHtml,
100
+ personaRuntime,
101
+ runtimeInstalled,
102
+ splitCommands,
103
+ syncPermBlock,
104
+ versionLabel,
105
+ versionOptionsHtml,
106
+ } from "/persona-ui.js";
78
107
  import {
79
108
  editorUrl,
80
109
  findPaths,
@@ -240,7 +269,7 @@ function nameHtml(r) {
240
269
  * persona, not the name already shown. Else "".
241
270
  */
242
271
  function sessionExtra(r) {
243
- const n = r?.kind === "agent" ? (r.sessionName ?? "").trim() : "";
272
+ const n = r?.kind === "agent" ? (r.sessionTitle ?? "").trim() : "";
244
273
  if (!n || n === r.persona || n === paneName(r) || n === displayName(r))
245
274
  return "";
246
275
  return n;
@@ -377,9 +406,10 @@ function rowHtml(r) {
377
406
  const revokedTag = r.tokenRevoked
378
407
  ? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
379
408
  : "";
409
+ const ovTags = r.kind === "agent" ? overrideTagsHtml(r) : "";
380
410
  const tags =
381
- rotTag || revokedTag
382
- ? `<span class="agtags">${revokedTag}${rotTag}</span>`
411
+ rotTag || revokedTag || ovTags
412
+ ? `<span class="agtags">${revokedTag}${rotTag}${ovTags}</span>`
383
413
  : "";
384
414
  return (
385
415
  `<button type="button" class="agr" data-id="${esc(r.id)}" data-kind="${esc(r.kind)}" data-state="${esc(r.state)}"` +
@@ -883,11 +913,26 @@ function refit(p) {
883
913
  markSize(p);
884
914
  }
885
915
 
886
- /** The pane's size and the PTY's, on the pane (data-cols, data-pty-cols). */
916
+ /**
917
+ * The pane's size and the PTY's, on the pane (data-cols, data-pty-cols),
918
+ * and the cell size xterm drew with (data-cell-w, data-cell-h: its
919
+ * renderer's CSS cell, sub-pixel), so a check can tell a missed row from
920
+ * a fit's remainder.
921
+ */
887
922
  function markSize(p) {
888
923
  if (!p.term) return;
889
924
  p.el.dataset.cols = String(p.term.cols);
890
925
  p.el.dataset.rows = String(p.term.rows);
926
+ let cell = null;
927
+ try {
928
+ cell = p.term._core?._renderService?.dimensions?.css?.cell ?? null;
929
+ } catch {
930
+ cell = null;
931
+ }
932
+ if (cell?.width > 0 && cell?.height > 0) {
933
+ p.el.dataset.cellW = String(cell.width);
934
+ p.el.dataset.cellH = String(cell.height);
935
+ }
891
936
  if (p.pty) {
892
937
  p.el.dataset.ptyCols = String(p.pty.cols);
893
938
  p.el.dataset.ptyRows = String(p.pty.rows);
@@ -1448,7 +1493,7 @@ function review() {
1448
1493
  )?.focus();
1449
1494
  }
1450
1495
 
1451
- // ── Start agent ─────────────────────────────────────────────────────────
1496
+ // ── Start agent (AG-10.1) ───────────────────────────────────────────────
1452
1497
 
1453
1498
  // Personas and runtimes, per workspace (AG-0.5: a workspace's own runtime
1454
1499
  // paths win), asked once per page each.
@@ -1470,79 +1515,478 @@ function personasFor(ws) {
1470
1515
  return personasAsked.get(key);
1471
1516
  }
1472
1517
 
1473
- /** Fill the form for the workspace picked; false when the ask failed. */
1518
+ /**
1519
+ * "Plain session": no persona, a shell in the workspace (AG-10.1). A ":"
1520
+ * is never in a persona's id.
1521
+ */
1522
+ const PLAIN = ":plain";
1523
+
1524
+ /** The dialog's state while it is open. */
1525
+ const sd = {
1526
+ data: null,
1527
+ f: { q: "", cat: "", src: "" },
1528
+ pid: "",
1529
+ det: false,
1530
+ what: false,
1531
+ detail: null,
1532
+ // Advanced: "" = the persona's.
1533
+ model: "",
1534
+ ver: "",
1535
+ intent: "ask",
1536
+ extras: [],
1537
+ extrasOpen: false,
1538
+ };
1539
+
1540
+ const $d = (sel) => st.root.querySelector(sel);
1541
+ const curPersona = () =>
1542
+ sd.pid === PLAIN
1543
+ ? null
1544
+ : (sd.data?.personas ?? []).find((p) => p.id === sd.pid) || null;
1545
+
1546
+ /** The runtime the dialog will start: the one picked, else the persona's. */
1547
+ function chosenRuntime() {
1548
+ const p = curPersona();
1549
+ return $d("#ag-runtime").value || personaRuntime(sd.data, p);
1550
+ }
1551
+
1552
+ /** The persona's own model and version, on the runtime chosen (AG-12.3). */
1553
+ function personaModel() {
1554
+ const p = curPersona();
1555
+ const same = p && personaRuntime(sd.data, p) === chosenRuntime();
1556
+ return {
1557
+ model: same ? p.model || "" : "",
1558
+ version: same ? p.version || "latest" : "latest",
1559
+ };
1560
+ }
1561
+
1562
+ /** The persona list's rows, and Plain session pinned at the bottom. */
1563
+ function plistHtml() {
1564
+ const ps = sd.data?.personas ?? [];
1565
+ const cats = sd.data?.categories ?? [];
1566
+ return (
1567
+ personaListHtml(sd.f, ps, cats, (p) =>
1568
+ personaRowHtml(p, p.id === sd.pid, sd.data),
1569
+ ) +
1570
+ `<div class="pgh"><span>${esc(t("ag.session"))}</span><span></span></div>` +
1571
+ `<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
+ );
1573
+ }
1574
+
1575
+ function paintPlist(keepScroll = true) {
1576
+ const l = $d("#ag-persona");
1577
+ const top = l.scrollTop;
1578
+ l.innerHTML = plistHtml();
1579
+ l.scrollTop = keepScroll ? top : 0;
1580
+ }
1581
+
1582
+ /** The Details row (and Duplicate or Edit) under the list, and Details. */
1583
+ function paintDetails() {
1584
+ const p = curPersona();
1585
+ const el = $d("#ag-detrow");
1586
+ if (!p) {
1587
+ el.innerHTML = "";
1588
+ return;
1589
+ }
1590
+ const mine = p.origin === "local";
1591
+ el.innerHTML =
1592
+ `<div class="fldh"><button type="button" class="lnk" data-x="det" aria-expanded="${sd.det}" aria-controls="ag-det">${esc(sd.det ? t("ag.new.hide") : t("ag.new.details", { n: personaName(p) }))}</button><span class="sp"></span>` +
1593
+ (mine
1594
+ ? `<button type="button" class="lnk" data-x="edit">${esc(t("pe.editB"))}</button>`
1595
+ : "") +
1596
+ `<button type="button" class="lnk" data-x="dup">${esc(t("pe.dup"))}</button></div>` +
1597
+ (sd.det
1598
+ ? personaDetailHtml(
1599
+ sd.detail?.id === p.id ? sd.detail : { ...p, instructions: null },
1600
+ sd.data,
1601
+ )
1602
+ : "");
1603
+ }
1604
+
1605
+ /**
1606
+ * The runtime select and its readiness line (AG-0.2), and AG-12.2: a
1607
+ * persona whose runtime isn't installed starts on the first one that is,
1608
+ * and the dialog says so.
1609
+ */
1610
+ function paintRuntime(reset) {
1611
+ const rs = $d("#ag-runtime");
1612
+ const go = $d("#ag-go");
1613
+ const ready = $d("#ag-r-ready");
1614
+ 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;
1618
+ const fb = $d("#ag-r-fb");
1619
+ fb.hidden = true;
1620
+ if (plain) {
1621
+ $d("#ag-r-miss").hidden = true;
1622
+ ready.hidden = true;
1623
+ go.disabled = false;
1624
+ go.dataset.blocked = "";
1625
+ return;
1626
+ }
1627
+ const personas = sd.data?.personas ?? [];
1628
+ const runtimes = sd.data?.runtimes ?? [];
1629
+ const keep = reset ? "" : rs.value;
1630
+ rs.innerHTML = runtimeOptions(personas, runtimes, sd.pid, t);
1631
+ if ([...rs.options].some((o) => o.value === keep && !o.disabled))
1632
+ rs.value = keep;
1633
+ const p = curPersona();
1634
+ if (reset && p && !runtimeInstalled(sd.data, p)) {
1635
+ const first = [...rs.options].find((o) => o.value && !o.disabled);
1636
+ if (first) {
1637
+ rs.value = first.value;
1638
+ fb.textContent = t("rt.fallback", {
1639
+ p: personaName(p),
1640
+ r: runtimeName(runtimes, personaRuntime(sd.data, p)),
1641
+ f: runtimeName(runtimes, first.value),
1642
+ });
1643
+ fb.hidden = false;
1644
+ }
1645
+ }
1646
+ // AG-12.1: what isn't installed here, and Install… (later).
1647
+ const miss = runtimes.filter(
1648
+ (x) => !x.available && x.denTools !== undefined && x.source !== "setting",
1649
+ );
1650
+ const ml = $d("#ag-r-miss");
1651
+ ml.innerHTML = miss.length
1652
+ ? `${esc(t("rt.missingLine"))} ${miss.map((x) => `<span class="mono">${esc(x.label || x.id)}</span>`).join(", ")} · <span class="lnk" aria-disabled="true" title="${esc(t("rt.later"))}">${esc(t("rt.install"))}</span> <span class="tagx">${esc(t("rt.later"))}</span>`
1653
+ : "";
1654
+ ml.hidden = !miss.length;
1655
+ const r = readiness(sd.data, sd.pid, rs.value, t);
1656
+ ready.innerHTML = r.html;
1657
+ ready.hidden = !r.html;
1658
+ ready.dataset.tone = r.tone;
1659
+ go.disabled = !r.ok;
1660
+ go.dataset.blocked = r.ok ? "" : "runtime";
1661
+ }
1662
+
1663
+ /** Advanced (AG-12, AG-13): model, version and the permission block. */
1664
+ function paintAdvanced() {
1665
+ const p = curPersona();
1666
+ const body = $d("#ag-adv-b");
1667
+ if (!p) {
1668
+ body.innerHTML = "";
1669
+ return;
1670
+ }
1671
+ const rt = chosenRuntime();
1672
+ const rtv = (sd.data?.runtimes ?? []).find((r) => r.id === rt);
1673
+ const name = runtimeName(sd.data?.runtimes ?? [], rt);
1674
+ const def = personaModel();
1675
+ const models = rtv?.models ?? [];
1676
+ const model = sd.model || def.model;
1677
+ const first = t("ag.new.modelDef", {
1678
+ m: `${def.model || t("pe.rtDefault")} · ${versionLabel(def.model ? def.version : "latest")}`,
1679
+ });
1680
+ const verFirst = t("ag.new.verDef", {
1681
+ v: versionLabel(
1682
+ sd.model && sd.model !== def.model ? "latest" : def.version,
1683
+ ),
1684
+ });
1685
+ body.innerHTML =
1686
+ `<p>${esc(t("ag.new.advNote"))}</p>` +
1687
+ `<div class="two"><label class="fld"><span>${esc(t("ag.new.model"))}</span><select id="ag-model"${models.length ? "" : " disabled"}>${models.length ? modelOptionsHtml(sd.data, rt, sd.model, first) : `<option value="">${esc(t("pe.nsModel", { r: name }))}</option>`}</select></label>` +
1688
+ `<label class="fld"><span>${esc(t("ag.new.ver"))}</span><select id="ag-ver"${models.length && model ? "" : " disabled"}>${versionOptionsHtml(sd.data, rt, model, sd.ver, verFirst)}</select></label></div>` +
1689
+ permBlockHtml("ag", sd.data, {
1690
+ intent: sd.intent,
1691
+ role: p.role,
1692
+ extras: sd.extras,
1693
+ runtime: rt,
1694
+ extrasOpen: sd.extrasOpen,
1695
+ disabled: !rtv?.permissions,
1696
+ });
1697
+ $d("#ag-tools").disabled = false;
1698
+ }
1699
+
1700
+ /** Pick persona `id` (or PLAIN): its own choices fill Advanced in. */
1701
+ function pickPersona(id) {
1702
+ sd.pid = id;
1703
+ sd.det = sd.det && id !== PLAIN;
1704
+ const p = curPersona();
1705
+ sd.model = "";
1706
+ sd.ver = "";
1707
+ sd.intent = p?.permission || "ask";
1708
+ sd.extras = [...(p?.allowedCommands ?? [])];
1709
+ sd.extrasOpen = sd.extras.length > 0;
1710
+ for (const b of st.root.querySelectorAll("#ag-persona [data-pid]"))
1711
+ b.setAttribute("aria-checked", String(b.dataset.pid === id));
1712
+ paintDetails();
1713
+ paintRuntime(true);
1714
+ paintAdvanced();
1715
+ if (sd.det) void loadDetail();
1716
+ }
1717
+
1718
+ async function loadDetail() {
1719
+ const p = curPersona();
1720
+ if (!p) return;
1721
+ try {
1722
+ const d = await personaDetail(p.id);
1723
+ if (curPersona()?.id !== d.id) return;
1724
+ sd.detail = d;
1725
+ paintDetails();
1726
+ } catch (err) {
1727
+ window.denShell?.errorToast?.(err);
1728
+ }
1729
+ }
1730
+
1731
+ /** The whole persona field, for the data asked. */
1732
+ function paintPersonaField() {
1733
+ const ps = sd.data?.personas ?? [];
1734
+ $d("#ag-pfbar").innerHTML = filterBarHtml(
1735
+ sd.f,
1736
+ "ag",
1737
+ ps,
1738
+ sd.data?.categories ?? [],
1739
+ );
1740
+ $d("#ag-what").hidden = !sd.what;
1741
+ paintPlist(false);
1742
+ }
1743
+
1744
+ /** Ask for the workspace's personas and runtimes; false when it failed. */
1474
1745
  async function fillForWorkspace(ws) {
1475
- let data;
1476
1746
  try {
1477
- data = await personasFor(ws);
1747
+ sd.data = normalizeData(await personasFor(ws));
1478
1748
  } catch (err) {
1479
1749
  window.denShell?.errorToast?.(err);
1480
1750
  return false;
1481
1751
  }
1482
- const form = st.root.querySelector("#ag-form");
1483
- const keep = [
1484
- form.querySelector("#ag-persona").value,
1485
- form.querySelector("#ag-runtime").value,
1486
- ];
1487
- fillStartForm(form, data, t);
1488
- // The persona and runtime picked stay, where the new list still has them.
1489
- const ps = form.querySelector("#ag-persona");
1490
- if (keep[0] && [...ps.options].some((o) => o.value === keep[0])) {
1491
- ps.value = keep[0];
1492
- ps.onchange?.();
1493
- }
1494
- const rs = form.querySelector("#ag-runtime");
1495
- if ([...rs.options].some((o) => o.value === keep[1] && !o.disabled)) {
1496
- rs.value = keep[1];
1497
- rs.onchange?.();
1752
+ const ps = sd.data?.personas ?? [];
1753
+ if (sd.pid !== PLAIN && !ps.some((p) => p.id === sd.pid)) {
1754
+ // The first persona the list shows (grouped by category).
1755
+ const cats = (sd.data?.categories ?? []).map((c) => c.id);
1756
+ const sorted = [...ps].sort(
1757
+ (a, b) =>
1758
+ (cats.indexOf(a.category) + 1 || 99) -
1759
+ (cats.indexOf(b.category) + 1 || 99),
1760
+ );
1761
+ sd.pid = sorted[0]?.id ?? PLAIN;
1498
1762
  }
1763
+ paintPersonaField();
1764
+ pickPersona(sd.pid);
1499
1765
  return true;
1500
1766
  }
1501
1767
 
1502
1768
  async function openStart() {
1503
- const dlg = st.root.querySelector("#ag-dlg");
1769
+ const dlg = $d("#ag-dlg");
1504
1770
  if (!dlg || dlg.open) return;
1505
1771
  const shown = viewWorkspaces();
1506
- const wsSel = st.root.querySelector("#ag-ws");
1772
+ const wsSel = $d("#ag-ws");
1507
1773
  wsSel.innerHTML = shown
1508
1774
  .map((w) => `<option value="${esc(w)}">${esc(w)}</option>`)
1509
1775
  .join("");
1510
- st.root.querySelector("#ag-ws-l").hidden = shown.length <= 1;
1776
+ wsSel.disabled = shown.length <= 1;
1777
+ sd.f = { q: "", cat: "", src: "" };
1778
+ sd.det = false;
1779
+ sd.what = false;
1780
+ $d("#ag-err").hidden = true;
1781
+ $d("#ag-task").value = "";
1782
+ $d("#ag-adv").open = false;
1511
1783
  if (!(await fillForWorkspace(wsSel.value || shown[0]))) return;
1512
1784
  if (dlg.open) return;
1513
1785
  if (typeof dlg.showModal === "function") dlg.showModal();
1514
1786
  else dlg.setAttribute("open", "");
1787
+ $d("#ag-q")?.focus();
1788
+ }
1789
+
1790
+ /** What Start sends (agent-routes.ts checks each field again). */
1791
+ function startBody() {
1792
+ const p = curPersona();
1793
+ const rt = chosenRuntime();
1794
+ const rtv = (sd.data?.runtimes ?? []).find((r) => r.id === rt);
1795
+ const runtime = $d("#ag-runtime").value;
1796
+ const body = { persona: p.id, ...(runtime ? { runtime } : {}) };
1797
+ const def = personaModel();
1798
+ if (rtv?.models?.length) {
1799
+ if (sd.model) body.model = sd.model;
1800
+ if (sd.ver) {
1801
+ body.version = sd.ver;
1802
+ body.model ??= def.model || undefined;
1803
+ if (!body.model) delete body.version;
1804
+ }
1805
+ }
1806
+ if (rtv?.permissions) {
1807
+ // A persona's own mode Den doesn't name (Claude Code's plan) stays its
1808
+ // own unless a mode is picked.
1809
+ if (p.permission !== undefined || sd.intent !== "ask")
1810
+ body.permission = sd.intent;
1811
+ body.allow = sd.extras;
1812
+ }
1813
+ const task = $d("#ag-task").value.trim();
1814
+ if (task) body.instruction = task;
1815
+ return body;
1816
+ }
1817
+
1818
+ function showStartError(err) {
1819
+ const el = $d("#ag-err");
1820
+ el.textContent = err?.message || String(err);
1821
+ el.hidden = false;
1515
1822
  }
1516
1823
 
1517
1824
  async function submitStart(form) {
1518
- const dlg = st.root.querySelector("#ag-dlg");
1519
- const go = st.root.querySelector("#ag-go");
1825
+ const dlg = $d("#ag-dlg");
1826
+ const go = $d("#ag-go");
1520
1827
  if (go.disabled) return;
1521
1828
  const ws = form.querySelector("#ag-ws").value || viewWorkspaces()[0];
1522
- const persona = form.querySelector("#ag-persona").value;
1523
- const runtime = form.querySelector("#ag-runtime").value;
1524
- if (!ws || !persona) return;
1829
+ if (!ws || !sd.pid) return;
1830
+ const bad = sd.extras.find((c) => commandProblem(c));
1831
+ if (sd.pid !== PLAIN && bad)
1832
+ return showStartError(new Error(t("pe.badCmd", { c: bad })));
1525
1833
  go.disabled = true;
1526
1834
  go.textContent = t("ag.starting");
1527
1835
  try {
1836
+ if (sd.pid === PLAIN) {
1837
+ const r = await api(
1838
+ "POST",
1839
+ `/api/workspaces/${encodeURIComponent(ws)}/sessions`,
1840
+ );
1841
+ window.denShell?.toast?.(
1842
+ t("ag.new.started", { p: t("ag.session"), w: ws }),
1843
+ );
1844
+ dlg.close?.();
1845
+ await refresh();
1846
+ if (r?.session?.id) await openPane(r.session.id);
1847
+ return;
1848
+ }
1528
1849
  const r = await api(
1529
1850
  "POST",
1530
1851
  `/api/workspaces/${encodeURIComponent(ws)}/agents`,
1531
- {
1532
- persona,
1533
- ...(runtime ? { runtime } : {}),
1534
- },
1852
+ startBody(),
1535
1853
  );
1536
- window.denShell?.toast?.(t("ag.started", { id: r?.agent?.id ?? persona }));
1854
+ window.denShell?.toast?.(t("ag.started", { id: r?.agent?.id ?? sd.pid }));
1855
+ for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
1537
1856
  dlg.close?.();
1538
1857
  await refresh();
1539
1858
  if (r?.agent?.sessionId) await openPane(r.agent.sessionId);
1540
1859
  } catch (err) {
1541
- window.denShell?.errorToast?.(err);
1860
+ showStartError(err);
1542
1861
  } finally {
1543
1862
  go.disabled = false;
1544
1863
  go.textContent = t("ag.go");
1864
+ if (dlg.open) paintRuntime(false);
1865
+ }
1866
+ }
1867
+
1868
+ /** New persona, Edit or Duplicate from the dialog; back to it after. */
1869
+ async function editFromStart(kind) {
1870
+ const p = curPersona();
1871
+ let src = null;
1872
+ if (kind !== "new" && p) {
1873
+ try {
1874
+ src = await personaDetail(p.id);
1875
+ } catch (err) {
1876
+ return void window.denShell?.errorToast?.(err);
1877
+ }
1545
1878
  }
1879
+ openPersonaEditor({
1880
+ data: sd.data,
1881
+ src,
1882
+ dup: kind === "dup",
1883
+ onSaved: async (saved) => {
1884
+ personasAsked.clear();
1885
+ if (saved) sd.pid = saved.id;
1886
+ await fillForWorkspace($d("#ag-ws").value);
1887
+ },
1888
+ });
1889
+ }
1890
+
1891
+ /** Wire the dialog once (mountAgentsPage). */
1892
+ function wireStartDialog(root) {
1893
+ const form = root.querySelector("#ag-form");
1894
+ form.addEventListener("click", (e) => {
1895
+ const pr = e.target.closest("#ag-persona [data-pid]");
1896
+ if (pr) {
1897
+ e.preventDefault();
1898
+ pickPersona(pr.dataset.pid);
1899
+ pr.focus();
1900
+ return;
1901
+ }
1902
+ const chip = e.target.closest("[data-pf]");
1903
+ if (chip) {
1904
+ e.preventDefault();
1905
+ sd.f[chip.dataset.pf] = chip.dataset.v;
1906
+ for (const c of form.querySelectorAll(`[data-pf="${chip.dataset.pf}"]`))
1907
+ c.setAttribute("aria-pressed", String(c.dataset.v === chip.dataset.v));
1908
+ return paintPlist(false);
1909
+ }
1910
+ const pm = e.target.closest("[data-pm]");
1911
+ if (pm) {
1912
+ e.preventDefault();
1913
+ if (pm.disabled) return;
1914
+ sd.intent = pm.dataset.pm;
1915
+ return syncPermBlock(form, "ag", sd.data, {
1916
+ intent: sd.intent,
1917
+ role: curPersona()?.role,
1918
+ extras: sd.extras,
1919
+ runtime: chosenRuntime(),
1920
+ });
1921
+ }
1922
+ const x = e.target.closest("[data-x]")?.dataset.x;
1923
+ if (!x) return;
1924
+ e.preventDefault();
1925
+ if (x === "what") {
1926
+ sd.what = !sd.what;
1927
+ $d("#ag-what").hidden = !sd.what;
1928
+ e.target
1929
+ .closest("[data-x]")
1930
+ .setAttribute("aria-expanded", String(sd.what));
1931
+ } else if (x === "det") {
1932
+ sd.det = !sd.det;
1933
+ paintDetails();
1934
+ if (sd.det) void loadDetail();
1935
+ } else if (x === "clear") {
1936
+ sd.f = { q: "", cat: "", src: "" };
1937
+ paintPersonaField();
1938
+ } else if (x === "raw") window.denShell?.toast?.(t("pe.rawLater"));
1939
+ else if (x === "newp") void editFromStart("new");
1940
+ else if (x === "dup") void editFromStart("dup");
1941
+ else if (x === "edit") void editFromStart("edit");
1942
+ });
1943
+ form.addEventListener("input", (e) => {
1944
+ if (e.target.id === "ag-q") {
1945
+ sd.f.q = e.target.value;
1946
+ paintPlist(false);
1947
+ } else if (e.target.id === "ag-tools") {
1948
+ sd.extras = splitCommands(e.target.value);
1949
+ syncPermBlock(form, "ag", sd.data, {
1950
+ intent: sd.intent,
1951
+ role: curPersona()?.role,
1952
+ extras: sd.extras,
1953
+ runtime: chosenRuntime(),
1954
+ });
1955
+ }
1956
+ });
1957
+ form.addEventListener(
1958
+ "toggle",
1959
+ (e) => {
1960
+ if (e.target.id === "ag-xtra") sd.extrasOpen = e.target.open;
1961
+ },
1962
+ true,
1963
+ );
1964
+ form.addEventListener("change", (e) => {
1965
+ const id = e.target.id;
1966
+ if (id === "ag-ws") void fillForWorkspace(e.target.value);
1967
+ else if (id === "ag-runtime") {
1968
+ sd.model = "";
1969
+ sd.ver = "";
1970
+ $d("#ag-r-fb").hidden = true;
1971
+ paintRuntime(false);
1972
+ paintAdvanced();
1973
+ } else if (id === "ag-model") {
1974
+ sd.model = e.target.value;
1975
+ sd.ver = "";
1976
+ paintAdvanced();
1977
+ } else if (id === "ag-ver") sd.ver = e.target.value;
1978
+ });
1979
+ // ↑ ↓ move through the persona list (a radio group).
1980
+ root.querySelector("#ag-persona").addEventListener("keydown", (e) => {
1981
+ if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
1982
+ const rows = [...e.currentTarget.querySelectorAll("[data-pid]")];
1983
+ const i = rows.indexOf(document.activeElement);
1984
+ const next = rows[i + (e.key === "ArrowDown" ? 1 : -1)];
1985
+ if (!next) return;
1986
+ e.preventDefault();
1987
+ pickPersona(next.dataset.pid);
1988
+ next.focus();
1989
+ });
1546
1990
  }
1547
1991
 
1548
1992
  /**
@@ -1589,10 +2033,21 @@ function translateStatic(root) {
1589
2033
  set("#ag-h", "n.agents");
1590
2034
  set("#ag-start", "ag.start");
1591
2035
  set("#ag-clear", "ag.clear");
1592
- set("#ag-dlg-h", "ag.start");
2036
+ set("#ag-dlg-h", "ag.new.title");
1593
2037
  set("#ag-ws-t", "ag.ws");
1594
2038
  set("#ag-p-t", "ag.persona");
1595
2039
  set("#ag-r-t", "ag.runtime");
2040
+ set("#ag-what-b", "ag.new.what");
2041
+ set("#ag-newp", "ag.new.newP");
2042
+ set("#ag-what-p", "pe.what");
2043
+ set("#ag-task-t", "ag.new.task");
2044
+ set("#ag-adv-s", "ag.new.adv");
2045
+ set("#ag-note", "ag.new.note");
2046
+ const task = root.querySelector("#ag-task");
2047
+ if (task) task.placeholder = t("ag.new.taskPh");
2048
+ root
2049
+ .querySelector("#ag-persona")
2050
+ ?.setAttribute("aria-label", t("ag.persona"));
1596
2051
  set("#ag-cancel", "ag.cancel");
1597
2052
  set("#ag-go", "ag.go");
1598
2053
  for (const b of root.querySelectorAll("[data-layouts] button")) {
@@ -1605,8 +2060,6 @@ function translateStatic(root) {
1605
2060
  root
1606
2061
  .querySelector("[data-layouts]")
1607
2062
  ?.setAttribute("aria-label", t("ag.layout"));
1608
- const h = root.querySelector("#ag-dlg-h");
1609
- if (h) h.textContent = h.textContent.replace(/^\+\s*/, "");
1610
2063
  }
1611
2064
 
1612
2065
  /** Fill the page drawn by agents.html and wire it. */
@@ -1679,9 +2132,7 @@ export async function mountAgentsPage(root) {
1679
2132
  root.querySelector("#ag-clear")?.addEventListener("click", (e) => {
1680
2133
  void clearStopped(e.currentTarget);
1681
2134
  });
1682
- root.querySelector("#ag-ws").addEventListener("change", (e) => {
1683
- void fillForWorkspace(e.currentTarget.value);
1684
- });
2135
+ wireStartDialog(root);
1685
2136
  root.querySelector("#ag-cancel").addEventListener("click", () => {
1686
2137
  root.querySelector("#ag-dlg").close?.();
1687
2138
  });
@@ -1693,7 +2144,7 @@ export async function mountAgentsPage(root) {
1693
2144
  // cancels (the dialog's own cancel). A button keeps its own Enter.
1694
2145
  root.querySelector("#ag-form").addEventListener("keydown", (e) => {
1695
2146
  if (e.key !== "Enter" || e.isComposing) return;
1696
- if (e.target.closest("button, textarea")) return;
2147
+ if (e.target.closest("button, textarea, summary")) return;
1697
2148
  e.preventDefault();
1698
2149
  if (!st.root.querySelector("#ag-go").disabled)
1699
2150
  e.currentTarget.requestSubmit();