@monster-orange/den 0.4.15 → 0.4.17
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 +125 -0
- package/README.md +7 -7
- package/docs/distributions.md +137 -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 +304 -1
- package/src/ui/cli/agents-command.ts +71 -7
- package/src/ui/server/agent-routes.ts +92 -2
- package/src/ui/server/check-pool.ts +72 -0
- package/src/ui/server/github-routes.ts +1 -1
- package/src/ui/server/index.ts +127 -15
- package/src/ui/server/public/agents-page.js +358 -38
- package/src/ui/server/public/agents.html +10 -0
- package/src/ui/server/public/environments.html +223 -140
- package/src/ui/server/public/error-page.js +70 -15
- package/src/ui/server/public/error.html +18 -0
- package/src/ui/server/public/graph.css +18 -0
- package/src/ui/server/public/graph.js +23 -6
- package/src/ui/server/public/i18n/es.js +47 -5
- package/src/ui/server/public/i18n/ja.js +47 -5
- package/src/ui/server/public/index.html +6 -0
- package/src/ui/server/public/local-dev.html +188 -25
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/persona-ui.css +11 -0
- package/src/ui/server/public/release.html +19 -1
- package/src/ui/server/public/shell.js +187 -12
- package/src/ui/server/public/topbar.js +34 -1
- package/src/ui/server/public/view-scope.js +61 -0
- package/src/ui/server/public/workspace-picker.js +2 -0
- package/src/ui/server/route-classes.ts +8 -0
- package/src/ui/server/run-history.ts +181 -0
- package/src/ui/server/session-routes.ts +97 -17
- package/src/ui/server/ui-assets.ts +2 -0
|
@@ -147,6 +147,9 @@ const st = {
|
|
|
147
147
|
signedOut: false,
|
|
148
148
|
open: [],
|
|
149
149
|
panes: new Map(),
|
|
150
|
+
// AG-18: agents (ws\0id) whose persona change started fresh, for the
|
|
151
|
+
// pane's line once it follows them.
|
|
152
|
+
fresh: new Set(),
|
|
150
153
|
timer: null,
|
|
151
154
|
xterm: null,
|
|
152
155
|
csrf: null,
|
|
@@ -326,6 +329,18 @@ function rotationWhy(rot) {
|
|
|
326
329
|
);
|
|
327
330
|
}
|
|
328
331
|
|
|
332
|
+
/** The Workspaces page, focused on workspace `ws` (0.4.17). */
|
|
333
|
+
function wsHref(ws) {
|
|
334
|
+
const nav = window.denShell?.nav?.() ?? [];
|
|
335
|
+
const base = nav.find((n) => n.id === "local-dev")?.href ?? "/local-dev";
|
|
336
|
+
return `${base}?ws=${encodeURIComponent(ws)}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Workspace `ws` as a link to it on Workspaces. */
|
|
340
|
+
function wsLinkHtml(ws) {
|
|
341
|
+
return `<a class="wslink" href="${esc(wsHref(ws))}" title="${esc(t("ag.goWs", { ws }))}">${esc(ws)}</a>`;
|
|
342
|
+
}
|
|
343
|
+
|
|
329
344
|
/** Its rotation, when one is due: "ready" or "due" (den#4). */
|
|
330
345
|
function rotationOf(r) {
|
|
331
346
|
if (!r.rotation?.due) return "";
|
|
@@ -480,7 +495,7 @@ function paintList() {
|
|
|
480
495
|
? groups
|
|
481
496
|
.map(
|
|
482
497
|
(g) =>
|
|
483
|
-
`<div class="wsg" data-ws="${esc(g.ws)}"><div class="wsh"><span>${
|
|
498
|
+
`<div class="wsg" data-ws="${esc(g.ws)}"><div class="wsh"><span>${wsLinkHtml(g.ws)}</span><span>${esc(g.rows.length)}</span></div>` +
|
|
484
499
|
g.rows.map(rowHtml).join("") +
|
|
485
500
|
`</div>`,
|
|
486
501
|
)
|
|
@@ -565,6 +580,7 @@ async function refresh() {
|
|
|
565
580
|
} catch {
|
|
566
581
|
/* the last list stays; the next ask may answer */
|
|
567
582
|
}
|
|
583
|
+
followAgents();
|
|
568
584
|
st.scopeLabel = scopeLabelFor(viewWorkspaces());
|
|
569
585
|
paintList();
|
|
570
586
|
paintSub();
|
|
@@ -572,14 +588,84 @@ async function refresh() {
|
|
|
572
588
|
const strip = st.root.querySelector("#agr > .agtabs");
|
|
573
589
|
if (strip) paintTabs(strip);
|
|
574
590
|
if (!st.root.isConnected) return;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
591
|
+
// A pane whose session just ended may have a new one soon (a persona
|
|
592
|
+
// change, a resume): look again every second for a while.
|
|
593
|
+
const soon = Date.now() < (st.followUntil ?? 0);
|
|
594
|
+
st.timer = setTimeout(
|
|
595
|
+
() => {
|
|
596
|
+
if (!document.hidden) void refresh();
|
|
597
|
+
else st.timer = setTimeout(refresh, REFRESH_MS);
|
|
598
|
+
},
|
|
599
|
+
soon ? FOLLOW_MS : REFRESH_MS,
|
|
600
|
+
);
|
|
579
601
|
}
|
|
580
602
|
|
|
581
603
|
const rowOf = (id) => st.data?.sessions?.find((s) => s.id === id) ?? null;
|
|
582
604
|
|
|
605
|
+
/** How often, and how long, the list is read after a pane's session ends. */
|
|
606
|
+
const FOLLOW_MS = 1_000;
|
|
607
|
+
const FOLLOW_FOR_MS = 15_000;
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* A pane follows its agent, not a session (AG-18): when the agent's
|
|
611
|
+
* session changes (Change persona, Resume, a rotation), the pane moves to
|
|
612
|
+
* the new one in the same slot, its scrollback kept.
|
|
613
|
+
*/
|
|
614
|
+
function followAgents() {
|
|
615
|
+
for (const p of [...st.panes.values()]) {
|
|
616
|
+
const r = rowOf(p.id);
|
|
617
|
+
if (r?.kind === "agent" && r.agent) {
|
|
618
|
+
p.agent = r.agent;
|
|
619
|
+
p.agentWs = r.workspace;
|
|
620
|
+
}
|
|
621
|
+
if (!p.agent || p.closed || r?.state === "running") continue;
|
|
622
|
+
const next = st.data?.sessions?.find(
|
|
623
|
+
(x) =>
|
|
624
|
+
x.kind === "agent" &&
|
|
625
|
+
x.agent === p.agent &&
|
|
626
|
+
x.workspace === p.agentWs &&
|
|
627
|
+
x.id !== p.id &&
|
|
628
|
+
x.state === "running",
|
|
629
|
+
);
|
|
630
|
+
if (next && !st.panes.has(next.id)) retarget(p, next);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** The pane's line for a resume that had nothing to continue (AG-14). */
|
|
635
|
+
function freshLine(p) {
|
|
636
|
+
p.term?.write(`\x1b[2m${t("ag.freshLine")}\x1b[0m\r\n`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Move pane `p` to session row `row`, where it was. */
|
|
640
|
+
function retarget(p, row) {
|
|
641
|
+
const old = p.id;
|
|
642
|
+
const sock = p.sock;
|
|
643
|
+
p.sock = null;
|
|
644
|
+
try {
|
|
645
|
+
sock?.close(1000, "switched");
|
|
646
|
+
} catch {
|
|
647
|
+
/* already closed */
|
|
648
|
+
}
|
|
649
|
+
st.panes.delete(old);
|
|
650
|
+
p.id = row.id;
|
|
651
|
+
st.panes.set(row.id, p);
|
|
652
|
+
st.open = st.open.map((x) => (x === old ? row.id : x));
|
|
653
|
+
if (st.side.delete(old)) st.side.add(row.id);
|
|
654
|
+
if (st.cur === old) st.cur = row.id;
|
|
655
|
+
p.el.dataset.id = row.id;
|
|
656
|
+
p.el.setAttribute("aria-label", row.label ?? row.id);
|
|
657
|
+
p.exited = false;
|
|
658
|
+
p.lost = false;
|
|
659
|
+
p.term?.write(
|
|
660
|
+
`\r\n\x1b[2m${t("ag.switched", { p: row.persona ?? row.agent })}\x1b[0m\r\n`,
|
|
661
|
+
);
|
|
662
|
+
if (st.fresh.delete(`${p.agentWs}\u0000${p.agent}`)) freshLine(p);
|
|
663
|
+
paintBar(p);
|
|
664
|
+
paintLayout();
|
|
665
|
+
writeUrl();
|
|
666
|
+
void connect(p);
|
|
667
|
+
}
|
|
668
|
+
|
|
583
669
|
// ── the panes ───────────────────────────────────────────────────────────
|
|
584
670
|
|
|
585
671
|
async function loadXterm() {
|
|
@@ -704,6 +790,7 @@ function paintLayout() {
|
|
|
704
790
|
if (n === 0) {
|
|
705
791
|
box.className = "agempty";
|
|
706
792
|
delete box.dataset.layout;
|
|
793
|
+
delete box.dataset.ws;
|
|
707
794
|
box.innerHTML = `<div>${esc(t("ag.empty"))}</div>`;
|
|
708
795
|
return;
|
|
709
796
|
}
|
|
@@ -711,6 +798,10 @@ function paintLayout() {
|
|
|
711
798
|
const shown = visibleIds();
|
|
712
799
|
box.className = `agp n${Math.max(1, shown.length)}${layout === "tabs" ? " tabs" : ""}`;
|
|
713
800
|
box.dataset.layout = layout;
|
|
801
|
+
// The front agent's workspace: ⌘K lists it first (0.4.17).
|
|
802
|
+
const front = rowOf(st.cur)?.workspace;
|
|
803
|
+
if (front) box.dataset.ws = front;
|
|
804
|
+
else delete box.dataset.ws;
|
|
714
805
|
const empty = box.querySelector(":scope > div:not(.pane):not(.agtabs)");
|
|
715
806
|
if (empty) empty.remove();
|
|
716
807
|
let strip = box.querySelector(":scope > .agtabs");
|
|
@@ -854,10 +945,14 @@ function paintBar(p) {
|
|
|
854
945
|
const r = rowOf(p.id);
|
|
855
946
|
const bar = p.el.querySelector(".bar2");
|
|
856
947
|
const name = r ? paneName(r) : p.label || p.id;
|
|
857
|
-
// AG-4b: the session's name last, when it says more.
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
948
|
+
// AG-4b: the session's name last, when it says more. The workspace is a
|
|
949
|
+
// link to it on Workspaces (0.4.17).
|
|
950
|
+
const ws = r ? r.workspace : p.workspace || "";
|
|
951
|
+
const rest = r
|
|
952
|
+
? [r.runtime, sessionExtra(r)].filter(Boolean).join(" · ")
|
|
953
|
+
: "";
|
|
954
|
+
const where =
|
|
955
|
+
(ws ? wsLinkHtml(ws) : "") + (ws && rest ? " · " : "") + esc(rest);
|
|
861
956
|
const tone = r ? stateTone(r) : p.exited ? "off" : "pending";
|
|
862
957
|
const stop =
|
|
863
958
|
r?.kind === "agent" && (r.state === "running" || r.state === "stopping")
|
|
@@ -871,13 +966,18 @@ function paintBar(p) {
|
|
|
871
966
|
const reconnect = p.lost
|
|
872
967
|
? `<button type="button" class="den-btn sm" data-act="reconnect">${esc(t("ag.reconnect"))}</button>`
|
|
873
968
|
: "";
|
|
969
|
+
// AG-18: ⋯, its menu: Change persona… (a runtime that can resume).
|
|
970
|
+
const more =
|
|
971
|
+
r?.kind === "agent" && r.personaChange
|
|
972
|
+
? `<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>`
|
|
973
|
+
: "";
|
|
874
974
|
bar.innerHTML =
|
|
875
975
|
`<span class="den-dot ${tone}" aria-hidden="true"></span>` +
|
|
876
|
-
`<b>${esc(name)}</b><span class="where mono">${
|
|
976
|
+
`<b>${esc(name)}</b><span class="where mono">${where}</span>` +
|
|
877
977
|
(r?.tokenRevoked
|
|
878
978
|
? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
|
|
879
979
|
: "") +
|
|
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>`;
|
|
980
|
+
`<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
981
|
// Tabs: the tab's × closes it; the pane's bar has none.
|
|
882
982
|
const x = bar.querySelector('[data-act="close"]');
|
|
883
983
|
if (x) x.hidden = shownLayout() === "tabs";
|
|
@@ -1043,6 +1143,8 @@ async function connect(p) {
|
|
|
1043
1143
|
paintBar(p);
|
|
1044
1144
|
} else if (msg?.type === "exit") {
|
|
1045
1145
|
p.exited = true;
|
|
1146
|
+
// Its agent may continue in a new session (followAgents).
|
|
1147
|
+
st.followUntil = Date.now() + FOLLOW_FOR_MS;
|
|
1046
1148
|
const code =
|
|
1047
1149
|
typeof msg.exitCode === "number" ? ` (${msg.exitCode})` : "";
|
|
1048
1150
|
p.term?.write(`\r\n${t("ag.exitedLine", { code })}\r\n`);
|
|
@@ -1052,21 +1154,41 @@ async function connect(p) {
|
|
|
1052
1154
|
}
|
|
1053
1155
|
p.term?.write(new Uint8Array(e.data));
|
|
1054
1156
|
};
|
|
1055
|
-
sock.onclose = (e) => {
|
|
1157
|
+
sock.onclose = async (e) => {
|
|
1056
1158
|
if (p.sock !== sock) return;
|
|
1057
1159
|
p.sock = null;
|
|
1058
1160
|
if (p.closed) return;
|
|
1059
|
-
// Refused before it opened:
|
|
1060
|
-
//
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1161
|
+
// Refused before it opened: a browser sees only the close, so ask the
|
|
1162
|
+
// route why with a plain GET (0.4.17). "Sign in" only when it's 401.
|
|
1163
|
+
let why = e.reason || String(e.code);
|
|
1164
|
+
if (!opened) {
|
|
1165
|
+
void refresh();
|
|
1166
|
+
why = await refusalReason(p.id);
|
|
1167
|
+
if (p.closed || p.sock) return;
|
|
1168
|
+
}
|
|
1064
1169
|
p.term?.write(`\r\n${t("ag.lost", { why })}\r\n`);
|
|
1065
1170
|
p.lost = true;
|
|
1066
1171
|
paintBar(p);
|
|
1067
1172
|
};
|
|
1068
1173
|
}
|
|
1069
1174
|
|
|
1175
|
+
/** Why session `id`'s io upgrade was refused, in words for its pane. */
|
|
1176
|
+
async function refusalReason(id) {
|
|
1177
|
+
try {
|
|
1178
|
+
const r = await fetch(`/api/sessions/${encodeURIComponent(id)}/io`, {
|
|
1179
|
+
headers: { accept: "application/json" },
|
|
1180
|
+
credentials: "same-origin",
|
|
1181
|
+
});
|
|
1182
|
+
if (r.status === 401) return t("ag.signin", { cli: CLI });
|
|
1183
|
+
const b = await r.json().catch(() => null);
|
|
1184
|
+
if (r.status === 426) return t("ag.refusedUnknown");
|
|
1185
|
+
if (typeof b?.error === "string" && b.error) return b.error;
|
|
1186
|
+
return t("ag.refusedStatus", { status: r.status });
|
|
1187
|
+
} catch {
|
|
1188
|
+
return t("ag.refusedUnknown");
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1070
1192
|
// ── links and keys in a pane (AG-4b) ───────────────────────────────────
|
|
1071
1193
|
|
|
1072
1194
|
/** What Shift+Enter sends to a pane's session (a plain session: Enter). */
|
|
@@ -1255,7 +1377,7 @@ async function openPane(id, { front = true } = {}) {
|
|
|
1255
1377
|
) {
|
|
1256
1378
|
if (e.isComposing) return true;
|
|
1257
1379
|
e.preventDefault();
|
|
1258
|
-
if (softNewlineKey(e)) typeIn(softNewlineOf(id));
|
|
1380
|
+
if (softNewlineKey(e)) typeIn(softNewlineOf(p.id));
|
|
1259
1381
|
return false;
|
|
1260
1382
|
}
|
|
1261
1383
|
return true;
|
|
@@ -1349,9 +1471,13 @@ async function restartRow(r, verb, button) {
|
|
|
1349
1471
|
{ id: r.agent },
|
|
1350
1472
|
),
|
|
1351
1473
|
);
|
|
1352
|
-
|
|
1474
|
+
// Its open pane follows it to the new session (followAgents, in
|
|
1475
|
+
// refresh); one that couldn't (not running yet) is closed for it.
|
|
1353
1476
|
await refresh();
|
|
1354
|
-
|
|
1477
|
+
const next = out?.agent?.sessionId;
|
|
1478
|
+
if (next && next !== r.id && st.panes.has(r.id))
|
|
1479
|
+
closePane(r.id, { quiet: true });
|
|
1480
|
+
if (next) await openPane(next);
|
|
1355
1481
|
} catch (err) {
|
|
1356
1482
|
window.denShell?.errorToast?.(err);
|
|
1357
1483
|
await refresh();
|
|
@@ -1538,6 +1664,9 @@ const sd = {
|
|
|
1538
1664
|
// AG-17.1, Bring in: null for Start; else the workspace's conversations,
|
|
1539
1665
|
// the runtimes that can't bring one in, and the one picked.
|
|
1540
1666
|
bring: null,
|
|
1667
|
+
// AG-18, Change persona: null; else the agent's row (id, workspace,
|
|
1668
|
+
// persona, model) whose persona changes.
|
|
1669
|
+
change: null,
|
|
1541
1670
|
};
|
|
1542
1671
|
|
|
1543
1672
|
/** The conversation picked in Bring in, when it can be brought in. */
|
|
@@ -1625,7 +1754,38 @@ function pickConversation(id) {
|
|
|
1625
1754
|
sd.bring.pick = id;
|
|
1626
1755
|
for (const b of st.root.querySelectorAll("#ag-conv [data-cid]"))
|
|
1627
1756
|
b.setAttribute("aria-checked", String(b.dataset.cid === id));
|
|
1628
|
-
|
|
1757
|
+
// AG-17.1c: its suggested persona, preselected (never submitted).
|
|
1758
|
+
if (c.suggest && (sd.data?.personas ?? []).some((p) => p.id === c.suggest))
|
|
1759
|
+
pickPersona(c.suggest);
|
|
1760
|
+
else paintRuntime(false);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* AG-17.1c, AG-18: what the persona changes, in one short list under it:
|
|
1765
|
+
* the model kept (or the one picked), what it works as from now on, and
|
|
1766
|
+
* for Bring in the MCP servers it won't have.
|
|
1767
|
+
*/
|
|
1768
|
+
function paintKeep() {
|
|
1769
|
+
const el = $d("#ag-keep");
|
|
1770
|
+
if (!el) return;
|
|
1771
|
+
const p = curPersona();
|
|
1772
|
+
const from = sd.bring ? bringPick() : sd.change;
|
|
1773
|
+
el.hidden = !p || !from;
|
|
1774
|
+
if (el.hidden) return void (el.innerHTML = "");
|
|
1775
|
+
const items = [];
|
|
1776
|
+
if (sd.model) items.push(t("ag.keep.modelNew", { m: sd.model }));
|
|
1777
|
+
else if (from.model) items.push(t("ag.keep.model", { m: from.model }));
|
|
1778
|
+
items.push(
|
|
1779
|
+
t("ag.keep.from", {
|
|
1780
|
+
p: personaName(p),
|
|
1781
|
+
mode: t(`pm.${p.permission || "ask"}`),
|
|
1782
|
+
}),
|
|
1783
|
+
);
|
|
1784
|
+
if (sd.bring && from.lost?.length)
|
|
1785
|
+
items.push(t("ag.keep.lost", { s: from.lost.join(", ") }));
|
|
1786
|
+
el.innerHTML = items
|
|
1787
|
+
.map((x, i) => `<li data-keep="${i}">${esc(x)}</li>`)
|
|
1788
|
+
.join("");
|
|
1629
1789
|
}
|
|
1630
1790
|
|
|
1631
1791
|
const $d = (sel) => st.root.querySelector(sel);
|
|
@@ -1636,6 +1796,10 @@ const curPersona = () =>
|
|
|
1636
1796
|
|
|
1637
1797
|
/** The runtime the dialog will start: the one picked, else the persona's. */
|
|
1638
1798
|
function chosenRuntime() {
|
|
1799
|
+
// Change persona and Bring in: the agent's, or the conversation's.
|
|
1800
|
+
if (sd.change?.runtime) return sd.change.runtime;
|
|
1801
|
+
const conv = sd.bring ? bringPick() : null;
|
|
1802
|
+
if (conv?.runtime) return conv.runtime;
|
|
1639
1803
|
const p = curPersona();
|
|
1640
1804
|
return $d("#ag-runtime").value || personaRuntime(sd.data, p);
|
|
1641
1805
|
}
|
|
@@ -1657,8 +1821,9 @@ function plistHtml() {
|
|
|
1657
1821
|
const rows = personaListHtml(sd.f, ps, cats, (p) =>
|
|
1658
1822
|
personaRowHtml(p, p.id === sd.pid, sd.data),
|
|
1659
1823
|
);
|
|
1660
|
-
// Bring in continues a conversation:
|
|
1661
|
-
|
|
1824
|
+
// Bring in continues a conversation, and a change keeps one: each needs
|
|
1825
|
+
// a persona.
|
|
1826
|
+
if (sd.bring || sd.change) return rows;
|
|
1662
1827
|
return (
|
|
1663
1828
|
rows +
|
|
1664
1829
|
`<div class="pgh"><span>${esc(t("ag.session"))}</span><span></span></div>` +
|
|
@@ -1670,6 +1835,18 @@ function paintPlist(keepScroll = true) {
|
|
|
1670
1835
|
const l = $d("#ag-persona");
|
|
1671
1836
|
const top = l.scrollTop;
|
|
1672
1837
|
l.innerHTML = plistHtml();
|
|
1838
|
+
// AG-18: the agent's persona now, marked.
|
|
1839
|
+
const cur =
|
|
1840
|
+
sd.change &&
|
|
1841
|
+
[...l.querySelectorAll("[data-pid]")].find(
|
|
1842
|
+
(b) => b.dataset.pid === sd.change.persona,
|
|
1843
|
+
);
|
|
1844
|
+
cur
|
|
1845
|
+
?.querySelector(".pn")
|
|
1846
|
+
?.insertAdjacentHTML(
|
|
1847
|
+
"beforeend",
|
|
1848
|
+
` <span class="tagx" data-current>${esc(t("ag.pc.current"))}</span>`,
|
|
1849
|
+
);
|
|
1673
1850
|
l.scrollTop = keepScroll ? top : 0;
|
|
1674
1851
|
}
|
|
1675
1852
|
|
|
@@ -1703,6 +1880,7 @@ function paintDetails() {
|
|
|
1703
1880
|
*/
|
|
1704
1881
|
function paintRuntime(reset) {
|
|
1705
1882
|
paintRuntimeFields(reset);
|
|
1883
|
+
paintKeep();
|
|
1706
1884
|
// Last: a Name that names a running agent disables Start (AG-10.10b).
|
|
1707
1885
|
paintStartsAs();
|
|
1708
1886
|
}
|
|
@@ -1714,10 +1892,22 @@ function paintRuntimeFields(reset) {
|
|
|
1714
1892
|
const plain = sd.pid === PLAIN;
|
|
1715
1893
|
// Bring in: the conversation's runtime, its own history; no first
|
|
1716
1894
|
// instruction, no Advanced (it keeps what the persona gives).
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
$d("#ag-
|
|
1895
|
+
// Change persona: the agent's runtime and name stay. Advanced (the
|
|
1896
|
+
// model) shows for both (AG-17.1c, AG-18).
|
|
1897
|
+
const keeps = !!sd.bring || !!sd.change;
|
|
1898
|
+
$d("#ag-r-l").hidden = plain || keeps;
|
|
1899
|
+
$d("#ag-task-l").hidden = plain || keeps;
|
|
1900
|
+
$d("#ag-adv").hidden = plain;
|
|
1901
|
+
$d("#ag-name-l").hidden = plain || keeps;
|
|
1902
|
+
if (sd.change) {
|
|
1903
|
+
$d("#ag-r-miss").hidden = true;
|
|
1904
|
+
ready.hidden = true;
|
|
1905
|
+
const p = curPersona();
|
|
1906
|
+
const ok = !!p && (p.id !== sd.change.persona || !!sd.model);
|
|
1907
|
+
go.disabled = !ok;
|
|
1908
|
+
go.dataset.blocked = ok ? "" : "persona";
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1721
1911
|
if (sd.bring) {
|
|
1722
1912
|
$d("#ag-r-miss").hidden = true;
|
|
1723
1913
|
ready.hidden = true;
|
|
@@ -1784,6 +1974,18 @@ function paintAdvanced() {
|
|
|
1784
1974
|
const name = runtimeName(sd.data?.runtimes ?? [], rt);
|
|
1785
1975
|
const def = personaModel();
|
|
1786
1976
|
const models = rtv?.models ?? [];
|
|
1977
|
+
// AG-17.1c, AG-18: the conversation's model stays unless one is picked.
|
|
1978
|
+
const from = sd.bring ? bringPick() : sd.change;
|
|
1979
|
+
if (sd.bring || sd.change) {
|
|
1980
|
+
const keep = from?.model
|
|
1981
|
+
? t("ag.keep.opt", { m: from.model })
|
|
1982
|
+
: t("ag.keep.optNone");
|
|
1983
|
+
body.innerHTML =
|
|
1984
|
+
`<p>${esc(t("ag.keep.advNote"))}</p>` +
|
|
1985
|
+
`<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>` +
|
|
1986
|
+
`<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>`;
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1787
1989
|
const model = sd.model || def.model;
|
|
1788
1990
|
const first = t("ag.new.modelDef", {
|
|
1789
1991
|
// No model of its own: just the version, so the label fits the select.
|
|
@@ -1864,8 +2066,14 @@ async function fillForWorkspace(ws) {
|
|
|
1864
2066
|
return false;
|
|
1865
2067
|
}
|
|
1866
2068
|
if (!(await fillConversations(ws))) return false;
|
|
1867
|
-
if (sd.bring && sd.pid === PLAIN) sd.pid = "";
|
|
2069
|
+
if ((sd.bring || sd.change) && sd.pid === PLAIN) sd.pid = "";
|
|
1868
2070
|
const ps = sd.data?.personas ?? [];
|
|
2071
|
+
// AG-17.1c: the picked conversation's suggested persona, preselected.
|
|
2072
|
+
const sug = sd.bring ? bringPick()?.suggest : null;
|
|
2073
|
+
if (sug && ps.some((p) => p.id === sug)) sd.pid = sug;
|
|
2074
|
+
// AG-18: its persona now (marked; Change stays off until another).
|
|
2075
|
+
if (sd.change && ps.some((p) => p.id === sd.change.persona))
|
|
2076
|
+
sd.pid = sd.change.persona;
|
|
1869
2077
|
if (sd.pid !== PLAIN && !ps.some((p) => p.id === sd.pid)) {
|
|
1870
2078
|
// The first persona the list shows (grouped by category).
|
|
1871
2079
|
const cats = (sd.data?.categories ?? []).map((c) => c.id);
|
|
@@ -1882,19 +2090,41 @@ async function fillForWorkspace(ws) {
|
|
|
1882
2090
|
}
|
|
1883
2091
|
|
|
1884
2092
|
/** The dialog's words for Start, or for Bring in (AG-17.1). */
|
|
1885
|
-
function dialogMode(bring) {
|
|
2093
|
+
function dialogMode(bring, change = null) {
|
|
1886
2094
|
sd.bring = bring ? { list: [], runtimes: [], root: "", pick: "" } : null;
|
|
1887
|
-
|
|
1888
|
-
$d("#ag-
|
|
1889
|
-
|
|
2095
|
+
sd.change = change;
|
|
2096
|
+
$d("#ag-dlg-h").textContent = change
|
|
2097
|
+
? t("ag.pc.title", { id: change.name })
|
|
2098
|
+
: t(bring ? "ag.bring.title" : "ag.new.title");
|
|
2099
|
+
$d("#ag-note").textContent = t(
|
|
2100
|
+
change ? "ag.pc.note" : bring ? "ag.bring.note" : "ag.new.note",
|
|
2101
|
+
);
|
|
2102
|
+
$d("#ag-go").textContent = goLabel();
|
|
1890
2103
|
$d("#ag-conv-f").hidden = !bring;
|
|
1891
2104
|
}
|
|
1892
2105
|
|
|
1893
|
-
|
|
2106
|
+
/** The submit button's words for the dialog's mode. */
|
|
2107
|
+
function goLabel() {
|
|
2108
|
+
return t(sd.change ? "ag.pc.go" : sd.bring ? "ag.bring.go" : "ag.go");
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
/** AG-18: Change persona… for agent row `r` (its persona, model, name). */
|
|
2112
|
+
function openPersonaChange(r) {
|
|
2113
|
+
return openStart(false, {
|
|
2114
|
+
id: r.agent,
|
|
2115
|
+
workspace: r.workspace,
|
|
2116
|
+
persona: r.persona,
|
|
2117
|
+
runtime: r.runtime,
|
|
2118
|
+
model: r.model || "",
|
|
2119
|
+
name: paneName(r),
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
async function openStart(bring = false, change = null) {
|
|
1894
2124
|
const dlg = $d("#ag-dlg");
|
|
1895
2125
|
if (!dlg || dlg.open) return;
|
|
1896
|
-
dialogMode(bring);
|
|
1897
|
-
const shown = viewWorkspaces();
|
|
2126
|
+
dialogMode(bring, change);
|
|
2127
|
+
const shown = change ? [change.workspace] : viewWorkspaces();
|
|
1898
2128
|
const wsSel = $d("#ag-ws");
|
|
1899
2129
|
wsSel.innerHTML = shown
|
|
1900
2130
|
.map((w) => `<option value="${esc(w)}">${esc(w)}</option>`)
|
|
@@ -1989,7 +2219,8 @@ export function nameSlug(name) {
|
|
|
1989
2219
|
function paintStartsAs() {
|
|
1990
2220
|
const el = $d("#ag-as");
|
|
1991
2221
|
if (!el) return;
|
|
1992
|
-
const p =
|
|
2222
|
+
const p =
|
|
2223
|
+
sd.pid && sd.pid !== PLAIN && !sd.bring && !sd.change ? curPersona() : null;
|
|
1993
2224
|
el.hidden = !p;
|
|
1994
2225
|
el.textContent = "";
|
|
1995
2226
|
if (!p) return;
|
|
@@ -2059,6 +2290,7 @@ async function submitStart(form) {
|
|
|
2059
2290
|
const ws = form.querySelector("#ag-ws").value || viewWorkspaces()[0];
|
|
2060
2291
|
if (!ws || !sd.pid) return;
|
|
2061
2292
|
if (sd.bring) return submitBringIn(ws);
|
|
2293
|
+
if (sd.change) return submitPersonaChange();
|
|
2062
2294
|
const bad = sd.extras.find((c) => commandProblem(c));
|
|
2063
2295
|
if (sd.pid !== PLAIN && bad)
|
|
2064
2296
|
return showStartError(new Error(t("pe.badCmd", { c: bad })));
|
|
@@ -2095,11 +2327,82 @@ async function submitStart(form) {
|
|
|
2095
2327
|
showStartError(err);
|
|
2096
2328
|
} finally {
|
|
2097
2329
|
go.disabled = false;
|
|
2098
|
-
go.textContent =
|
|
2330
|
+
go.textContent = goLabel();
|
|
2331
|
+
if (dlg.open) paintRuntime(false);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
/**
|
|
2336
|
+
* AG-18: change the agent's persona (the same conversation resumes as the
|
|
2337
|
+
* new one). Mid-turn the server asks: After this turn, or Stop it now.
|
|
2338
|
+
*/
|
|
2339
|
+
async function submitPersonaChange(when) {
|
|
2340
|
+
const dlg = $d("#ag-dlg");
|
|
2341
|
+
const go = $d("#ag-go");
|
|
2342
|
+
const ch = sd.change;
|
|
2343
|
+
const p = curPersona();
|
|
2344
|
+
if (!ch || !p) return;
|
|
2345
|
+
const body = { persona: p.id };
|
|
2346
|
+
if (sd.model) body.model = sd.model;
|
|
2347
|
+
if (sd.model && sd.ver) body.version = sd.ver;
|
|
2348
|
+
if (when) body.when = when;
|
|
2349
|
+
go.disabled = true;
|
|
2350
|
+
go.textContent = t("ag.pc.going");
|
|
2351
|
+
$d("#ag-err").hidden = true;
|
|
2352
|
+
try {
|
|
2353
|
+
const r = await api(
|
|
2354
|
+
"POST",
|
|
2355
|
+
`/api/workspaces/${encodeURIComponent(ch.workspace)}/agents/${encodeURIComponent(ch.id)}/persona`,
|
|
2356
|
+
body,
|
|
2357
|
+
);
|
|
2358
|
+
window.denShell?.toast?.(
|
|
2359
|
+
t(
|
|
2360
|
+
r?.pending ? "ag.pc.pending" : r?.fresh ? "ag.pc.fresh" : "ag.pc.done",
|
|
2361
|
+
{ id: ch.name, p: personaName(p) },
|
|
2362
|
+
),
|
|
2363
|
+
);
|
|
2364
|
+
// Never prompted: nothing was resumed. Its pane says so, now if it
|
|
2365
|
+
// has already moved to the new session, else when it does.
|
|
2366
|
+
if (r?.fresh) {
|
|
2367
|
+
const pane = [...st.panes.values()].find(
|
|
2368
|
+
(q) => q.agent === ch.id && q.agentWs === ch.workspace,
|
|
2369
|
+
);
|
|
2370
|
+
if (pane && pane.id === r.agent?.sessionId) freshLine(pane);
|
|
2371
|
+
else st.fresh.add(`${ch.workspace}\u0000${ch.id}`);
|
|
2372
|
+
}
|
|
2373
|
+
for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
|
|
2374
|
+
dlg.close?.();
|
|
2375
|
+
await refresh();
|
|
2376
|
+
} catch (err) {
|
|
2377
|
+
if (err?.code === "busy") showBusy();
|
|
2378
|
+
else showStartError(err);
|
|
2379
|
+
} finally {
|
|
2380
|
+
go.textContent = goLabel();
|
|
2099
2381
|
if (dlg.open) paintRuntime(false);
|
|
2100
2382
|
}
|
|
2101
2383
|
}
|
|
2102
2384
|
|
|
2385
|
+
/** It's mid-turn: never cut silently; the person picks (AG-18). */
|
|
2386
|
+
function showBusy() {
|
|
2387
|
+
const el = $d("#ag-err");
|
|
2388
|
+
el.textContent = t("ag.pc.busy") + " ";
|
|
2389
|
+
const b = (id, label, when) => {
|
|
2390
|
+
const x = document.createElement("button");
|
|
2391
|
+
x.type = "button";
|
|
2392
|
+
x.className = "den-btn sm";
|
|
2393
|
+
x.id = id;
|
|
2394
|
+
x.textContent = label;
|
|
2395
|
+
x.addEventListener("click", () => void submitPersonaChange(when));
|
|
2396
|
+
return x;
|
|
2397
|
+
};
|
|
2398
|
+
el.append(
|
|
2399
|
+
b("ag-pc-after", t("ag.pc.after"), "after-turn"),
|
|
2400
|
+
" ",
|
|
2401
|
+
b("ag-pc-now", t("ag.pc.now"), "now"),
|
|
2402
|
+
);
|
|
2403
|
+
el.hidden = false;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2103
2406
|
/** Bring in (AG-17.1): the runtime's resume of the conversation picked. */
|
|
2104
2407
|
async function submitBringIn(ws) {
|
|
2105
2408
|
const dlg = $d("#ag-dlg");
|
|
@@ -2113,7 +2416,13 @@ async function submitBringIn(ws) {
|
|
|
2113
2416
|
const r = await api(
|
|
2114
2417
|
"POST",
|
|
2115
2418
|
`/api/workspaces/${encodeURIComponent(ws)}/conversations/${encodeURIComponent(c.id)}/import`,
|
|
2116
|
-
{
|
|
2419
|
+
{
|
|
2420
|
+
persona: p.id,
|
|
2421
|
+
runtime: c.runtime,
|
|
2422
|
+
// AG-17.1c: the conversation's own model unless one is picked.
|
|
2423
|
+
...(sd.model ? { model: sd.model } : {}),
|
|
2424
|
+
...(sd.model && sd.ver ? { version: sd.ver } : {}),
|
|
2425
|
+
},
|
|
2117
2426
|
);
|
|
2118
2427
|
window.denShell?.toast?.(t("ag.bring.done", { id: r?.agent?.id ?? p.id }));
|
|
2119
2428
|
for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
|
|
@@ -2248,6 +2557,7 @@ function wireStartDialog(root) {
|
|
|
2248
2557
|
sd.model = e.target.value;
|
|
2249
2558
|
sd.ver = "";
|
|
2250
2559
|
paintAdvanced();
|
|
2560
|
+
paintRuntime(false);
|
|
2251
2561
|
} else if (id === "ag-ver") sd.ver = e.target.value;
|
|
2252
2562
|
});
|
|
2253
2563
|
// ↑ ↓ move through the persona list (a radio group).
|
|
@@ -2402,6 +2712,16 @@ export async function mountAgentsPage(root) {
|
|
|
2402
2712
|
const r = rowOf(id);
|
|
2403
2713
|
if (r) void restartRow(r, "resume", b);
|
|
2404
2714
|
} else if (b.dataset.act === "reconnect") void connect(p);
|
|
2715
|
+
else if (b.dataset.act === "more") {
|
|
2716
|
+
const r = rowOf(id);
|
|
2717
|
+
if (!r) return;
|
|
2718
|
+
window.denShell?.openMenu?.(b, {
|
|
2719
|
+
label: t("ag.more", { id: r.agent }),
|
|
2720
|
+
items: [
|
|
2721
|
+
{ label: t("ag.pc.item"), onSelect: () => void openPersonaChange(r) },
|
|
2722
|
+
],
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2405
2725
|
});
|
|
2406
2726
|
root.querySelector("#ag-start").addEventListener("click", () => {
|
|
2407
2727
|
void openStart();
|
|
@@ -412,6 +412,15 @@
|
|
|
412
412
|
flex: none;
|
|
413
413
|
white-space: nowrap;
|
|
414
414
|
}
|
|
415
|
+
.wslink {
|
|
416
|
+
color: inherit;
|
|
417
|
+
text-decoration: none;
|
|
418
|
+
}
|
|
419
|
+
.wslink:hover,
|
|
420
|
+
.wslink:focus-visible {
|
|
421
|
+
color: var(--text);
|
|
422
|
+
text-decoration: underline;
|
|
423
|
+
}
|
|
415
424
|
.pane .bar2 .where {
|
|
416
425
|
flex: 0 1 auto;
|
|
417
426
|
min-width: 0;
|
|
@@ -701,6 +710,7 @@
|
|
|
701
710
|
aria-label="Persona"
|
|
702
711
|
></div>
|
|
703
712
|
<div id="ag-detrow"></div>
|
|
713
|
+
<ul class="den-keep" id="ag-keep" role="status" hidden></ul>
|
|
704
714
|
</div>
|
|
705
715
|
<div class="two">
|
|
706
716
|
<label class="fld" id="ag-r-l"
|