@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.
- package/CHANGELOG.md +102 -0
- package/README.md +7 -7
- package/docs/distributions.md +146 -10
- package/package.json +1 -1
- package/src/shared/agents/default-pack/be-dev.md +1 -0
- package/src/shared/agents/default-pack/devops.md +1 -0
- package/src/shared/agents/default-pack/fe-dev.md +1 -0
- package/src/shared/agents/default-pack/orchestrator.md +3 -1
- package/src/shared/agents/default-pack/planner.md +1 -0
- package/src/shared/agents/default-pack/reviewer.md +1 -0
- package/src/shared/agents/default-pack/tester.md +16 -0
- package/src/shared/agents/default-pack/ux-designer.md +1 -0
- package/src/shared/agents/den-claude.ts +119 -13
- package/src/shared/agents/permissions.ts +108 -0
- package/src/shared/agents/persona-store.ts +286 -0
- package/src/shared/agents/personas.ts +116 -12
- package/src/shared/agents/records.ts +28 -0
- package/src/shared/agents/roles.ts +288 -0
- package/src/shared/agents/runtime.ts +41 -0
- package/src/shared/den-config.ts +20 -1
- package/src/ui/cli/agent-ops.ts +535 -17
- package/src/ui/cli/agents-command.ts +12 -2
- package/src/ui/cli/env-probe.ts +4 -1
- package/src/ui/mcp/tools.ts +1 -1
- package/src/ui/server/agent-routes.ts +260 -21
- package/src/ui/server/public/agent-start.js +3 -2
- package/src/ui/server/public/agents-page.js +499 -48
- package/src/ui/server/public/agents.html +58 -15
- package/src/ui/server/public/i18n/es.js +120 -0
- package/src/ui/server/public/i18n/ja.js +120 -0
- package/src/ui/server/public/persona-ui.css +539 -0
- package/src/ui/server/public/persona-ui.js +796 -0
- package/src/ui/server/public/settings-page.js +4 -9
- package/src/ui/server/public/settings-registry.js +22 -0
- package/src/ui/server/public/settings.html +1 -0
- package/src/ui/server/public/shell.js +117 -0
- package/src/ui/server/public/topbar.js +2 -2
- package/src/ui/server/route-classes.ts +18 -0
- package/src/ui/server/session-routes.ts +21 -5
- 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-
|
|
31
|
-
*
|
|
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 {
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
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 =
|
|
1769
|
+
const dlg = $d("#ag-dlg");
|
|
1504
1770
|
if (!dlg || dlg.open) return;
|
|
1505
1771
|
const shown = viewWorkspaces();
|
|
1506
|
-
const wsSel =
|
|
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
|
-
|
|
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 =
|
|
1519
|
-
const 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
|
-
|
|
1523
|
-
const
|
|
1524
|
-
if (
|
|
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 ??
|
|
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
|
-
|
|
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.
|
|
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
|
|
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();
|