@monster-orange/den 0.4.22 → 0.4.24
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 +73 -0
- package/docs/distributions.md +91 -30
- package/package.json +1 -1
- package/service/install.sh +5 -3
- package/src/shared/agents/guardrails/rotation-hook.ts +5 -0
- package/src/shared/agents/rotate-settings.ts +309 -0
- package/src/shared/agents/rotation.ts +110 -5
- package/src/shared/den-config.ts +27 -12
- package/src/ui/cli/agent-ops.ts +514 -15
- package/src/ui/cli/agents-command.ts +169 -3
- package/src/ui/cli/doctor-command.ts +24 -1
- package/src/ui/cli/doctor-where.ts +59 -13
- package/src/ui/cli/server-agent-ops.ts +2 -2
- package/src/ui/cli/web-command.ts +32 -5
- package/src/ui/cli/web-service.ts +160 -0
- package/src/ui/server/agent-routes.ts +161 -5
- package/src/ui/server/index.ts +13 -5
- package/src/ui/server/public/agents-page.js +214 -22
- package/src/ui/server/public/agents.html +32 -0
- package/src/ui/server/public/combobox.js +150 -0
- package/src/ui/server/public/error-page.js +26 -14
- package/src/ui/server/public/i18n/es.js +47 -4
- package/src/ui/server/public/i18n/ja.js +43 -4
- package/src/ui/server/public/settings-registry.js +318 -0
- package/src/ui/server/public/settings.html +166 -4
- package/src/ui/server/public/shell.js +62 -16
- package/src/ui/server/public/svc-panel.js +54 -15
- package/src/ui/server/route-classes.ts +21 -0
- package/src/ui/server/session-routes.ts +23 -5
- package/src/ui/server/ui-assets.ts +2 -0
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { themeNotice, themeOptions } from "./appearance.js";
|
|
31
31
|
import { qrSvg } from "./qr.js";
|
|
32
32
|
import { renderPersonasSettings } from "./persona-ui.js";
|
|
33
|
+
import { comboHtml, wireCombo } from "./combobox.js";
|
|
33
34
|
|
|
34
35
|
export const DEN_SETTING_CATEGORIES = [
|
|
35
36
|
{
|
|
@@ -889,6 +890,28 @@ export const DEN_SETTINGS = [
|
|
|
889
890
|
read: () => null,
|
|
890
891
|
},
|
|
891
892
|
|
|
893
|
+
{
|
|
894
|
+
id: "agents.rotation",
|
|
895
|
+
category: "agents",
|
|
896
|
+
label: "Rotation",
|
|
897
|
+
description:
|
|
898
|
+
"When an agent's conversation has compacted this many times (or its pull request merged), Den starts it again from its handoff: a fresh conversation, the same name, persona, workspace and folder. Auto asks the agent for its handoff when it is idle, then relaunches it by itself; Ask shows buttons on the agent; Off does nothing. Overrides win over this install's value: agent, persona in a workspace, persona, workspace, service. __DEN_CLI__ agents rotate",
|
|
899
|
+
keywords: [
|
|
900
|
+
"rotation",
|
|
901
|
+
"rotate",
|
|
902
|
+
"relaunch",
|
|
903
|
+
"handoff",
|
|
904
|
+
"compaction",
|
|
905
|
+
"compactions",
|
|
906
|
+
"context",
|
|
907
|
+
"restart",
|
|
908
|
+
"agent",
|
|
909
|
+
],
|
|
910
|
+
scope: "user",
|
|
911
|
+
control: { type: "custom", render: renderRotation },
|
|
912
|
+
read: () => null,
|
|
913
|
+
},
|
|
914
|
+
|
|
892
915
|
{
|
|
893
916
|
id: "agents.personas",
|
|
894
917
|
category: "agents",
|
|
@@ -1253,6 +1276,301 @@ function renderRuntimes(host, ctx) {
|
|
|
1253
1276
|
void load();
|
|
1254
1277
|
}
|
|
1255
1278
|
|
|
1279
|
+
const ROT_SCOPES = [
|
|
1280
|
+
["agent", "Agent", "agent id"],
|
|
1281
|
+
["persona-workspace", "Persona in a workspace", "persona@workspace"],
|
|
1282
|
+
["persona", "Persona", "persona id"],
|
|
1283
|
+
["workspace", "Workspace", "workspace"],
|
|
1284
|
+
["service", "Service", "service, as the workspace names it"],
|
|
1285
|
+
];
|
|
1286
|
+
const ROT_KEYS = [
|
|
1287
|
+
["after_compactions", "After compactions", "a number, or never"],
|
|
1288
|
+
["on_due", "When due", "auto, ask or off"],
|
|
1289
|
+
["on_pr_merged", "When its PR merges", "true or false"],
|
|
1290
|
+
];
|
|
1291
|
+
const rotVal = (v) => (v === undefined ? "" : String(v));
|
|
1292
|
+
/** A value as a person reads it (D30): "never", "Auto", "Relaunch". */
|
|
1293
|
+
function rotValText(k, v) {
|
|
1294
|
+
if (k === "on_due")
|
|
1295
|
+
return { auto: "Auto", ask: "Ask", off: "Off" }[v] ?? rotVal(v);
|
|
1296
|
+
if (k === "on_pr_merged")
|
|
1297
|
+
return v === true ? "Relaunch" : v === false ? "Keep going" : rotVal(v);
|
|
1298
|
+
return rotVal(v);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* D30: an override's target as a name, from the pickers' choices. One that
|
|
1303
|
+
* no longer exists: its id, and gone.
|
|
1304
|
+
*/
|
|
1305
|
+
export function rotTargetName(scope, target, c = {}) {
|
|
1306
|
+
const find = (list, id) => (list || []).find((x) => x.id === id);
|
|
1307
|
+
if (scope === "agent") {
|
|
1308
|
+
const a = find(c.agents, target);
|
|
1309
|
+
return a
|
|
1310
|
+
? { name: a.workspace ? `${a.name} (${a.workspace})` : a.name }
|
|
1311
|
+
: { name: target, gone: true };
|
|
1312
|
+
}
|
|
1313
|
+
if (scope === "persona-workspace") {
|
|
1314
|
+
const [pid, ws] = String(target).split("@");
|
|
1315
|
+
const p = find(c.personas, pid);
|
|
1316
|
+
const w = find(c.workspaces, ws);
|
|
1317
|
+
return p && w
|
|
1318
|
+
? { name: `${p.name} in ${w.name}` }
|
|
1319
|
+
: { name: target, gone: true };
|
|
1320
|
+
}
|
|
1321
|
+
const list =
|
|
1322
|
+
scope === "persona"
|
|
1323
|
+
? c.personas
|
|
1324
|
+
: scope === "workspace"
|
|
1325
|
+
? c.workspaces
|
|
1326
|
+
: c.services;
|
|
1327
|
+
const x = find(list, target);
|
|
1328
|
+
return x ? { name: x.name } : { name: target, gone: true };
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/** D30: each picker's rows, from GET /api/agents/rotation?choices=1. */
|
|
1332
|
+
export function rotPickItems(kind, c = {}) {
|
|
1333
|
+
if (kind === "agent")
|
|
1334
|
+
return (c.agents || []).map((a) => ({
|
|
1335
|
+
id: a.id,
|
|
1336
|
+
label: a.name,
|
|
1337
|
+
sub: [a.workspace, a.state].filter(Boolean).join(" · "),
|
|
1338
|
+
}));
|
|
1339
|
+
if (kind === "persona")
|
|
1340
|
+
return (c.personas || []).map((p) => ({
|
|
1341
|
+
id: p.id,
|
|
1342
|
+
label: p.name,
|
|
1343
|
+
sub: p.category,
|
|
1344
|
+
}));
|
|
1345
|
+
if (kind === "workspace")
|
|
1346
|
+
return (c.workspaces || []).map((w) => ({ id: w.id, label: w.name }));
|
|
1347
|
+
return (c.services || []).map((x) => ({
|
|
1348
|
+
id: x.id,
|
|
1349
|
+
label: x.name,
|
|
1350
|
+
sub: x.workspace,
|
|
1351
|
+
}));
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/** A value typed in the overrides form, as the key takes it. */
|
|
1355
|
+
export function rotationValue(key, raw) {
|
|
1356
|
+
const v = String(raw ?? "").trim();
|
|
1357
|
+
if (key === "after_compactions")
|
|
1358
|
+
return v === "never" || v === "0"
|
|
1359
|
+
? "never"
|
|
1360
|
+
: /^\d+$/.test(v)
|
|
1361
|
+
? Number(v)
|
|
1362
|
+
: v;
|
|
1363
|
+
if (key === "on_pr_merged")
|
|
1364
|
+
return v === "true" ? true : v === "false" ? false : v;
|
|
1365
|
+
return v;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/** The Rotation row's HTML (b: GET /api/agents/rotation). */
|
|
1369
|
+
export function rotationHtml(b) {
|
|
1370
|
+
const inherited = (k) =>
|
|
1371
|
+
b.org?.[k] !== undefined
|
|
1372
|
+
? `org config: ${rotVal(b.org[k])}`
|
|
1373
|
+
: `default: ${rotVal(b.defaults?.[k])}`;
|
|
1374
|
+
const inst = b.install || {};
|
|
1375
|
+
const after = inst.after_compactions;
|
|
1376
|
+
const field = (k, control) =>
|
|
1377
|
+
`<div class="rot-f" data-rot-key="${k}"><label for="rot-${k}">${escR(ROT_KEYS.find((x) => x[0] === k)[1])}</label>${control}<span class="set-tag" data-rot-src>${escR(inst[k] !== undefined ? "this install" : inherited(k))}</span></div>`;
|
|
1378
|
+
const sel = (k, opts) =>
|
|
1379
|
+
`<select id="rot-${k}" data-focus-key="rot-${k}"><option value=""${inst[k] === undefined ? " selected" : ""}>inherit (${escR(inherited(k))})</option>` +
|
|
1380
|
+
opts
|
|
1381
|
+
.map(
|
|
1382
|
+
([v, l]) =>
|
|
1383
|
+
`<option value="${escR(v)}"${rotVal(inst[k]) === v ? " selected" : ""}>${escR(l)}</option>`,
|
|
1384
|
+
)
|
|
1385
|
+
.join("") +
|
|
1386
|
+
`</select>`;
|
|
1387
|
+
const rows = [];
|
|
1388
|
+
for (const [scope, byTarget] of Object.entries(b.overrides || {}))
|
|
1389
|
+
for (const [target, vals] of Object.entries(byTarget || {}))
|
|
1390
|
+
for (const [k, v] of Object.entries(vals || {})) {
|
|
1391
|
+
const who = rotTargetName(scope, target, b.choices);
|
|
1392
|
+
const setting = (ROT_KEYS.find((x) => x[0] === k) || [k, k])[1];
|
|
1393
|
+
rows.push(
|
|
1394
|
+
`<tr data-rot-row${who.gone ? " data-rot-gone" : ""}><td data-l="Scope">${escR((ROT_SCOPES.find((x) => x[0] === scope) || [scope, scope])[1])}</td>` +
|
|
1395
|
+
`<td data-l="For"${who.gone ? ' class="rot-gone"' : ""}>${who.gone ? `<span class="den-mono">${escR(who.name)}</span> (gone)` : escR(who.name)}</td>` +
|
|
1396
|
+
`<td data-l="Setting">${escR(setting)}</td><td data-l="Value">${escR(rotValText(k, v))}</td>` +
|
|
1397
|
+
`<td class="rot-act"><button type="button" class="btn sm" data-rot-clear data-scope="${escR(scope)}" data-target="${escR(target)}" data-key="${escR(k)}" aria-label="${who.gone ? "Remove" : "Clear"} ${escR(setting)} for ${escR(who.name)}">${who.gone ? "Remove" : "Clear"}</button></td></tr>`,
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
return (
|
|
1401
|
+
`<div class="rot" data-rotation-settings>` +
|
|
1402
|
+
`<div class="rot-inst">` +
|
|
1403
|
+
field(
|
|
1404
|
+
"after_compactions",
|
|
1405
|
+
`<input type="text" id="rot-after_compactions" data-focus-key="rot-after_compactions" inputmode="numeric" spellcheck="false" autocomplete="off" placeholder="${escR(inherited("after_compactions"))}" value="${escR(after === undefined ? "" : rotVal(after))}"> <button type="button" class="btn sm" data-rot-save-after>Save</button>`,
|
|
1406
|
+
) +
|
|
1407
|
+
field(
|
|
1408
|
+
"on_due",
|
|
1409
|
+
sel("on_due", [
|
|
1410
|
+
["auto", "Auto"],
|
|
1411
|
+
["ask", "Ask"],
|
|
1412
|
+
["off", "Off"],
|
|
1413
|
+
]),
|
|
1414
|
+
) +
|
|
1415
|
+
field(
|
|
1416
|
+
"on_pr_merged",
|
|
1417
|
+
sel("on_pr_merged", [
|
|
1418
|
+
["true", "Relaunch"],
|
|
1419
|
+
["false", "Keep going"],
|
|
1420
|
+
]),
|
|
1421
|
+
) +
|
|
1422
|
+
`</div>` +
|
|
1423
|
+
`<div class="tablewrap"><table class="svc rot-ov"><caption>Overrides (the most specific wins)</caption><thead><tr><th scope="col">Scope</th><th scope="col">For</th><th scope="col">Setting</th><th scope="col">Value</th><th scope="col" aria-label="Actions"></th></tr></thead><tbody>` +
|
|
1424
|
+
(rows.length
|
|
1425
|
+
? rows.join("")
|
|
1426
|
+
: `<tr><td colspan="5" class="set-note">No overrides: every agent uses this install's values.</td></tr>`) +
|
|
1427
|
+
`</tbody></table></div>` +
|
|
1428
|
+
`<form class="rot-add" data-rot-add>` +
|
|
1429
|
+
`<span class="rot-pick"><label for="rot-a-scope">Scope</label> <select id="rot-a-scope">${ROT_SCOPES.map(([v, l]) => `<option value="${v}">${escR(l)}</option>`).join("")}</select></span>` +
|
|
1430
|
+
// The target: picked by name, never typed as an id (D30).
|
|
1431
|
+
`<span class="rot-pick" data-for="agent">${comboHtml({ id: "rot-a-agent", label: "Agent", placeholder: "Type to find an agent" })}</span>` +
|
|
1432
|
+
`<span class="rot-pick" data-for="persona persona-workspace" hidden>${comboHtml({ id: "rot-a-persona", label: "Persona", placeholder: "Type to find a persona" })}</span>` +
|
|
1433
|
+
`<span class="rot-pick" data-for="workspace persona-workspace" hidden>${comboHtml({ id: "rot-a-ws", label: "Workspace", placeholder: "Type to find a workspace" })}</span>` +
|
|
1434
|
+
`<span class="rot-pick" data-for="service" hidden>${comboHtml({ id: "rot-a-service", label: "Service", placeholder: "Type to find a service" })}</span>` +
|
|
1435
|
+
`<span class="rot-pick"><label for="rot-a-key">Setting</label> <select id="rot-a-key">${ROT_KEYS.map(([v, l]) => `<option value="${v}">${escR(l)}</option>`).join("")}</select></span>` +
|
|
1436
|
+
`<span class="rot-pick" data-val="after_compactions"><label for="rot-a-n">Value</label> <input type="number" id="rot-a-n" min="1" max="100" step="1" value="3"> <label class="rot-never"><input type="checkbox" id="rot-a-never"> never</label></span>` +
|
|
1437
|
+
`<span class="rot-pick" data-val="on_due" hidden><label for="rot-a-due">Value</label> <select id="rot-a-due"><option value="auto">Auto</option><option value="ask">Ask</option><option value="off">Off</option></select></span>` +
|
|
1438
|
+
`<span class="rot-pick" data-val="on_pr_merged" hidden><label for="rot-a-pr">Value</label> <select id="rot-a-pr"><option value="true">Relaunch</option><option value="false">Keep going</option></select></span>` +
|
|
1439
|
+
`<button type="submit" class="btn sm primary">Add override</button></form>` +
|
|
1440
|
+
`<div class="rt-err" role="alert" hidden></div>` +
|
|
1441
|
+
`</div>`
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/** The Rotation row: this install's values, the overrides, + Add override. */
|
|
1446
|
+
function renderRotation(host, ctx) {
|
|
1447
|
+
const err = () => host.querySelector(".rt-err");
|
|
1448
|
+
const fail = (x) => {
|
|
1449
|
+
const e = err();
|
|
1450
|
+
if (e) {
|
|
1451
|
+
e.textContent = x?.message || "Not saved";
|
|
1452
|
+
e.hidden = false;
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
const load = () => {
|
|
1456
|
+
if (ctx.shell?.configInvalid?.() === true) {
|
|
1457
|
+
host.innerHTML = `<div class="set-note">Fix the config first: the org's rotation is read from it.</div>`;
|
|
1458
|
+
return Promise.resolve();
|
|
1459
|
+
}
|
|
1460
|
+
return ctx.shell
|
|
1461
|
+
.api("GET", "/api/agents/rotation?choices=1")
|
|
1462
|
+
.then((b) => {
|
|
1463
|
+
host.innerHTML = rotationHtml(b || {});
|
|
1464
|
+
wireAdd(b?.choices || {});
|
|
1465
|
+
})
|
|
1466
|
+
.catch((x) => {
|
|
1467
|
+
host.innerHTML = `<div class="set-note">${escR(x?.message || "The rotation settings did not load.")}</div>`;
|
|
1468
|
+
});
|
|
1469
|
+
};
|
|
1470
|
+
// D30: the add form's pickers, filled from live data.
|
|
1471
|
+
let picks = {};
|
|
1472
|
+
const wireAdd = (c) => {
|
|
1473
|
+
picks = {};
|
|
1474
|
+
for (const [kind, id] of [
|
|
1475
|
+
["agent", "rot-a-agent"],
|
|
1476
|
+
["persona", "rot-a-persona"],
|
|
1477
|
+
["workspace", "rot-a-ws"],
|
|
1478
|
+
["service", "rot-a-service"],
|
|
1479
|
+
])
|
|
1480
|
+
picks[kind] = wireCombo(host, id, { items: () => rotPickItems(kind, c) });
|
|
1481
|
+
showFor();
|
|
1482
|
+
};
|
|
1483
|
+
const showFor = () => {
|
|
1484
|
+
const scope = host.querySelector("#rot-a-scope")?.value;
|
|
1485
|
+
const key = host.querySelector("#rot-a-key")?.value;
|
|
1486
|
+
for (const el of host.querySelectorAll(".rot-add [data-for]"))
|
|
1487
|
+
el.hidden = !el.dataset.for.split(" ").includes(scope);
|
|
1488
|
+
for (const el of host.querySelectorAll(".rot-add [data-val]"))
|
|
1489
|
+
el.hidden = el.dataset.val !== key;
|
|
1490
|
+
const n = host.querySelector("#rot-a-n");
|
|
1491
|
+
const never = host.querySelector("#rot-a-never");
|
|
1492
|
+
if (n && never) n.disabled = never.checked;
|
|
1493
|
+
};
|
|
1494
|
+
const targetOf = (scope) => {
|
|
1495
|
+
const v = (k) => picks[k]?.value() || "";
|
|
1496
|
+
if (scope === "persona-workspace")
|
|
1497
|
+
return v("persona") && v("workspace")
|
|
1498
|
+
? `${v("persona")}@${v("workspace")}`
|
|
1499
|
+
: "";
|
|
1500
|
+
return v(scope);
|
|
1501
|
+
};
|
|
1502
|
+
const valueOf = (key) => {
|
|
1503
|
+
if (key === "on_due") return host.querySelector("#rot-a-due").value;
|
|
1504
|
+
if (key === "on_pr_merged")
|
|
1505
|
+
return host.querySelector("#rot-a-pr").value === "true";
|
|
1506
|
+
if (host.querySelector("#rot-a-never").checked) return "never";
|
|
1507
|
+
return Number(host.querySelector("#rot-a-n").value);
|
|
1508
|
+
};
|
|
1509
|
+
const put = async (body) => {
|
|
1510
|
+
try {
|
|
1511
|
+
await ctx.shell.api("PUT", "/api/agents/rotation", body);
|
|
1512
|
+
await load();
|
|
1513
|
+
} catch (x) {
|
|
1514
|
+
fail(x);
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
host.addEventListener("change", (e) => {
|
|
1518
|
+
const k = e.target.closest("[data-rot-key]")?.dataset.rotKey;
|
|
1519
|
+
if (
|
|
1520
|
+
["rot-a-scope", "rot-a-key", "rot-a-never"].includes(e.target.id) ||
|
|
1521
|
+
e.target.closest?.(".rot-add")
|
|
1522
|
+
) {
|
|
1523
|
+
showFor();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
if (!k || k === "after_compactions" || e.target.tagName !== "SELECT")
|
|
1527
|
+
return;
|
|
1528
|
+
const v = e.target.value;
|
|
1529
|
+
void put({
|
|
1530
|
+
scope: "install",
|
|
1531
|
+
key: k,
|
|
1532
|
+
value: v === "" ? null : rotationValue(k, v),
|
|
1533
|
+
});
|
|
1534
|
+
});
|
|
1535
|
+
host.addEventListener("click", (e) => {
|
|
1536
|
+
const b = e.target.closest("button");
|
|
1537
|
+
if (!b) return;
|
|
1538
|
+
if (b.hasAttribute("data-rot-save-after")) {
|
|
1539
|
+
const v = host.querySelector("#rot-after_compactions")?.value.trim();
|
|
1540
|
+
void put({
|
|
1541
|
+
scope: "install",
|
|
1542
|
+
key: "after_compactions",
|
|
1543
|
+
value: v ? rotationValue("after_compactions", v) : null,
|
|
1544
|
+
});
|
|
1545
|
+
} else if (b.hasAttribute("data-rot-clear"))
|
|
1546
|
+
void put({
|
|
1547
|
+
scope: b.dataset.scope,
|
|
1548
|
+
target: b.dataset.target,
|
|
1549
|
+
key: b.dataset.key,
|
|
1550
|
+
value: null,
|
|
1551
|
+
});
|
|
1552
|
+
});
|
|
1553
|
+
host.addEventListener("submit", (e) => {
|
|
1554
|
+
const f = e.target.closest("[data-rot-add]");
|
|
1555
|
+
if (!f) return;
|
|
1556
|
+
e.preventDefault();
|
|
1557
|
+
const key = f.querySelector("#rot-a-key").value;
|
|
1558
|
+
const scope = f.querySelector("#rot-a-scope").value;
|
|
1559
|
+
const target = targetOf(scope);
|
|
1560
|
+
if (!target) {
|
|
1561
|
+
fail({
|
|
1562
|
+
message:
|
|
1563
|
+
scope === "persona-workspace"
|
|
1564
|
+
? "Pick a persona and a workspace."
|
|
1565
|
+
: `Pick a ${(ROT_SCOPES.find((x) => x[0] === scope) || [scope, scope])[1].toLowerCase()}.`,
|
|
1566
|
+
});
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
void put({ scope, target, key, value: valueOf(key) });
|
|
1570
|
+
});
|
|
1571
|
+
void load();
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1256
1574
|
/** Register Den's categories and rows with the shell's SETTINGS registry. */
|
|
1257
1575
|
export function registerDenSettings(shell) {
|
|
1258
1576
|
for (const c of DEN_SETTING_CATEGORIES) shell.registerSettingsCategory(c);
|
|
@@ -384,10 +384,9 @@
|
|
|
384
384
|
}
|
|
385
385
|
.rt {
|
|
386
386
|
display: grid;
|
|
387
|
-
grid-template-columns:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
) auto;
|
|
387
|
+
grid-template-columns:
|
|
388
|
+
minmax(0, 1.2fr) minmax(0, 0.8fr) minmax(0, 2fr)
|
|
389
|
+
auto;
|
|
391
390
|
align-items: center;
|
|
392
391
|
gap: 4px 12px;
|
|
393
392
|
padding: 8px 10px;
|
|
@@ -427,6 +426,169 @@
|
|
|
427
426
|
.rt-err[hidden] {
|
|
428
427
|
display: none;
|
|
429
428
|
}
|
|
429
|
+
/* D28: Settings › Agents › Rotation. */
|
|
430
|
+
.rot-inst {
|
|
431
|
+
display: grid;
|
|
432
|
+
gap: 8px;
|
|
433
|
+
margin-bottom: 12px;
|
|
434
|
+
}
|
|
435
|
+
.rot-f,
|
|
436
|
+
.rot-add {
|
|
437
|
+
display: flex;
|
|
438
|
+
flex-wrap: wrap;
|
|
439
|
+
align-items: center;
|
|
440
|
+
gap: 6px;
|
|
441
|
+
min-width: 0;
|
|
442
|
+
}
|
|
443
|
+
.rot-f label {
|
|
444
|
+
flex: 0 0 150px;
|
|
445
|
+
}
|
|
446
|
+
.rot-f input,
|
|
447
|
+
.rot-f select,
|
|
448
|
+
.rot-add input,
|
|
449
|
+
.rot-add select {
|
|
450
|
+
flex: 1 1 140px;
|
|
451
|
+
min-width: 0;
|
|
452
|
+
max-width: 100%;
|
|
453
|
+
}
|
|
454
|
+
.rot-add {
|
|
455
|
+
margin-top: 10px;
|
|
456
|
+
}
|
|
457
|
+
/* D30: the add form: each control with its label, wrapping. */
|
|
458
|
+
.rot-pick {
|
|
459
|
+
display: inline-flex;
|
|
460
|
+
align-items: center;
|
|
461
|
+
gap: 6px;
|
|
462
|
+
min-width: 0;
|
|
463
|
+
flex: 1 1 200px;
|
|
464
|
+
flex-wrap: wrap;
|
|
465
|
+
}
|
|
466
|
+
.rot-pick[hidden] {
|
|
467
|
+
display: none;
|
|
468
|
+
}
|
|
469
|
+
.rot-pick input[type="number"] {
|
|
470
|
+
width: 6em;
|
|
471
|
+
flex: 0 0 auto;
|
|
472
|
+
}
|
|
473
|
+
.rot-never {
|
|
474
|
+
display: inline-flex;
|
|
475
|
+
align-items: center;
|
|
476
|
+
gap: 4px;
|
|
477
|
+
}
|
|
478
|
+
.rot-gone {
|
|
479
|
+
color: var(--muted);
|
|
480
|
+
}
|
|
481
|
+
/* The shared picker (combobox.js). */
|
|
482
|
+
.den-combo {
|
|
483
|
+
display: inline-flex;
|
|
484
|
+
align-items: center;
|
|
485
|
+
gap: 6px;
|
|
486
|
+
min-width: 0;
|
|
487
|
+
flex: 1 1 200px;
|
|
488
|
+
flex-wrap: wrap;
|
|
489
|
+
}
|
|
490
|
+
.den-combo-box {
|
|
491
|
+
position: relative;
|
|
492
|
+
flex: 1 1 160px;
|
|
493
|
+
min-width: 0;
|
|
494
|
+
}
|
|
495
|
+
.den-combo-box input {
|
|
496
|
+
width: 100%;
|
|
497
|
+
box-sizing: border-box;
|
|
498
|
+
}
|
|
499
|
+
.den-combo-list {
|
|
500
|
+
position: absolute;
|
|
501
|
+
z-index: 20;
|
|
502
|
+
left: 0;
|
|
503
|
+
right: 0;
|
|
504
|
+
top: calc(100% + 2px);
|
|
505
|
+
max-height: 240px;
|
|
506
|
+
overflow-y: auto;
|
|
507
|
+
margin: 0;
|
|
508
|
+
padding: 4px;
|
|
509
|
+
list-style: none;
|
|
510
|
+
background: var(--surface);
|
|
511
|
+
border: 1px solid var(--border);
|
|
512
|
+
border-radius: 8px;
|
|
513
|
+
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
|
|
514
|
+
}
|
|
515
|
+
.den-combo-list[hidden] {
|
|
516
|
+
display: none;
|
|
517
|
+
}
|
|
518
|
+
.den-combo-list li {
|
|
519
|
+
display: flex;
|
|
520
|
+
flex-wrap: wrap;
|
|
521
|
+
justify-content: space-between;
|
|
522
|
+
gap: 2px 8px;
|
|
523
|
+
padding: 5px 8px;
|
|
524
|
+
border-radius: 6px;
|
|
525
|
+
cursor: pointer;
|
|
526
|
+
}
|
|
527
|
+
.den-combo-list li[aria-selected="true"] {
|
|
528
|
+
background: var(--sunken);
|
|
529
|
+
}
|
|
530
|
+
.den-combo-list li[aria-disabled="true"] {
|
|
531
|
+
color: var(--muted);
|
|
532
|
+
cursor: default;
|
|
533
|
+
}
|
|
534
|
+
.den-combo-s {
|
|
535
|
+
color: var(--muted);
|
|
536
|
+
font-size: 12px;
|
|
537
|
+
}
|
|
538
|
+
.rot-ov caption {
|
|
539
|
+
text-align: left;
|
|
540
|
+
color: var(--muted);
|
|
541
|
+
font-size: 12.5px;
|
|
542
|
+
padding-bottom: 4px;
|
|
543
|
+
}
|
|
544
|
+
.rot-ov th,
|
|
545
|
+
.rot-ov td {
|
|
546
|
+
padding: 4px 10px 4px 0;
|
|
547
|
+
text-align: left;
|
|
548
|
+
/* Break a key at its "_", never mid-word. */
|
|
549
|
+
overflow-wrap: normal;
|
|
550
|
+
word-break: normal;
|
|
551
|
+
}
|
|
552
|
+
/* Narrow: each override is a card of label/value lines, so no
|
|
553
|
+
column is squeezed into breaking words mid-letter. */
|
|
554
|
+
@media (max-width: 560px) {
|
|
555
|
+
.rot-ov thead {
|
|
556
|
+
position: absolute;
|
|
557
|
+
width: 1px;
|
|
558
|
+
height: 1px;
|
|
559
|
+
overflow: hidden;
|
|
560
|
+
clip: rect(0 0 0 0);
|
|
561
|
+
}
|
|
562
|
+
.rot-ov,
|
|
563
|
+
.rot-ov tbody,
|
|
564
|
+
.rot-ov tr,
|
|
565
|
+
.rot-ov td {
|
|
566
|
+
display: block;
|
|
567
|
+
width: 100%;
|
|
568
|
+
}
|
|
569
|
+
.rot-ov caption {
|
|
570
|
+
display: block;
|
|
571
|
+
}
|
|
572
|
+
.rot-ov tr[data-rot-row] {
|
|
573
|
+
padding: 6px 0;
|
|
574
|
+
border-top: 1px solid var(--border);
|
|
575
|
+
}
|
|
576
|
+
.rot-ov td {
|
|
577
|
+
display: flex;
|
|
578
|
+
gap: 8px;
|
|
579
|
+
padding: 2px 0;
|
|
580
|
+
overflow-wrap: anywhere;
|
|
581
|
+
}
|
|
582
|
+
.rot-ov td[data-l]::before {
|
|
583
|
+
content: attr(data-l);
|
|
584
|
+
flex: 0 0 64px;
|
|
585
|
+
color: var(--muted);
|
|
586
|
+
font-family: inherit;
|
|
587
|
+
}
|
|
588
|
+
.rot-ov td.rot-act {
|
|
589
|
+
padding-top: 4px;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
430
592
|
.rt-fl {
|
|
431
593
|
flex-basis: 100%;
|
|
432
594
|
color: var(--muted);
|
|
@@ -426,9 +426,10 @@
|
|
|
426
426
|
"err.signin.claimOrg":
|
|
427
427
|
"Members of the {org} GitHub org can claim it, in a browser on {host}.",
|
|
428
428
|
"err.signin.claimHere": "This is my computer: connect GitHub",
|
|
429
|
+
"err.signin.claimHereOn": "On {host} itself: connect GitHub",
|
|
430
|
+
"err.signin.claimOrgOnly":
|
|
431
|
+
"Members of the {org} GitHub org can claim it, from a browser on {host}. Open this page on {host}, or set web.owner_github to sign in from anywhere.",
|
|
429
432
|
"err.signin.claimAs": "Sign in as @{login}",
|
|
430
|
-
"err.signin.claimHereNote":
|
|
431
|
-
"This is my computer works only in a browser on {host}. Anywhere else it opens nothing.",
|
|
432
433
|
"err.signin.claimChecking": "Checking this is {host}…",
|
|
433
434
|
"err.signin.githubRefused":
|
|
434
435
|
"@{login} isn't the owner of this {name}. Only the owner's account can use Sign in with GitHub. Use Approve this computer instead.",
|
|
@@ -611,9 +612,48 @@
|
|
|
611
612
|
"Its session rotated ({why}), but it wrote no handoff.md. Resume it to write one, or start fresh.",
|
|
612
613
|
"ag.rot.badHandoff":
|
|
613
614
|
"Its session rotated ({why}), but its handoff.md is not complete. Resume it to fix it, or start fresh.",
|
|
614
|
-
"ag.relaunch": "Relaunch
|
|
615
|
-
"ag.relaunchName": "Relaunch {id}
|
|
615
|
+
"ag.relaunch": "Relaunch now",
|
|
616
|
+
"ag.relaunchName": "Relaunch {id} now",
|
|
616
617
|
"ag.relaunched": "Relaunched {id} from its handoff",
|
|
618
|
+
"ag.rc.autoAsk":
|
|
619
|
+
"Due ({why}). Den asks it for its handoff when it is idle, then relaunches it.",
|
|
620
|
+
"ag.rc.autoReady":
|
|
621
|
+
"Due ({why}). Its handoff is ready: Den relaunches it when it is idle.",
|
|
622
|
+
"ag.rc.ready": "Due ({why}). Its handoff is ready.",
|
|
623
|
+
"ag.rc.missing": "Due ({why}). It has no handoff yet.",
|
|
624
|
+
"ag.rc.bad": "Due ({why}). Its handoff is not complete.",
|
|
625
|
+
"ag.rc.fellBack": "Due ({why}). It wrote no handoff after 2 asks.",
|
|
626
|
+
"ag.rc.wanted":
|
|
627
|
+
"Due ({why}). Den relaunches it once its handoff is written.",
|
|
628
|
+
"ag.rc.write": "Write handoff",
|
|
629
|
+
"ag.rc.writeName": "Ask {id} to write its handoff",
|
|
630
|
+
"ag.rc.keep": "Keep this conversation",
|
|
631
|
+
"ag.rc.keepName": "Keep {id}'s conversation",
|
|
632
|
+
"ag.rc.never": "Don't ask again for this agent",
|
|
633
|
+
"ag.rc.settings": "Settings",
|
|
634
|
+
"ag.rc.from":
|
|
635
|
+
"Rotates after {n} compactions ({src}); when due: {mode} ({src2})",
|
|
636
|
+
"ag.rc.fellBackBad":
|
|
637
|
+
"Due ({why}). Its handoff still fails the check after 2 asks.",
|
|
638
|
+
"ag.rc.from.one":
|
|
639
|
+
"Rotates after {n} compaction ({src}); when due: {mode} ({src2})",
|
|
640
|
+
"ag.rc.fromNever": "Never rotates ({src}); when due: {mode} ({src2})",
|
|
641
|
+
"ag.rc.midTurn": "{id} is mid-turn: ask again when its turn ends",
|
|
642
|
+
"ag.rc.neverN": "never",
|
|
643
|
+
"ag.rc.mode.auto": "relaunch by itself",
|
|
644
|
+
"ag.rc.mode.ask": "ask",
|
|
645
|
+
"ag.rc.mode.off": "nothing",
|
|
646
|
+
"ag.rc.menu": "Rotation settings",
|
|
647
|
+
"ag.rc.noState": "No rotation yet: it counts from its next start.",
|
|
648
|
+
"ag.rc.asked": "Asked {id} for its handoff",
|
|
649
|
+
"ag.rc.askedRelaunch":
|
|
650
|
+
"Asked {id} for its handoff. Den relaunches it once it passes.",
|
|
651
|
+
"ag.rc.askedResumed":
|
|
652
|
+
"Resumed {id} to write its handoff. Den relaunches it once it passes.",
|
|
653
|
+
"ag.rc.kept": "Kept {id}'s conversation: its count starts again",
|
|
654
|
+
"ag.rc.keptNever": "Kept {id}'s conversation: it won't rotate again",
|
|
655
|
+
"ag.rc.autoRelaunched":
|
|
656
|
+
"Relaunched {id} from its handoff after {why}. The old conversation {old} is kept.",
|
|
617
657
|
"ag.interrupted": "{n} agents were interrupted when {name} restarted",
|
|
618
658
|
"ag.interrupted.one": "{n} agent was interrupted when {name} restarted",
|
|
619
659
|
"ag.resumeAll": "Resume all",
|
|
@@ -815,6 +855,7 @@
|
|
|
815
855
|
"ver.pinned": "pinned {v}",
|
|
816
856
|
"ver.usedLatest": "{n} now tracks the latest version",
|
|
817
857
|
"set.agents.personas": "Personas",
|
|
858
|
+
"set.agents.rotation": "Rotation",
|
|
818
859
|
// The Agents tab (AG-4a-term, agents-page.js).
|
|
819
860
|
"n.agents": "Agents",
|
|
820
861
|
"kind.agent": "Agent",
|
|
@@ -2280,28 +2321,31 @@
|
|
|
2280
2321
|
var down = Object.prototype.hasOwnProperty.call(off, id)
|
|
2281
2322
|
? !!off[id]
|
|
2282
2323
|
: state.health[id] === "down";
|
|
2324
|
+
// D27: a real link (never popup-blocked, as window.open was in Safari).
|
|
2325
|
+
var live = !down && /^https?:\/\//i.test(url);
|
|
2283
2326
|
return (
|
|
2284
|
-
'<
|
|
2327
|
+
'<a class="den-pvisit" tabindex="-1" data-visit="' +
|
|
2285
2328
|
esc(id) +
|
|
2286
2329
|
'" data-url="' +
|
|
2287
2330
|
esc(url) +
|
|
2288
|
-
'"
|
|
2331
|
+
'"' +
|
|
2332
|
+
(live
|
|
2333
|
+
? ' href="' + esc(url) + '" target="_blank" rel="noopener noreferrer"'
|
|
2334
|
+
: ' role="link" aria-disabled="true"') +
|
|
2335
|
+
' aria-label="' +
|
|
2289
2336
|
esc(t("act.visitX", { s: id })) +
|
|
2290
2337
|
'" title="' +
|
|
2291
2338
|
esc(down ? t("svp.notRunning") : url) +
|
|
2292
|
-
'"'
|
|
2293
|
-
(down ? ' aria-disabled="true"' : "") +
|
|
2294
|
-
'><span aria-hidden="true">↗</span></button>'
|
|
2339
|
+
'"><span aria-hidden="true">↗</span></a>'
|
|
2295
2340
|
);
|
|
2296
2341
|
}
|
|
2342
|
+
/** A pin's Visit ↗ clicked: true when the link opens by itself. */
|
|
2297
2343
|
function visitPin(b) {
|
|
2298
|
-
if (b.getAttribute("aria-disabled") === "true") {
|
|
2344
|
+
if (b.getAttribute("aria-disabled") === "true" || !b.hasAttribute("href")) {
|
|
2299
2345
|
toast(t("svp.notRunning"));
|
|
2300
|
-
return;
|
|
2346
|
+
return false;
|
|
2301
2347
|
}
|
|
2302
|
-
|
|
2303
|
-
if (url && /^https?:\/\//i.test(url))
|
|
2304
|
-
window.open(url, "_blank", "noopener");
|
|
2348
|
+
return true;
|
|
2305
2349
|
}
|
|
2306
2350
|
// 0.4.18: the service panel (svc-panel.js registers it).
|
|
2307
2351
|
var servicePanel = null;
|
|
@@ -2613,8 +2657,7 @@
|
|
|
2613
2657
|
var tgt = e.target;
|
|
2614
2658
|
var v = tgt && tgt.closest ? tgt.closest("[data-visit]") : null;
|
|
2615
2659
|
if (v) {
|
|
2616
|
-
e.preventDefault();
|
|
2617
|
-
visitPin(v);
|
|
2660
|
+
if (!visitPin(v)) e.preventDefault();
|
|
2618
2661
|
return;
|
|
2619
2662
|
}
|
|
2620
2663
|
// 0.4.18: a plain click (or Enter) on a pinned row opens its panel;
|
|
@@ -4978,6 +5021,9 @@
|
|
|
4978
5021
|
".den-btn.primary { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--on-accent); }",
|
|
4979
5022
|
".den-btn.sm { padding: 2px 9px; font-size: 12.5px; }",
|
|
4980
5023
|
".den-btn:disabled { opacity: .6; cursor: default; }",
|
|
5024
|
+
// D27: Visit is a link styled as a button.
|
|
5025
|
+
"a.den-btn, a.den-pvisit { text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }",
|
|
5026
|
+
"a.den-pvisit { display: grid; }",
|
|
4981
5027
|
// Status drawer (.scrim, .drawer, .prob-card, .grp, .svl: R15)
|
|
4982
5028
|
".den-scrim { position: fixed; inset: 0; background: rgb(0 0 0 / 0.35); z-index: 40; }",
|
|
4983
5029
|
".den-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(420px, 100%); z-index: 41;",
|