@monster-orange/den 0.4.13 → 0.4.15
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 +176 -0
- package/README.md +7 -7
- package/docs/distributions.md +185 -7
- package/package.json +1 -1
- package/src/shared/actor-token.ts +12 -0
- package/src/shared/agents/conversations.ts +256 -0
- package/src/shared/agents/den-claude.ts +19 -0
- package/src/shared/agents/records.ts +23 -0
- package/src/shared/agents/runtime.ts +14 -0
- package/src/shared/claim-code.ts +163 -0
- package/src/shared/cli-command.ts +31 -0
- package/src/shared/den-config.ts +14 -0
- package/src/shared/environment-manager/service-runner.ts +31 -12
- package/src/shared/environment-manager/workspaces.ts +31 -0
- package/src/shared/web-session.ts +64 -5
- package/src/ui/cli/agent-ops.ts +320 -18
- package/src/ui/cli/agents-command.ts +89 -2
- package/src/ui/cli/doctor-command.ts +29 -3
- package/src/ui/cli/doctor-onboard.ts +26 -0
- package/src/ui/cli/flag-parser.ts +9 -1
- package/src/ui/cli/index.ts +29 -3
- package/src/ui/cli/onboard-command.ts +180 -0
- package/src/ui/cli/onboard-reads.ts +613 -0
- package/src/ui/cli/repo-command.ts +156 -10
- package/src/ui/cli/web-command.ts +92 -1
- package/src/ui/mcp/api.ts +26 -3
- package/src/ui/mcp/server.ts +12 -2
- package/src/ui/mcp/tools.ts +427 -1
- package/src/ui/server/agent-resume-grant.ts +35 -0
- package/src/ui/server/agent-routes.ts +149 -1
- package/src/ui/server/approval-routes.ts +390 -0
- package/src/ui/server/approvals.ts +416 -0
- package/src/ui/server/github-routes.ts +12 -4
- package/src/ui/server/held-actions.ts +203 -0
- package/src/ui/server/http-helpers.ts +35 -0
- package/src/ui/server/index.ts +286 -16
- package/src/ui/server/lifecycle-exec.ts +70 -0
- package/src/ui/server/onboard-routes.ts +132 -0
- package/src/ui/server/pair-routes.ts +7 -4
- package/src/ui/server/public/agents-page.js +288 -8
- package/src/ui/server/public/agents.html +18 -0
- package/src/ui/server/public/error-page.js +27 -3
- package/src/ui/server/public/header.js +1 -1
- package/src/ui/server/public/i18n/es.js +52 -0
- package/src/ui/server/public/i18n/ja.js +55 -0
- package/src/ui/server/public/index.html +1 -1
- package/src/ui/server/public/local-dev.html +5 -4
- package/src/ui/server/public/persona-ui.css +45 -0
- package/src/ui/server/public/release.html +0 -1
- package/src/ui/server/public/settings-registry.js +3 -1
- package/src/ui/server/public/settings.html +9 -1
- package/src/ui/server/public/shell.js +336 -0
- package/src/ui/server/request-guard.ts +19 -4
- package/src/ui/server/route-classes.ts +41 -1
- package/src/ui/server/session-routes.ts +11 -3
- package/src/ui/server/workspace-routes.ts +21 -10
- package/themes/den/den-theme.json +6 -6
|
@@ -22,8 +22,8 @@ export interface PairRouteDeps {
|
|
|
22
22
|
pairs: PairRequests;
|
|
23
23
|
sendJson: (res: http.ServerResponse, body: unknown, status?: number) => void;
|
|
24
24
|
readBody: (req: http.IncomingMessage) => Promise<string>;
|
|
25
|
-
/** A new browser session for this request (via "code")
|
|
26
|
-
signIn: (req: http.IncomingMessage) => string;
|
|
25
|
+
/** A new browser session for this request (via "code"): its Set-Cookie line and approval key. */
|
|
26
|
+
signIn: (req: http.IncomingMessage) => { cookie: string; approveKey: string };
|
|
27
27
|
/** The client's address for a new request's record, when Den knows it. */
|
|
28
28
|
client: (req: http.IncomingMessage) => string | undefined;
|
|
29
29
|
/** The commands that approve `code`: on the server, and by ssh from the browser's machine. */
|
|
@@ -132,8 +132,11 @@ export async function handlePairRoutes(
|
|
|
132
132
|
deps.sendJson(res, { ok: false, state: "gone" }, 404);
|
|
133
133
|
return true;
|
|
134
134
|
}
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
const s = deps.signIn(req);
|
|
136
|
+
res.setHeader("Set-Cookie", s.cookie);
|
|
137
|
+
res.setHeader("Cache-Control", "no-store");
|
|
138
|
+
// The approval key goes to this answer only (web-session.ts).
|
|
139
|
+
deps.sendJson(res, { ok: true, state: "ok", approveKey: s.approveKey });
|
|
137
140
|
return true;
|
|
138
141
|
}
|
|
139
142
|
deps.sendJson(res, {
|
|
@@ -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
|
-
|
|
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
|
);
|
|
@@ -1608,13 +1702,30 @@ function paintDetails() {
|
|
|
1608
1702
|
* and the dialog says so.
|
|
1609
1703
|
*/
|
|
1610
1704
|
function paintRuntime(reset) {
|
|
1705
|
+
paintRuntimeFields(reset);
|
|
1706
|
+
// Last: a Name that names a running agent disables Start (AG-10.10b).
|
|
1707
|
+
paintStartsAs();
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function paintRuntimeFields(reset) {
|
|
1611
1711
|
const rs = $d("#ag-runtime");
|
|
1612
1712
|
const go = $d("#ag-go");
|
|
1613
1713
|
const ready = $d("#ag-r-ready");
|
|
1614
1714
|
const plain = sd.pid === PLAIN;
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
$d("#ag-
|
|
1715
|
+
// Bring in: the conversation's runtime, its own history; no first
|
|
1716
|
+
// instruction, no Advanced (it keeps what the persona gives).
|
|
1717
|
+
$d("#ag-r-l").hidden = plain || !!sd.bring;
|
|
1718
|
+
$d("#ag-task-l").hidden = plain || !!sd.bring;
|
|
1719
|
+
$d("#ag-adv").hidden = plain || !!sd.bring;
|
|
1720
|
+
$d("#ag-name-l").hidden = plain || !!sd.bring;
|
|
1721
|
+
if (sd.bring) {
|
|
1722
|
+
$d("#ag-r-miss").hidden = true;
|
|
1723
|
+
ready.hidden = true;
|
|
1724
|
+
const ok = !!bringPick() && !!curPersona();
|
|
1725
|
+
go.disabled = !ok;
|
|
1726
|
+
go.dataset.blocked = ok ? "" : "conversation";
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1618
1729
|
const fb = $d("#ag-r-fb");
|
|
1619
1730
|
fb.hidden = true;
|
|
1620
1731
|
if (plain) {
|
|
@@ -1675,7 +1786,10 @@ function paintAdvanced() {
|
|
|
1675
1786
|
const models = rtv?.models ?? [];
|
|
1676
1787
|
const model = sd.model || def.model;
|
|
1677
1788
|
const first = t("ag.new.modelDef", {
|
|
1678
|
-
|
|
1789
|
+
// No model of its own: just the version, so the label fits the select.
|
|
1790
|
+
m: def.model
|
|
1791
|
+
? `${def.model} · ${versionLabel(def.version)}`
|
|
1792
|
+
: versionLabel("latest"),
|
|
1679
1793
|
});
|
|
1680
1794
|
const verFirst = t("ag.new.verDef", {
|
|
1681
1795
|
v: versionLabel(
|
|
@@ -1749,6 +1863,8 @@ async function fillForWorkspace(ws) {
|
|
|
1749
1863
|
window.denShell?.errorToast?.(err);
|
|
1750
1864
|
return false;
|
|
1751
1865
|
}
|
|
1866
|
+
if (!(await fillConversations(ws))) return false;
|
|
1867
|
+
if (sd.bring && sd.pid === PLAIN) sd.pid = "";
|
|
1752
1868
|
const ps = sd.data?.personas ?? [];
|
|
1753
1869
|
if (sd.pid !== PLAIN && !ps.some((p) => p.id === sd.pid)) {
|
|
1754
1870
|
// The first persona the list shows (grouped by category).
|
|
@@ -1765,9 +1881,19 @@ async function fillForWorkspace(ws) {
|
|
|
1765
1881
|
return true;
|
|
1766
1882
|
}
|
|
1767
1883
|
|
|
1768
|
-
|
|
1884
|
+
/** The dialog's words for Start, or for Bring in (AG-17.1). */
|
|
1885
|
+
function dialogMode(bring) {
|
|
1886
|
+
sd.bring = bring ? { list: [], runtimes: [], root: "", pick: "" } : null;
|
|
1887
|
+
$d("#ag-dlg-h").textContent = t(bring ? "ag.bring.title" : "ag.new.title");
|
|
1888
|
+
$d("#ag-note").textContent = t(bring ? "ag.bring.note" : "ag.new.note");
|
|
1889
|
+
$d("#ag-go").textContent = t(bring ? "ag.bring.go" : "ag.go");
|
|
1890
|
+
$d("#ag-conv-f").hidden = !bring;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
async function openStart(bring = false) {
|
|
1769
1894
|
const dlg = $d("#ag-dlg");
|
|
1770
1895
|
if (!dlg || dlg.open) return;
|
|
1896
|
+
dialogMode(bring);
|
|
1771
1897
|
const shown = viewWorkspaces();
|
|
1772
1898
|
const wsSel = $d("#ag-ws");
|
|
1773
1899
|
wsSel.innerHTML = shown
|
|
@@ -1812,21 +1938,127 @@ function startBody() {
|
|
|
1812
1938
|
}
|
|
1813
1939
|
const task = $d("#ag-task").value.trim();
|
|
1814
1940
|
if (task) body.instruction = task;
|
|
1941
|
+
const name = $d("#ag-name").value.trim();
|
|
1942
|
+
if (name) body.name = name;
|
|
1815
1943
|
return body;
|
|
1816
1944
|
}
|
|
1817
1945
|
|
|
1818
1946
|
function showStartError(err) {
|
|
1819
1947
|
const el = $d("#ag-err");
|
|
1820
1948
|
el.textContent = err?.message || String(err);
|
|
1949
|
+
// AG-10.10: that agent is running: offer to open it.
|
|
1950
|
+
const running = err?.data?.running;
|
|
1951
|
+
if (running?.sessionId) {
|
|
1952
|
+
const b = document.createElement("button");
|
|
1953
|
+
b.type = "button";
|
|
1954
|
+
b.className = "den-btn sm";
|
|
1955
|
+
b.id = "ag-open-running";
|
|
1956
|
+
b.textContent = t("ag.new.openRunning");
|
|
1957
|
+
b.addEventListener("click", () => {
|
|
1958
|
+
$d("#ag-dlg").close?.();
|
|
1959
|
+
void openPane(running.sessionId);
|
|
1960
|
+
});
|
|
1961
|
+
el.append(" ", b);
|
|
1962
|
+
}
|
|
1821
1963
|
el.hidden = false;
|
|
1822
1964
|
}
|
|
1823
1965
|
|
|
1966
|
+
/**
|
|
1967
|
+
* AG-10.10b: the id a Name gives, as the server's nameSlug: lowercase,
|
|
1968
|
+
* spaces to "-", only id characters, at most 64; null when none is left.
|
|
1969
|
+
*/
|
|
1970
|
+
export function nameSlug(name) {
|
|
1971
|
+
const s = String(name)
|
|
1972
|
+
.trim()
|
|
1973
|
+
.toLowerCase()
|
|
1974
|
+
.replace(/\s+/g, "-")
|
|
1975
|
+
.replace(/[^a-z0-9._-]/g, "")
|
|
1976
|
+
.replace(/-{2,}/g, "-")
|
|
1977
|
+
.replace(/^[^a-z0-9]+/, "")
|
|
1978
|
+
.slice(0, 64)
|
|
1979
|
+
.replace(/[-._]+$/, "");
|
|
1980
|
+
return s && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(s) ? s : null;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* The id this start takes (AG-10.10, 10.10b), said as you type: a Name is
|
|
1985
|
+
* the id as its slug. A running agent here with that id or title: "is
|
|
1986
|
+
* already running · Open it", Start off. A stopped one: "Resume it", or
|
|
1987
|
+
* Start takes the next suffix.
|
|
1988
|
+
*/
|
|
1989
|
+
function paintStartsAs() {
|
|
1990
|
+
const el = $d("#ag-as");
|
|
1991
|
+
if (!el) return;
|
|
1992
|
+
const p = sd.pid && sd.pid !== PLAIN && !sd.bring ? curPersona() : null;
|
|
1993
|
+
el.hidden = !p;
|
|
1994
|
+
el.textContent = "";
|
|
1995
|
+
if (!p) return;
|
|
1996
|
+
const name = ($d("#ag-name")?.value ?? "").trim();
|
|
1997
|
+
const slug = name ? nameSlug(name) : null;
|
|
1998
|
+
const ws = $d("#ag-ws")?.value || viewWorkspaces()[0];
|
|
1999
|
+
const here = (st.data?.sessions ?? []).filter(
|
|
2000
|
+
(r) => r.kind === "agent" && r.workspace === ws,
|
|
2001
|
+
);
|
|
2002
|
+
const live = (r) => r.state === "running" || r.state === "stopping";
|
|
2003
|
+
const match = name
|
|
2004
|
+
? (here.find(
|
|
2005
|
+
(r) =>
|
|
2006
|
+
live(r) &&
|
|
2007
|
+
(r.agent === slug ||
|
|
2008
|
+
(r.displayName ?? "").toLowerCase() === name.toLowerCase()),
|
|
2009
|
+
) ?? here.find((r) => r.agent === slug))
|
|
2010
|
+
: null;
|
|
2011
|
+
const act = (label, fn) => {
|
|
2012
|
+
const b = document.createElement("button");
|
|
2013
|
+
b.type = "button";
|
|
2014
|
+
b.className = "den-btn sm";
|
|
2015
|
+
b.textContent = label;
|
|
2016
|
+
b.addEventListener("click", fn);
|
|
2017
|
+
return b;
|
|
2018
|
+
};
|
|
2019
|
+
if (match && live(match)) {
|
|
2020
|
+
el.append(
|
|
2021
|
+
t("ag.new.isRunning", { id: match.agent }) + " · ",
|
|
2022
|
+
act(t("ag.new.openIt"), () => {
|
|
2023
|
+
$d("#ag-dlg").close?.();
|
|
2024
|
+
void openPane(match.id);
|
|
2025
|
+
}),
|
|
2026
|
+
);
|
|
2027
|
+
$d("#ag-go").disabled = true;
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
if (match) {
|
|
2031
|
+
el.append(
|
|
2032
|
+
t("ag.new.isStopped", { id: match.agent }) + " · ",
|
|
2033
|
+
act(t("ag.new.resumeIt"), () => {
|
|
2034
|
+
$d("#ag-dlg").close?.();
|
|
2035
|
+
void restartRow(match, "resume", null);
|
|
2036
|
+
}),
|
|
2037
|
+
" " + t("ag.new.orNext", { id: nextFree(slug) }),
|
|
2038
|
+
);
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
el.textContent = t("ag.new.as", {
|
|
2042
|
+
id: slug ?? sd.data?.startsAs?.[p.id] ?? p.id,
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
/** The first `<slug>-<n>` no agent on this page has (the server decides). */
|
|
2047
|
+
function nextFree(slug) {
|
|
2048
|
+
const ids = new Set((st.data?.sessions ?? []).map((r) => r.agent));
|
|
2049
|
+
for (let n = 2; n < 1000; n++)
|
|
2050
|
+
if (!ids.has(`${slug.slice(0, 58)}-${n}`))
|
|
2051
|
+
return `${slug.slice(0, 58)}-${n}`;
|
|
2052
|
+
return slug;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1824
2055
|
async function submitStart(form) {
|
|
1825
2056
|
const dlg = $d("#ag-dlg");
|
|
1826
2057
|
const go = $d("#ag-go");
|
|
1827
2058
|
if (go.disabled) return;
|
|
1828
2059
|
const ws = form.querySelector("#ag-ws").value || viewWorkspaces()[0];
|
|
1829
2060
|
if (!ws || !sd.pid) return;
|
|
2061
|
+
if (sd.bring) return submitBringIn(ws);
|
|
1830
2062
|
const bad = sd.extras.find((c) => commandProblem(c));
|
|
1831
2063
|
if (sd.pid !== PLAIN && bad)
|
|
1832
2064
|
return showStartError(new Error(t("pe.badCmd", { c: bad })));
|
|
@@ -1852,6 +2084,9 @@ async function submitStart(form) {
|
|
|
1852
2084
|
startBody(),
|
|
1853
2085
|
);
|
|
1854
2086
|
window.denShell?.toast?.(t("ag.started", { id: r?.agent?.id ?? sd.pid }));
|
|
2087
|
+
// AG-10.10: the next start of this persona takes another id.
|
|
2088
|
+
personasAsked.delete(ws);
|
|
2089
|
+
$d("#ag-name").value = "";
|
|
1855
2090
|
for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
|
|
1856
2091
|
dlg.close?.();
|
|
1857
2092
|
await refresh();
|
|
@@ -1865,6 +2100,36 @@ async function submitStart(form) {
|
|
|
1865
2100
|
}
|
|
1866
2101
|
}
|
|
1867
2102
|
|
|
2103
|
+
/** Bring in (AG-17.1): the runtime's resume of the conversation picked. */
|
|
2104
|
+
async function submitBringIn(ws) {
|
|
2105
|
+
const dlg = $d("#ag-dlg");
|
|
2106
|
+
const go = $d("#ag-go");
|
|
2107
|
+
const c = bringPick();
|
|
2108
|
+
const p = curPersona();
|
|
2109
|
+
if (!c || !p) return;
|
|
2110
|
+
go.disabled = true;
|
|
2111
|
+
go.textContent = t("ag.bring.going");
|
|
2112
|
+
try {
|
|
2113
|
+
const r = await api(
|
|
2114
|
+
"POST",
|
|
2115
|
+
`/api/workspaces/${encodeURIComponent(ws)}/conversations/${encodeURIComponent(c.id)}/import`,
|
|
2116
|
+
{ persona: p.id, runtime: c.runtime },
|
|
2117
|
+
);
|
|
2118
|
+
window.denShell?.toast?.(t("ag.bring.done", { id: r?.agent?.id ?? p.id }));
|
|
2119
|
+
for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
|
|
2120
|
+
dlg.close?.();
|
|
2121
|
+
await refresh();
|
|
2122
|
+
if (r?.agent?.sessionId) await openPane(r.agent.sessionId);
|
|
2123
|
+
} catch (err) {
|
|
2124
|
+
showStartError(err);
|
|
2125
|
+
// Still open elsewhere, or taken since: the list says so now.
|
|
2126
|
+
await fillConversations(ws);
|
|
2127
|
+
} finally {
|
|
2128
|
+
go.textContent = t("ag.bring.go");
|
|
2129
|
+
if (dlg.open) paintRuntime(false);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
1868
2133
|
/** New persona, Edit or Duplicate from the dialog; back to it after. */
|
|
1869
2134
|
async function editFromStart(kind) {
|
|
1870
2135
|
const p = curPersona();
|
|
@@ -1892,6 +2157,12 @@ async function editFromStart(kind) {
|
|
|
1892
2157
|
function wireStartDialog(root) {
|
|
1893
2158
|
const form = root.querySelector("#ag-form");
|
|
1894
2159
|
form.addEventListener("click", (e) => {
|
|
2160
|
+
const cr = e.target.closest("#ag-conv [data-cid]");
|
|
2161
|
+
if (cr) {
|
|
2162
|
+
e.preventDefault();
|
|
2163
|
+
pickConversation(cr.dataset.cid);
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
1895
2166
|
const pr = e.target.closest("#ag-persona [data-pid]");
|
|
1896
2167
|
if (pr) {
|
|
1897
2168
|
e.preventDefault();
|
|
@@ -1944,6 +2215,9 @@ function wireStartDialog(root) {
|
|
|
1944
2215
|
if (e.target.id === "ag-q") {
|
|
1945
2216
|
sd.f.q = e.target.value;
|
|
1946
2217
|
paintPlist(false);
|
|
2218
|
+
} else if (e.target.id === "ag-name") {
|
|
2219
|
+
// AG-10.10b: what the Name gives, as you type.
|
|
2220
|
+
paintRuntime(false);
|
|
1947
2221
|
} else if (e.target.id === "ag-tools") {
|
|
1948
2222
|
sd.extras = splitCommands(e.target.value);
|
|
1949
2223
|
syncPermBlock(form, "ag", sd.data, {
|
|
@@ -2032,6 +2306,8 @@ function translateStatic(root) {
|
|
|
2032
2306
|
};
|
|
2033
2307
|
set("#ag-h", "n.agents");
|
|
2034
2308
|
set("#ag-start", "ag.start");
|
|
2309
|
+
set("#ag-bring", "ag.bring");
|
|
2310
|
+
set("#ag-conv-t", "ag.bring.conv");
|
|
2035
2311
|
set("#ag-clear", "ag.clear");
|
|
2036
2312
|
set("#ag-dlg-h", "ag.new.title");
|
|
2037
2313
|
set("#ag-ws-t", "ag.ws");
|
|
@@ -2041,6 +2317,7 @@ function translateStatic(root) {
|
|
|
2041
2317
|
set("#ag-newp", "ag.new.newP");
|
|
2042
2318
|
set("#ag-what-p", "pe.what");
|
|
2043
2319
|
set("#ag-task-t", "ag.new.task");
|
|
2320
|
+
set("#ag-name-t", "ag.new.name");
|
|
2044
2321
|
set("#ag-adv-s", "ag.new.adv");
|
|
2045
2322
|
set("#ag-note", "ag.new.note");
|
|
2046
2323
|
const task = root.querySelector("#ag-task");
|
|
@@ -2129,6 +2406,9 @@ export async function mountAgentsPage(root) {
|
|
|
2129
2406
|
root.querySelector("#ag-start").addEventListener("click", () => {
|
|
2130
2407
|
void openStart();
|
|
2131
2408
|
});
|
|
2409
|
+
root.querySelector("#ag-bring")?.addEventListener("click", () => {
|
|
2410
|
+
void openStart(true);
|
|
2411
|
+
});
|
|
2132
2412
|
root.querySelector("#ag-clear")?.addEventListener("click", (e) => {
|
|
2133
2413
|
void clearStopped(e.currentTarget);
|
|
2134
2414
|
});
|
|
@@ -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>
|
|
@@ -139,7 +139,7 @@ function withCmd(text, cmd) {
|
|
|
139
139
|
function withHost(t, key, vars) {
|
|
140
140
|
const marks = {};
|
|
141
141
|
const v = { ...vars };
|
|
142
|
-
for (const k of ["host", "target"])
|
|
142
|
+
for (const k of ["host", "target", "user"])
|
|
143
143
|
if (k in v) {
|
|
144
144
|
marks[k] = v[k];
|
|
145
145
|
v[k] = `\u0001${k}\u0001`;
|
|
@@ -203,6 +203,20 @@ export function countdownText(ms) {
|
|
|
203
203
|
* Where a signed-in page goes: the page that was asked for. On /signin,
|
|
204
204
|
* its same-origin `next` (else `/`); anywhere else, the same URL.
|
|
205
205
|
*/
|
|
206
|
+
/**
|
|
207
|
+
* 0.4.15: keep a sign-in's approval key (web-session.ts) in this origin's
|
|
208
|
+
* localStorage, where shell.js sends it from. It comes in the sign-in's
|
|
209
|
+
* own answer only.
|
|
210
|
+
*/
|
|
211
|
+
function keepApproveKey(b) {
|
|
212
|
+
if (!b || typeof b.approveKey !== "string") return;
|
|
213
|
+
try {
|
|
214
|
+
localStorage.setItem("den.approveKey", b.approveKey);
|
|
215
|
+
} catch {
|
|
216
|
+
/* storage blocked: this browser signs in again to approve */
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
206
220
|
export function landingAfterSignIn(loc) {
|
|
207
221
|
if (loc.pathname !== "/signin") return loc.pathname + loc.search + loc.hash;
|
|
208
222
|
const n = new URLSearchParams(loc.search).get("next") || "/";
|
|
@@ -263,6 +277,7 @@ function wireGithub(root, t, s) {
|
|
|
263
277
|
return;
|
|
264
278
|
}
|
|
265
279
|
if (b.state === "ok") {
|
|
280
|
+
keepApproveKey(b);
|
|
266
281
|
show(
|
|
267
282
|
`<p class="lead" style="margin-top:12px">${esc(t("err.signin.githubDone"))}</p>`,
|
|
268
283
|
);
|
|
@@ -351,6 +366,9 @@ function wirePair(root, t, s) {
|
|
|
351
366
|
if (!box) return;
|
|
352
367
|
const host = s.machine || "";
|
|
353
368
|
const target = s.ssh?.target || "";
|
|
369
|
+
// SEC-2a-UX: the account to log in as, so the SSH app doesn't guess one.
|
|
370
|
+
const user = s.ssh?.user || "";
|
|
371
|
+
const login = user ? `${user}@${target}` : target;
|
|
354
372
|
let poll = null;
|
|
355
373
|
let tick = null;
|
|
356
374
|
let gen = 0;
|
|
@@ -376,7 +394,7 @@ function wirePair(root, t, s) {
|
|
|
376
394
|
const show = (b) => {
|
|
377
395
|
const term =
|
|
378
396
|
s.ssh?.name && target
|
|
379
|
-
? `<a class="btn" id="den-pair-term" href="ssh://${esc(
|
|
397
|
+
? `<a class="btn" id="den-pair-term" href="ssh://${esc(login)}">${esc(t("err.signin.openTerminal"))}</a>`
|
|
380
398
|
: "";
|
|
381
399
|
box.innerHTML =
|
|
382
400
|
`<ol class="den-steps">` +
|
|
@@ -389,7 +407,11 @@ function wirePair(root, t, s) {
|
|
|
389
407
|
`<p>${withHost(t, "err.signin.noTerminal", { host })}</p>` +
|
|
390
408
|
cmdLine("den-pair-ssh", b.ssh, t, term) +
|
|
391
409
|
(term
|
|
392
|
-
? `<p class="den-pair-note"
|
|
410
|
+
? `<p class="den-pair-note" id="den-pair-note">${
|
|
411
|
+
user
|
|
412
|
+
? withHost(t, "err.signin.sshUserNote", { host, user })
|
|
413
|
+
: withHost(t, "err.signin.sshNote", { target })
|
|
414
|
+
}</p>`
|
|
393
415
|
: "");
|
|
394
416
|
wireCopy(box, t);
|
|
395
417
|
};
|
|
@@ -432,6 +454,7 @@ function wirePair(root, t, s) {
|
|
|
432
454
|
return;
|
|
433
455
|
}
|
|
434
456
|
if (p.state === "ok") {
|
|
457
|
+
keepApproveKey(p);
|
|
435
458
|
stop();
|
|
436
459
|
box.innerHTML = `<p class="lead" id="den-pair-in">${esc(t("err.signin.youreIn"))}</p>`;
|
|
437
460
|
location.replace(landingAfterSignIn(location));
|
|
@@ -524,6 +547,7 @@ function mountSignedOut(root, sh, d) {
|
|
|
524
547
|
.then((b) => {
|
|
525
548
|
go.disabled = false;
|
|
526
549
|
if (b.ok) {
|
|
550
|
+
keepApproveKey(b);
|
|
527
551
|
err.textContent = "";
|
|
528
552
|
form.innerHTML = `<p class="lead" id="den-confirm-in">${esc(t("err.signin.youreIn"))}</p>`;
|
|
529
553
|
location.replace(typeof b.next === "string" ? b.next : "/");
|
|
@@ -251,7 +251,7 @@ function _injectChangelogStyles() {
|
|
|
251
251
|
padding: 5px 16px; border-radius: 5px; font-size: 12px;
|
|
252
252
|
font-weight: 600; cursor: pointer;
|
|
253
253
|
}
|
|
254
|
-
.cl-got-it:hover {
|
|
254
|
+
.cl-got-it:hover { filter: brightness(0.92); }
|
|
255
255
|
`;
|
|
256
256
|
document.head.appendChild(style);
|
|
257
257
|
|
|
@@ -271,6 +271,8 @@
|
|
|
271
271
|
"err.signin.noTerminal":
|
|
272
272
|
"¿No tienes una terminal en {host}? Desde este ordenador, ejecuta:",
|
|
273
273
|
"err.signin.openTerminal": "Abrir Terminal",
|
|
274
|
+
"err.signin.sshUserNote":
|
|
275
|
+
"Inicia sesión con tu cuenta de {host} ({user}) y su contraseña, o con una clave SSH.",
|
|
274
276
|
"err.signin.sshNote":
|
|
275
277
|
"Abrir Terminal abre ssh://{target} en tu aplicación de terminal. Necesita acceso SSH a {target}.",
|
|
276
278
|
"err.signin.whyTitle": "¿Por qué?",
|
|
@@ -456,6 +458,14 @@
|
|
|
456
458
|
"ag.new.plainP":
|
|
457
459
|
"Una terminal en la carpeta del espacio de trabajo. Sin agente.",
|
|
458
460
|
"ag.new.task": "Primera instrucción (opcional)",
|
|
461
|
+
"ag.new.name": "Nombre (opcional)",
|
|
462
|
+
"ag.new.as": "Se inicia como {id}",
|
|
463
|
+
"ag.new.openRunning": "Abrir el que está en ejecución",
|
|
464
|
+
"ag.new.isRunning": "{id} ya está en ejecución",
|
|
465
|
+
"ag.new.openIt": "Abrirlo",
|
|
466
|
+
"ag.new.isStopped": "{id} está detenido",
|
|
467
|
+
"ag.new.resumeIt": "Reanudarlo",
|
|
468
|
+
"ag.new.orNext": "o Iniciar crea {id}",
|
|
459
469
|
"ag.new.taskPh":
|
|
460
470
|
"p. ej., Toma la historia del estado vacío. Sigue el diseño y añade una prueba.",
|
|
461
471
|
"ag.new.adv": "Avanzado: modelo y permisos",
|
|
@@ -465,6 +475,48 @@
|
|
|
465
475
|
"ag.new.modelDef": "Predeterminado de la persona ({m})",
|
|
466
476
|
"ag.new.ver": "Versión",
|
|
467
477
|
"ag.new.verDef": "Predeterminado de la persona ({v})",
|
|
478
|
+
// AG-17.1, Bring in.
|
|
479
|
+
"ag.bring": "Traer",
|
|
480
|
+
"ag.bring.title": "Traer una conversación",
|
|
481
|
+
"ag.bring.conv": "Conversación",
|
|
482
|
+
"ag.bring.help":
|
|
483
|
+
"Conversaciones iniciadas fuera de Den en la carpeta de este espacio de trabajo o en las de sus repositorios, la más reciente primero.",
|
|
484
|
+
"ag.bring.none":
|
|
485
|
+
"No hay conversaciones iniciadas fuera de Den en la carpeta de este espacio de trabajo ni en las de sus repositorios.",
|
|
486
|
+
"ag.bring.untitled": "Conversación sin título",
|
|
487
|
+
"ag.bring.open": "Abierta en otro lugar",
|
|
488
|
+
"ag.bring.openTip": "Sigue abierta en otra terminal. Ciérrala allí primero",
|
|
489
|
+
"ag.bring.agent": "Agente {id}",
|
|
490
|
+
"ag.bring.note":
|
|
491
|
+
"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.",
|
|
492
|
+
"ag.bring.go": "Traer",
|
|
493
|
+
"ag.bring.going": "Trayendo…",
|
|
494
|
+
"ag.bring.done": "Traída como {id}",
|
|
495
|
+
"ag.bring.now": "ahora mismo",
|
|
496
|
+
"ag.bring.min": "hace {n} min",
|
|
497
|
+
"ag.bring.h": "hace {n} h",
|
|
498
|
+
"ap.title": "{agent} quiere:",
|
|
499
|
+
"ap.agent": "El agente {name}",
|
|
500
|
+
"ap.noAgent": "Algo en esta máquina (sin identidad de agente)",
|
|
501
|
+
"ap.noAgentWarn": "Si no le pediste esto a un agente, deniégalo.",
|
|
502
|
+
"ap.signin.title": "Algo espera tu aprobación.",
|
|
503
|
+
"ap.signin": "Inicia sesión para verlo: ejecuta {cmd} y abre el enlace.",
|
|
504
|
+
"ap.do.workspace_create": 'Crear el espacio "{name}"',
|
|
505
|
+
"ap.do.repo_add":
|
|
506
|
+
'Añadir el repositorio de {path} al espacio "{workspace}"',
|
|
507
|
+
"ap.do.repo_add.new":
|
|
508
|
+
'Añadir el repositorio de {path} a un espacio nuevo "{workspace}"',
|
|
509
|
+
"ap.do.service_try": "Iniciar {service}",
|
|
510
|
+
"ap.says": "El agente dice:",
|
|
511
|
+
"ap.review": "Revisar",
|
|
512
|
+
"ap.hide": "Ocultar detalles",
|
|
513
|
+
"ap.later": "Más tarde",
|
|
514
|
+
"ap.deny": "Denegar",
|
|
515
|
+
"ap.allow": "Permitir",
|
|
516
|
+
"ap.waits": "Nada cambia hasta que lo permitas. Espera hasta las {time}.",
|
|
517
|
+
"ap.more": "{n} más en espera",
|
|
518
|
+
"ap.failed": "Se ejecutó y falló: {error}",
|
|
519
|
+
"ap.done": "Hecho: {what}",
|
|
468
520
|
"ag.new.note":
|
|
469
521
|
"Se inicia en la carpeta del espacio de trabajo. Sigue en ejecución si cierras el panel.",
|
|
470
522
|
"ag.new.started": "{p} iniciado en {w}",
|