@monster-orange/den 0.4.15 → 0.4.16
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 +67 -0
- package/README.md +7 -7
- package/docs/distributions.md +82 -11
- package/package.json +1 -1
- package/src/shared/agents/conversations.ts +122 -4
- package/src/shared/agents/den-claude.ts +13 -1
- package/src/shared/agents/records.ts +19 -0
- package/src/shared/agents/runtime.ts +5 -0
- package/src/shared/github-app.ts +7 -0
- package/src/ui/cli/agent-ops.ts +297 -1
- package/src/ui/cli/agents-command.ts +67 -3
- package/src/ui/server/agent-routes.ts +89 -2
- package/src/ui/server/github-routes.ts +1 -1
- package/src/ui/server/index.ts +22 -5
- package/src/ui/server/public/agents-page.js +287 -26
- package/src/ui/server/public/agents.html +1 -0
- package/src/ui/server/public/error-page.js +70 -15
- package/src/ui/server/public/error.html +18 -0
- package/src/ui/server/public/i18n/es.js +36 -5
- package/src/ui/server/public/i18n/ja.js +36 -5
- package/src/ui/server/public/persona-ui.css +11 -0
- package/src/ui/server/public/shell.js +35 -5
- package/src/ui/server/route-classes.ts +6 -0
- package/src/ui/server/session-routes.ts +3 -0
package/src/ui/server/index.ts
CHANGED
|
@@ -244,6 +244,7 @@ import {
|
|
|
244
244
|
githubClientId,
|
|
245
245
|
githubEndpoints,
|
|
246
246
|
noClientIdMessage,
|
|
247
|
+
GITHUB_SIGNIN_DOCS,
|
|
247
248
|
} from "../../shared/github-app.js";
|
|
248
249
|
import { githubDeviceClient } from "../../shared/github-device.js";
|
|
249
250
|
import { readWebOwner } from "../../shared/web-owner.js";
|
|
@@ -1331,10 +1332,19 @@ function denEdgeRoutes(): { host: string; url: string }[] {
|
|
|
1331
1332
|
}
|
|
1332
1333
|
}
|
|
1333
1334
|
|
|
1334
|
-
/**
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1335
|
+
/**
|
|
1336
|
+
* What the signed-out page says about Sign in with GitHub: its state, and
|
|
1337
|
+
* (SEC-2a-UX) the owner's login once one is linked, for step 1's "Owner:
|
|
1338
|
+
* @login".
|
|
1339
|
+
*/
|
|
1340
|
+
function githubSignin():
|
|
1341
|
+
| { state: "no_client_id" | "no_owner" }
|
|
1342
|
+
| { state: "ready"; ownerLogin: string } {
|
|
1343
|
+
if (!GITHUB_CLIENT_ID) return { state: "no_client_id" };
|
|
1344
|
+
const owner = readWebOwner(getStateDir());
|
|
1345
|
+
return owner
|
|
1346
|
+
? { state: "ready", ownerLogin: owner.login }
|
|
1347
|
+
: { state: "no_owner" };
|
|
1338
1348
|
}
|
|
1339
1349
|
|
|
1340
1350
|
/**
|
|
@@ -1426,6 +1436,7 @@ function sendSignedOut(
|
|
|
1426
1436
|
const cmd = cliCommand(bin, "web", "login", ...loginArgs);
|
|
1427
1437
|
const ssh = sshTargetOf(req);
|
|
1428
1438
|
const user = sshUserNow();
|
|
1439
|
+
const gh = githubSignin();
|
|
1429
1440
|
res.setHeader("Cache-Control", "no-store");
|
|
1430
1441
|
sendErrorPage(req, res, {
|
|
1431
1442
|
status: 401,
|
|
@@ -1449,8 +1460,10 @@ function sendSignedOut(
|
|
|
1449
1460
|
},
|
|
1450
1461
|
// SEC-2a: Sign in with GitHub, and why it can't be offered yet.
|
|
1451
1462
|
github: {
|
|
1452
|
-
state:
|
|
1463
|
+
state: gh.state,
|
|
1453
1464
|
owner: cliCommand(bin, "web", "owner", "link"),
|
|
1465
|
+
...(gh.state === "ready" ? { ownerLogin: gh.ownerLogin } : {}),
|
|
1466
|
+
learnMore: GITHUB_SIGNIN_DOCS,
|
|
1454
1467
|
...(GITHUB_CLIENT_ID
|
|
1455
1468
|
? {}
|
|
1456
1469
|
: { message: noClientIdMessage(product().cli) }),
|
|
@@ -4073,6 +4086,10 @@ async function handleRequest(
|
|
|
4073
4086
|
actor: null,
|
|
4074
4087
|
signedIn: false,
|
|
4075
4088
|
...(g && !g.beyond ? { identity: { kind: "local" } } : {}),
|
|
4089
|
+
// SEC-2a-UX: the signed-out page watches this, so Sign in with
|
|
4090
|
+
// GitHub appears once an owner is linked, with no reload. The
|
|
4091
|
+
// owner's login is what that page shows anyway.
|
|
4092
|
+
...(g?.beyond || !g ? { github: githubSignin() } : {}),
|
|
4076
4093
|
});
|
|
4077
4094
|
}
|
|
4078
4095
|
// Sign out: this browser's session ends and its cookie is cleared.
|
|
@@ -565,6 +565,7 @@ async function refresh() {
|
|
|
565
565
|
} catch {
|
|
566
566
|
/* the last list stays; the next ask may answer */
|
|
567
567
|
}
|
|
568
|
+
followAgents();
|
|
568
569
|
st.scopeLabel = scopeLabelFor(viewWorkspaces());
|
|
569
570
|
paintList();
|
|
570
571
|
paintSub();
|
|
@@ -572,14 +573,78 @@ async function refresh() {
|
|
|
572
573
|
const strip = st.root.querySelector("#agr > .agtabs");
|
|
573
574
|
if (strip) paintTabs(strip);
|
|
574
575
|
if (!st.root.isConnected) return;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
576
|
+
// A pane whose session just ended may have a new one soon (a persona
|
|
577
|
+
// change, a resume): look again every second for a while.
|
|
578
|
+
const soon = Date.now() < (st.followUntil ?? 0);
|
|
579
|
+
st.timer = setTimeout(
|
|
580
|
+
() => {
|
|
581
|
+
if (!document.hidden) void refresh();
|
|
582
|
+
else st.timer = setTimeout(refresh, REFRESH_MS);
|
|
583
|
+
},
|
|
584
|
+
soon ? FOLLOW_MS : REFRESH_MS,
|
|
585
|
+
);
|
|
579
586
|
}
|
|
580
587
|
|
|
581
588
|
const rowOf = (id) => st.data?.sessions?.find((s) => s.id === id) ?? null;
|
|
582
589
|
|
|
590
|
+
/** How often, and how long, the list is read after a pane's session ends. */
|
|
591
|
+
const FOLLOW_MS = 1_000;
|
|
592
|
+
const FOLLOW_FOR_MS = 15_000;
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* A pane follows its agent, not a session (AG-18): when the agent's
|
|
596
|
+
* session changes (Change persona, Resume, a rotation), the pane moves to
|
|
597
|
+
* the new one in the same slot, its scrollback kept.
|
|
598
|
+
*/
|
|
599
|
+
function followAgents() {
|
|
600
|
+
for (const p of [...st.panes.values()]) {
|
|
601
|
+
const r = rowOf(p.id);
|
|
602
|
+
if (r?.kind === "agent" && r.agent) {
|
|
603
|
+
p.agent = r.agent;
|
|
604
|
+
p.agentWs = r.workspace;
|
|
605
|
+
}
|
|
606
|
+
if (!p.agent || p.closed || r?.state === "running") continue;
|
|
607
|
+
const next = st.data?.sessions?.find(
|
|
608
|
+
(x) =>
|
|
609
|
+
x.kind === "agent" &&
|
|
610
|
+
x.agent === p.agent &&
|
|
611
|
+
x.workspace === p.agentWs &&
|
|
612
|
+
x.id !== p.id &&
|
|
613
|
+
x.state === "running",
|
|
614
|
+
);
|
|
615
|
+
if (next && !st.panes.has(next.id)) retarget(p, next);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Move pane `p` to session row `row`, where it was. */
|
|
620
|
+
function retarget(p, row) {
|
|
621
|
+
const old = p.id;
|
|
622
|
+
const sock = p.sock;
|
|
623
|
+
p.sock = null;
|
|
624
|
+
try {
|
|
625
|
+
sock?.close(1000, "switched");
|
|
626
|
+
} catch {
|
|
627
|
+
/* already closed */
|
|
628
|
+
}
|
|
629
|
+
st.panes.delete(old);
|
|
630
|
+
p.id = row.id;
|
|
631
|
+
st.panes.set(row.id, p);
|
|
632
|
+
st.open = st.open.map((x) => (x === old ? row.id : x));
|
|
633
|
+
if (st.side.delete(old)) st.side.add(row.id);
|
|
634
|
+
if (st.cur === old) st.cur = row.id;
|
|
635
|
+
p.el.dataset.id = row.id;
|
|
636
|
+
p.el.setAttribute("aria-label", row.label ?? row.id);
|
|
637
|
+
p.exited = false;
|
|
638
|
+
p.lost = false;
|
|
639
|
+
p.term?.write(
|
|
640
|
+
`\r\n\x1b[2m${t("ag.switched", { p: row.persona ?? row.agent })}\x1b[0m\r\n`,
|
|
641
|
+
);
|
|
642
|
+
paintBar(p);
|
|
643
|
+
paintLayout();
|
|
644
|
+
writeUrl();
|
|
645
|
+
void connect(p);
|
|
646
|
+
}
|
|
647
|
+
|
|
583
648
|
// ── the panes ───────────────────────────────────────────────────────────
|
|
584
649
|
|
|
585
650
|
async function loadXterm() {
|
|
@@ -871,13 +936,18 @@ function paintBar(p) {
|
|
|
871
936
|
const reconnect = p.lost
|
|
872
937
|
? `<button type="button" class="den-btn sm" data-act="reconnect">${esc(t("ag.reconnect"))}</button>`
|
|
873
938
|
: "";
|
|
939
|
+
// AG-18: ⋯, its menu: Change persona… (a runtime that can resume).
|
|
940
|
+
const more =
|
|
941
|
+
r?.kind === "agent" && r.personaChange
|
|
942
|
+
? `<button type="button" class="den-btn sm" data-act="more" aria-haspopup="menu" aria-expanded="false" aria-label="${esc(t("ag.more", { id: r.agent }))}" title="${esc(t("ag.more", { id: r.agent }))}">⋯</button>`
|
|
943
|
+
: "";
|
|
874
944
|
bar.innerHTML =
|
|
875
945
|
`<span class="den-dot ${tone}" aria-hidden="true"></span>` +
|
|
876
946
|
`<b>${esc(name)}</b><span class="where mono">${esc(where)}</span>` +
|
|
877
947
|
(r?.tokenRevoked
|
|
878
948
|
? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
|
|
879
949
|
: "") +
|
|
880
|
-
`<span class="x">${reconnect}${stop}${resume}<button type="button" class="den-btn sm" data-act="close" aria-label="${esc(t("ag.detachName", { id: name }))}" title="${esc(t("ag.detach"))}">×</button></span>`;
|
|
950
|
+
`<span class="x">${reconnect}${stop}${resume}${more}<button type="button" class="den-btn sm" data-act="close" aria-label="${esc(t("ag.detachName", { id: name }))}" title="${esc(t("ag.detach"))}">×</button></span>`;
|
|
881
951
|
// Tabs: the tab's × closes it; the pane's bar has none.
|
|
882
952
|
const x = bar.querySelector('[data-act="close"]');
|
|
883
953
|
if (x) x.hidden = shownLayout() === "tabs";
|
|
@@ -1043,6 +1113,8 @@ async function connect(p) {
|
|
|
1043
1113
|
paintBar(p);
|
|
1044
1114
|
} else if (msg?.type === "exit") {
|
|
1045
1115
|
p.exited = true;
|
|
1116
|
+
// Its agent may continue in a new session (followAgents).
|
|
1117
|
+
st.followUntil = Date.now() + FOLLOW_FOR_MS;
|
|
1046
1118
|
const code =
|
|
1047
1119
|
typeof msg.exitCode === "number" ? ` (${msg.exitCode})` : "";
|
|
1048
1120
|
p.term?.write(`\r\n${t("ag.exitedLine", { code })}\r\n`);
|
|
@@ -1255,7 +1327,7 @@ async function openPane(id, { front = true } = {}) {
|
|
|
1255
1327
|
) {
|
|
1256
1328
|
if (e.isComposing) return true;
|
|
1257
1329
|
e.preventDefault();
|
|
1258
|
-
if (softNewlineKey(e)) typeIn(softNewlineOf(id));
|
|
1330
|
+
if (softNewlineKey(e)) typeIn(softNewlineOf(p.id));
|
|
1259
1331
|
return false;
|
|
1260
1332
|
}
|
|
1261
1333
|
return true;
|
|
@@ -1349,9 +1421,13 @@ async function restartRow(r, verb, button) {
|
|
|
1349
1421
|
{ id: r.agent },
|
|
1350
1422
|
),
|
|
1351
1423
|
);
|
|
1352
|
-
|
|
1424
|
+
// Its open pane follows it to the new session (followAgents, in
|
|
1425
|
+
// refresh); one that couldn't (not running yet) is closed for it.
|
|
1353
1426
|
await refresh();
|
|
1354
|
-
|
|
1427
|
+
const next = out?.agent?.sessionId;
|
|
1428
|
+
if (next && next !== r.id && st.panes.has(r.id))
|
|
1429
|
+
closePane(r.id, { quiet: true });
|
|
1430
|
+
if (next) await openPane(next);
|
|
1355
1431
|
} catch (err) {
|
|
1356
1432
|
window.denShell?.errorToast?.(err);
|
|
1357
1433
|
await refresh();
|
|
@@ -1538,6 +1614,9 @@ const sd = {
|
|
|
1538
1614
|
// AG-17.1, Bring in: null for Start; else the workspace's conversations,
|
|
1539
1615
|
// the runtimes that can't bring one in, and the one picked.
|
|
1540
1616
|
bring: null,
|
|
1617
|
+
// AG-18, Change persona: null; else the agent's row (id, workspace,
|
|
1618
|
+
// persona, model) whose persona changes.
|
|
1619
|
+
change: null,
|
|
1541
1620
|
};
|
|
1542
1621
|
|
|
1543
1622
|
/** The conversation picked in Bring in, when it can be brought in. */
|
|
@@ -1625,7 +1704,38 @@ function pickConversation(id) {
|
|
|
1625
1704
|
sd.bring.pick = id;
|
|
1626
1705
|
for (const b of st.root.querySelectorAll("#ag-conv [data-cid]"))
|
|
1627
1706
|
b.setAttribute("aria-checked", String(b.dataset.cid === id));
|
|
1628
|
-
|
|
1707
|
+
// AG-17.1c: its suggested persona, preselected (never submitted).
|
|
1708
|
+
if (c.suggest && (sd.data?.personas ?? []).some((p) => p.id === c.suggest))
|
|
1709
|
+
pickPersona(c.suggest);
|
|
1710
|
+
else paintRuntime(false);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* AG-17.1c, AG-18: what the persona changes, in one short list under it:
|
|
1715
|
+
* the model kept (or the one picked), what it works as from now on, and
|
|
1716
|
+
* for Bring in the MCP servers it won't have.
|
|
1717
|
+
*/
|
|
1718
|
+
function paintKeep() {
|
|
1719
|
+
const el = $d("#ag-keep");
|
|
1720
|
+
if (!el) return;
|
|
1721
|
+
const p = curPersona();
|
|
1722
|
+
const from = sd.bring ? bringPick() : sd.change;
|
|
1723
|
+
el.hidden = !p || !from;
|
|
1724
|
+
if (el.hidden) return void (el.innerHTML = "");
|
|
1725
|
+
const items = [];
|
|
1726
|
+
if (sd.model) items.push(t("ag.keep.modelNew", { m: sd.model }));
|
|
1727
|
+
else if (from.model) items.push(t("ag.keep.model", { m: from.model }));
|
|
1728
|
+
items.push(
|
|
1729
|
+
t("ag.keep.from", {
|
|
1730
|
+
p: personaName(p),
|
|
1731
|
+
mode: t(`pm.${p.permission || "ask"}`),
|
|
1732
|
+
}),
|
|
1733
|
+
);
|
|
1734
|
+
if (sd.bring && from.lost?.length)
|
|
1735
|
+
items.push(t("ag.keep.lost", { s: from.lost.join(", ") }));
|
|
1736
|
+
el.innerHTML = items
|
|
1737
|
+
.map((x, i) => `<li data-keep="${i}">${esc(x)}</li>`)
|
|
1738
|
+
.join("");
|
|
1629
1739
|
}
|
|
1630
1740
|
|
|
1631
1741
|
const $d = (sel) => st.root.querySelector(sel);
|
|
@@ -1636,6 +1746,10 @@ const curPersona = () =>
|
|
|
1636
1746
|
|
|
1637
1747
|
/** The runtime the dialog will start: the one picked, else the persona's. */
|
|
1638
1748
|
function chosenRuntime() {
|
|
1749
|
+
// Change persona and Bring in: the agent's, or the conversation's.
|
|
1750
|
+
if (sd.change?.runtime) return sd.change.runtime;
|
|
1751
|
+
const conv = sd.bring ? bringPick() : null;
|
|
1752
|
+
if (conv?.runtime) return conv.runtime;
|
|
1639
1753
|
const p = curPersona();
|
|
1640
1754
|
return $d("#ag-runtime").value || personaRuntime(sd.data, p);
|
|
1641
1755
|
}
|
|
@@ -1657,8 +1771,9 @@ function plistHtml() {
|
|
|
1657
1771
|
const rows = personaListHtml(sd.f, ps, cats, (p) =>
|
|
1658
1772
|
personaRowHtml(p, p.id === sd.pid, sd.data),
|
|
1659
1773
|
);
|
|
1660
|
-
// Bring in continues a conversation:
|
|
1661
|
-
|
|
1774
|
+
// Bring in continues a conversation, and a change keeps one: each needs
|
|
1775
|
+
// a persona.
|
|
1776
|
+
if (sd.bring || sd.change) return rows;
|
|
1662
1777
|
return (
|
|
1663
1778
|
rows +
|
|
1664
1779
|
`<div class="pgh"><span>${esc(t("ag.session"))}</span><span></span></div>` +
|
|
@@ -1670,6 +1785,18 @@ function paintPlist(keepScroll = true) {
|
|
|
1670
1785
|
const l = $d("#ag-persona");
|
|
1671
1786
|
const top = l.scrollTop;
|
|
1672
1787
|
l.innerHTML = plistHtml();
|
|
1788
|
+
// AG-18: the agent's persona now, marked.
|
|
1789
|
+
const cur =
|
|
1790
|
+
sd.change &&
|
|
1791
|
+
[...l.querySelectorAll("[data-pid]")].find(
|
|
1792
|
+
(b) => b.dataset.pid === sd.change.persona,
|
|
1793
|
+
);
|
|
1794
|
+
cur
|
|
1795
|
+
?.querySelector(".pn")
|
|
1796
|
+
?.insertAdjacentHTML(
|
|
1797
|
+
"beforeend",
|
|
1798
|
+
` <span class="tagx" data-current>${esc(t("ag.pc.current"))}</span>`,
|
|
1799
|
+
);
|
|
1673
1800
|
l.scrollTop = keepScroll ? top : 0;
|
|
1674
1801
|
}
|
|
1675
1802
|
|
|
@@ -1703,6 +1830,7 @@ function paintDetails() {
|
|
|
1703
1830
|
*/
|
|
1704
1831
|
function paintRuntime(reset) {
|
|
1705
1832
|
paintRuntimeFields(reset);
|
|
1833
|
+
paintKeep();
|
|
1706
1834
|
// Last: a Name that names a running agent disables Start (AG-10.10b).
|
|
1707
1835
|
paintStartsAs();
|
|
1708
1836
|
}
|
|
@@ -1714,10 +1842,22 @@ function paintRuntimeFields(reset) {
|
|
|
1714
1842
|
const plain = sd.pid === PLAIN;
|
|
1715
1843
|
// Bring in: the conversation's runtime, its own history; no first
|
|
1716
1844
|
// instruction, no Advanced (it keeps what the persona gives).
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
$d("#ag-
|
|
1845
|
+
// Change persona: the agent's runtime and name stay. Advanced (the
|
|
1846
|
+
// model) shows for both (AG-17.1c, AG-18).
|
|
1847
|
+
const keeps = !!sd.bring || !!sd.change;
|
|
1848
|
+
$d("#ag-r-l").hidden = plain || keeps;
|
|
1849
|
+
$d("#ag-task-l").hidden = plain || keeps;
|
|
1850
|
+
$d("#ag-adv").hidden = plain;
|
|
1851
|
+
$d("#ag-name-l").hidden = plain || keeps;
|
|
1852
|
+
if (sd.change) {
|
|
1853
|
+
$d("#ag-r-miss").hidden = true;
|
|
1854
|
+
ready.hidden = true;
|
|
1855
|
+
const p = curPersona();
|
|
1856
|
+
const ok = !!p && (p.id !== sd.change.persona || !!sd.model);
|
|
1857
|
+
go.disabled = !ok;
|
|
1858
|
+
go.dataset.blocked = ok ? "" : "persona";
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1721
1861
|
if (sd.bring) {
|
|
1722
1862
|
$d("#ag-r-miss").hidden = true;
|
|
1723
1863
|
ready.hidden = true;
|
|
@@ -1784,6 +1924,18 @@ function paintAdvanced() {
|
|
|
1784
1924
|
const name = runtimeName(sd.data?.runtimes ?? [], rt);
|
|
1785
1925
|
const def = personaModel();
|
|
1786
1926
|
const models = rtv?.models ?? [];
|
|
1927
|
+
// AG-17.1c, AG-18: the conversation's model stays unless one is picked.
|
|
1928
|
+
const from = sd.bring ? bringPick() : sd.change;
|
|
1929
|
+
if (sd.bring || sd.change) {
|
|
1930
|
+
const keep = from?.model
|
|
1931
|
+
? t("ag.keep.opt", { m: from.model })
|
|
1932
|
+
: t("ag.keep.optNone");
|
|
1933
|
+
body.innerHTML =
|
|
1934
|
+
`<p>${esc(t("ag.keep.advNote"))}</p>` +
|
|
1935
|
+
`<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, keep) : `<option value="">${esc(keep)}</option>`}</select></label>` +
|
|
1936
|
+
`<label class="fld"><span>${esc(t("ag.new.ver"))}</span><select id="ag-ver"${models.length && sd.model ? "" : " disabled"}>${versionOptionsHtml(sd.data, rt, sd.model, sd.ver, t("ag.new.verDef", { v: versionLabel("latest") }))}</select></label></div>`;
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1787
1939
|
const model = sd.model || def.model;
|
|
1788
1940
|
const first = t("ag.new.modelDef", {
|
|
1789
1941
|
// No model of its own: just the version, so the label fits the select.
|
|
@@ -1864,8 +2016,14 @@ async function fillForWorkspace(ws) {
|
|
|
1864
2016
|
return false;
|
|
1865
2017
|
}
|
|
1866
2018
|
if (!(await fillConversations(ws))) return false;
|
|
1867
|
-
if (sd.bring && sd.pid === PLAIN) sd.pid = "";
|
|
2019
|
+
if ((sd.bring || sd.change) && sd.pid === PLAIN) sd.pid = "";
|
|
1868
2020
|
const ps = sd.data?.personas ?? [];
|
|
2021
|
+
// AG-17.1c: the picked conversation's suggested persona, preselected.
|
|
2022
|
+
const sug = sd.bring ? bringPick()?.suggest : null;
|
|
2023
|
+
if (sug && ps.some((p) => p.id === sug)) sd.pid = sug;
|
|
2024
|
+
// AG-18: its persona now (marked; Change stays off until another).
|
|
2025
|
+
if (sd.change && ps.some((p) => p.id === sd.change.persona))
|
|
2026
|
+
sd.pid = sd.change.persona;
|
|
1869
2027
|
if (sd.pid !== PLAIN && !ps.some((p) => p.id === sd.pid)) {
|
|
1870
2028
|
// The first persona the list shows (grouped by category).
|
|
1871
2029
|
const cats = (sd.data?.categories ?? []).map((c) => c.id);
|
|
@@ -1882,19 +2040,41 @@ async function fillForWorkspace(ws) {
|
|
|
1882
2040
|
}
|
|
1883
2041
|
|
|
1884
2042
|
/** The dialog's words for Start, or for Bring in (AG-17.1). */
|
|
1885
|
-
function dialogMode(bring) {
|
|
2043
|
+
function dialogMode(bring, change = null) {
|
|
1886
2044
|
sd.bring = bring ? { list: [], runtimes: [], root: "", pick: "" } : null;
|
|
1887
|
-
|
|
1888
|
-
$d("#ag-
|
|
1889
|
-
|
|
2045
|
+
sd.change = change;
|
|
2046
|
+
$d("#ag-dlg-h").textContent = change
|
|
2047
|
+
? t("ag.pc.title", { id: change.name })
|
|
2048
|
+
: t(bring ? "ag.bring.title" : "ag.new.title");
|
|
2049
|
+
$d("#ag-note").textContent = t(
|
|
2050
|
+
change ? "ag.pc.note" : bring ? "ag.bring.note" : "ag.new.note",
|
|
2051
|
+
);
|
|
2052
|
+
$d("#ag-go").textContent = goLabel();
|
|
1890
2053
|
$d("#ag-conv-f").hidden = !bring;
|
|
1891
2054
|
}
|
|
1892
2055
|
|
|
1893
|
-
|
|
2056
|
+
/** The submit button's words for the dialog's mode. */
|
|
2057
|
+
function goLabel() {
|
|
2058
|
+
return t(sd.change ? "ag.pc.go" : sd.bring ? "ag.bring.go" : "ag.go");
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
/** AG-18: Change persona… for agent row `r` (its persona, model, name). */
|
|
2062
|
+
function openPersonaChange(r) {
|
|
2063
|
+
return openStart(false, {
|
|
2064
|
+
id: r.agent,
|
|
2065
|
+
workspace: r.workspace,
|
|
2066
|
+
persona: r.persona,
|
|
2067
|
+
runtime: r.runtime,
|
|
2068
|
+
model: r.model || "",
|
|
2069
|
+
name: paneName(r),
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
async function openStart(bring = false, change = null) {
|
|
1894
2074
|
const dlg = $d("#ag-dlg");
|
|
1895
2075
|
if (!dlg || dlg.open) return;
|
|
1896
|
-
dialogMode(bring);
|
|
1897
|
-
const shown = viewWorkspaces();
|
|
2076
|
+
dialogMode(bring, change);
|
|
2077
|
+
const shown = change ? [change.workspace] : viewWorkspaces();
|
|
1898
2078
|
const wsSel = $d("#ag-ws");
|
|
1899
2079
|
wsSel.innerHTML = shown
|
|
1900
2080
|
.map((w) => `<option value="${esc(w)}">${esc(w)}</option>`)
|
|
@@ -1989,7 +2169,8 @@ export function nameSlug(name) {
|
|
|
1989
2169
|
function paintStartsAs() {
|
|
1990
2170
|
const el = $d("#ag-as");
|
|
1991
2171
|
if (!el) return;
|
|
1992
|
-
const p =
|
|
2172
|
+
const p =
|
|
2173
|
+
sd.pid && sd.pid !== PLAIN && !sd.bring && !sd.change ? curPersona() : null;
|
|
1993
2174
|
el.hidden = !p;
|
|
1994
2175
|
el.textContent = "";
|
|
1995
2176
|
if (!p) return;
|
|
@@ -2059,6 +2240,7 @@ async function submitStart(form) {
|
|
|
2059
2240
|
const ws = form.querySelector("#ag-ws").value || viewWorkspaces()[0];
|
|
2060
2241
|
if (!ws || !sd.pid) return;
|
|
2061
2242
|
if (sd.bring) return submitBringIn(ws);
|
|
2243
|
+
if (sd.change) return submitPersonaChange();
|
|
2062
2244
|
const bad = sd.extras.find((c) => commandProblem(c));
|
|
2063
2245
|
if (sd.pid !== PLAIN && bad)
|
|
2064
2246
|
return showStartError(new Error(t("pe.badCmd", { c: bad })));
|
|
@@ -2095,11 +2277,73 @@ async function submitStart(form) {
|
|
|
2095
2277
|
showStartError(err);
|
|
2096
2278
|
} finally {
|
|
2097
2279
|
go.disabled = false;
|
|
2098
|
-
go.textContent =
|
|
2280
|
+
go.textContent = goLabel();
|
|
2281
|
+
if (dlg.open) paintRuntime(false);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/**
|
|
2286
|
+
* AG-18: change the agent's persona (the same conversation resumes as the
|
|
2287
|
+
* new one). Mid-turn the server asks: After this turn, or Stop it now.
|
|
2288
|
+
*/
|
|
2289
|
+
async function submitPersonaChange(when) {
|
|
2290
|
+
const dlg = $d("#ag-dlg");
|
|
2291
|
+
const go = $d("#ag-go");
|
|
2292
|
+
const ch = sd.change;
|
|
2293
|
+
const p = curPersona();
|
|
2294
|
+
if (!ch || !p) return;
|
|
2295
|
+
const body = { persona: p.id };
|
|
2296
|
+
if (sd.model) body.model = sd.model;
|
|
2297
|
+
if (sd.model && sd.ver) body.version = sd.ver;
|
|
2298
|
+
if (when) body.when = when;
|
|
2299
|
+
go.disabled = true;
|
|
2300
|
+
go.textContent = t("ag.pc.going");
|
|
2301
|
+
$d("#ag-err").hidden = true;
|
|
2302
|
+
try {
|
|
2303
|
+
const r = await api(
|
|
2304
|
+
"POST",
|
|
2305
|
+
`/api/workspaces/${encodeURIComponent(ch.workspace)}/agents/${encodeURIComponent(ch.id)}/persona`,
|
|
2306
|
+
body,
|
|
2307
|
+
);
|
|
2308
|
+
window.denShell?.toast?.(
|
|
2309
|
+
t(r?.pending ? "ag.pc.pending" : "ag.pc.done", {
|
|
2310
|
+
id: ch.name,
|
|
2311
|
+
p: personaName(p),
|
|
2312
|
+
}),
|
|
2313
|
+
);
|
|
2314
|
+
for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
|
|
2315
|
+
dlg.close?.();
|
|
2316
|
+
await refresh();
|
|
2317
|
+
} catch (err) {
|
|
2318
|
+
if (err?.code === "busy") showBusy();
|
|
2319
|
+
else showStartError(err);
|
|
2320
|
+
} finally {
|
|
2321
|
+
go.textContent = goLabel();
|
|
2099
2322
|
if (dlg.open) paintRuntime(false);
|
|
2100
2323
|
}
|
|
2101
2324
|
}
|
|
2102
2325
|
|
|
2326
|
+
/** It's mid-turn: never cut silently; the person picks (AG-18). */
|
|
2327
|
+
function showBusy() {
|
|
2328
|
+
const el = $d("#ag-err");
|
|
2329
|
+
el.textContent = t("ag.pc.busy") + " ";
|
|
2330
|
+
const b = (id, label, when) => {
|
|
2331
|
+
const x = document.createElement("button");
|
|
2332
|
+
x.type = "button";
|
|
2333
|
+
x.className = "den-btn sm";
|
|
2334
|
+
x.id = id;
|
|
2335
|
+
x.textContent = label;
|
|
2336
|
+
x.addEventListener("click", () => void submitPersonaChange(when));
|
|
2337
|
+
return x;
|
|
2338
|
+
};
|
|
2339
|
+
el.append(
|
|
2340
|
+
b("ag-pc-after", t("ag.pc.after"), "after-turn"),
|
|
2341
|
+
" ",
|
|
2342
|
+
b("ag-pc-now", t("ag.pc.now"), "now"),
|
|
2343
|
+
);
|
|
2344
|
+
el.hidden = false;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2103
2347
|
/** Bring in (AG-17.1): the runtime's resume of the conversation picked. */
|
|
2104
2348
|
async function submitBringIn(ws) {
|
|
2105
2349
|
const dlg = $d("#ag-dlg");
|
|
@@ -2113,7 +2357,13 @@ async function submitBringIn(ws) {
|
|
|
2113
2357
|
const r = await api(
|
|
2114
2358
|
"POST",
|
|
2115
2359
|
`/api/workspaces/${encodeURIComponent(ws)}/conversations/${encodeURIComponent(c.id)}/import`,
|
|
2116
|
-
{
|
|
2360
|
+
{
|
|
2361
|
+
persona: p.id,
|
|
2362
|
+
runtime: c.runtime,
|
|
2363
|
+
// AG-17.1c: the conversation's own model unless one is picked.
|
|
2364
|
+
...(sd.model ? { model: sd.model } : {}),
|
|
2365
|
+
...(sd.model && sd.ver ? { version: sd.ver } : {}),
|
|
2366
|
+
},
|
|
2117
2367
|
);
|
|
2118
2368
|
window.denShell?.toast?.(t("ag.bring.done", { id: r?.agent?.id ?? p.id }));
|
|
2119
2369
|
for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
|
|
@@ -2248,6 +2498,7 @@ function wireStartDialog(root) {
|
|
|
2248
2498
|
sd.model = e.target.value;
|
|
2249
2499
|
sd.ver = "";
|
|
2250
2500
|
paintAdvanced();
|
|
2501
|
+
paintRuntime(false);
|
|
2251
2502
|
} else if (id === "ag-ver") sd.ver = e.target.value;
|
|
2252
2503
|
});
|
|
2253
2504
|
// ↑ ↓ move through the persona list (a radio group).
|
|
@@ -2402,6 +2653,16 @@ export async function mountAgentsPage(root) {
|
|
|
2402
2653
|
const r = rowOf(id);
|
|
2403
2654
|
if (r) void restartRow(r, "resume", b);
|
|
2404
2655
|
} else if (b.dataset.act === "reconnect") void connect(p);
|
|
2656
|
+
else if (b.dataset.act === "more") {
|
|
2657
|
+
const r = rowOf(id);
|
|
2658
|
+
if (!r) return;
|
|
2659
|
+
window.denShell?.openMenu?.(b, {
|
|
2660
|
+
label: t("ag.more", { id: r.agent }),
|
|
2661
|
+
items: [
|
|
2662
|
+
{ label: t("ag.pc.item"), onSelect: () => void openPersonaChange(r) },
|
|
2663
|
+
],
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2405
2666
|
});
|
|
2406
2667
|
root.querySelector("#ag-start").addEventListener("click", () => {
|
|
2407
2668
|
void openStart();
|
|
@@ -226,30 +226,84 @@ export function landingAfterSignIn(loc) {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
|
-
* Sign in with GitHub (SEC-2a):
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
229
|
+
* Sign in with GitHub (SEC-2a): shown only when a client id is configured.
|
|
230
|
+
* The server runs GitHub's device flow; the page shows the code and
|
|
231
|
+
* github.com/login/device, polls the server, and goes on once the owner
|
|
232
|
+
* approved it (the server sets the cookie). Another account is refused by
|
|
233
|
+
* name.
|
|
234
|
+
*
|
|
235
|
+
* SEC-2a-UX: the same three steps in every state (link the owner on the
|
|
236
|
+
* machine, enter the code on GitHub, come back and press the button).
|
|
237
|
+
* Once an owner is linked, step 1 is done and names it, and the button
|
|
238
|
+
* follows the steps. Below them: only the owner's account signs in this
|
|
239
|
+
* way, and Learn more.
|
|
234
240
|
*/
|
|
235
241
|
function githubBlock(s, t) {
|
|
236
242
|
const g = s.github;
|
|
237
243
|
if (!g || s.localhost || g.state === "no_client_id") return "";
|
|
238
244
|
const host = s.machine || "";
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
245
|
+
const ready = g.state === "ready";
|
|
246
|
+
const step1 = withHost(t, "err.signin.ghStep1", {
|
|
247
|
+
host,
|
|
248
|
+
cmd: "\u0000",
|
|
249
|
+
}).replace("\u0000", `<code class="mono">${esc(g.owner)}</code>`);
|
|
245
250
|
return (
|
|
246
|
-
`<div id="den-gh"
|
|
247
|
-
`<
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
`<div id="den-gh">` +
|
|
252
|
+
`<ol class="den-steps" id="den-gh-steps">` +
|
|
253
|
+
`<li id="den-gh-step1"${ready ? ' class="done"' : ""}><p>${step1}</p>` +
|
|
254
|
+
(ready && g.ownerLogin
|
|
255
|
+
? `<p class="den-gh-owner" id="den-gh-owner">${esc(t("err.signin.ghOwner", { login: g.ownerLogin }))}</p>`
|
|
256
|
+
: "") +
|
|
257
|
+
`</li>` +
|
|
258
|
+
`<li id="den-gh-step2"><p>${esc(t("err.signin.ghStep2"))}</p></li>` +
|
|
259
|
+
`<li id="den-gh-step3"><p>${esc(t("err.signin.ghStep3"))}</p></li>` +
|
|
260
|
+
`</ol>` +
|
|
261
|
+
(ready
|
|
262
|
+
? `<div class="acts" style="margin-top:0">` +
|
|
263
|
+
`<button type="button" class="btn primary" id="den-gh-start">${esc(t("err.signin.github"))}</button>` +
|
|
264
|
+
`</div><div id="den-gh-flow" aria-live="polite"></div>`
|
|
265
|
+
: "") +
|
|
266
|
+
`<p class="den-gh-note" id="den-gh-only">${esc(t("err.signin.ghOnly"))}` +
|
|
267
|
+
(g.learnMore
|
|
268
|
+
? ` <a id="den-gh-learn" href="${esc(g.learnMore)}" target="_blank" rel="noopener noreferrer">${esc(t("err.signin.ghLearn"))}</a>`
|
|
269
|
+
: "") +
|
|
270
|
+
`</p></div>`
|
|
250
271
|
);
|
|
251
272
|
}
|
|
252
273
|
|
|
274
|
+
/** How often, and how long, the page watches for an owner (SEC-2a-UX). */
|
|
275
|
+
export const OWNER_POLL_MS = 5000;
|
|
276
|
+
const OWNER_WATCH_MS = 30 * 60 * 1000;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* SEC-2a-UX: with no owner linked yet, watch the session probe; once the
|
|
280
|
+
* owner is linked on the machine, step 1 shows done and Sign in with
|
|
281
|
+
* GitHub appears, with no reload.
|
|
282
|
+
*/
|
|
283
|
+
function watchOwner(root, t, s, pollMs = OWNER_POLL_MS) {
|
|
284
|
+
if (s.github?.state !== "no_owner" || !root.querySelector("#den-gh")) return;
|
|
285
|
+
const until = Date.now() + OWNER_WATCH_MS;
|
|
286
|
+
const tick = () => {
|
|
287
|
+
if (Date.now() > until || !root.querySelector("#den-gh")) return;
|
|
288
|
+
fetch("/api/session", { cache: "no-store", credentials: "same-origin" })
|
|
289
|
+
.then((r) => r.json().catch(() => ({})))
|
|
290
|
+
.then((b) => {
|
|
291
|
+
const g = b?.github;
|
|
292
|
+
if (g?.state === "ready" && g.ownerLogin) {
|
|
293
|
+
s.github = { ...s.github, state: "ready", ownerLogin: g.ownerLogin };
|
|
294
|
+
const old = root.querySelector("#den-gh");
|
|
295
|
+
if (!old) return;
|
|
296
|
+
old.outerHTML = githubBlock(s, t);
|
|
297
|
+
wireGithub(root, t, s);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
setTimeout(tick, pollMs);
|
|
301
|
+
})
|
|
302
|
+
.catch(() => setTimeout(tick, pollMs));
|
|
303
|
+
};
|
|
304
|
+
setTimeout(tick, pollMs);
|
|
305
|
+
}
|
|
306
|
+
|
|
253
307
|
/** The device flow on the page: start, show the code, poll, go on. */
|
|
254
308
|
function wireGithub(root, t, s) {
|
|
255
309
|
const start = root.querySelector("#den-gh-start");
|
|
@@ -604,6 +658,7 @@ function mountSignedOut(root, sh, d) {
|
|
|
604
658
|
`</div>`;
|
|
605
659
|
wireCopy(root, t);
|
|
606
660
|
wireGithub(root, t, s);
|
|
661
|
+
watchOwner(root, t, s, window.__denOwnerPollMs || OWNER_POLL_MS);
|
|
607
662
|
wirePair(root, t, s);
|
|
608
663
|
if (s.expired) return;
|
|
609
664
|
let tried = false;
|