@monster-orange/den 0.4.26 → 0.4.28
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 +88 -0
- package/docs/distributions.md +145 -20
- package/package.json +1 -1
- package/src/shared/agents/claude-helpers.ts +623 -0
- package/src/shared/agents/den-claude.ts +54 -0
- package/src/shared/agents/helpers.ts +175 -0
- package/src/shared/agents/runtime.ts +15 -2
- package/src/shared/den-config.ts +42 -1
- package/src/shared/hub.ts +314 -82
- package/src/shared/secrets/onepassword.ts +52 -15
- package/src/ui/cli/agent-ops.ts +161 -3
- package/src/ui/cli/agents-command.ts +127 -1
- package/src/ui/cli/doctor-command.ts +11 -2
- package/src/ui/mcp/api.ts +8 -2
- package/src/ui/mcp/server.ts +13 -1
- package/src/ui/mcp/tools.ts +50 -0
- package/src/ui/server/agent-routes.ts +133 -1
- package/src/ui/server/hub-connect.ts +499 -0
- package/src/ui/server/index.ts +7 -1
- package/src/ui/server/public/agent-helpers.js +311 -0
- package/src/ui/server/public/agents-page.js +76 -9
- package/src/ui/server/public/i18n/es.js +41 -0
- package/src/ui/server/public/i18n/ja.js +39 -0
- package/src/ui/server/public/settings-registry.js +232 -33
- package/src/ui/server/public/settings.html +23 -0
- package/src/ui/server/public/shell.js +201 -0
- package/src/ui/server/public/topbar.js +5 -1
- package/src/ui/server/public/transcript-view.js +202 -0
- package/src/ui/server/request-guard.ts +3 -1
- package/src/ui/server/route-classes.ts +33 -0
- package/src/ui/server/session-routes.ts +71 -1
- package/src/ui/server/ui-assets.ts +3 -0
|
@@ -917,8 +917,19 @@ export const DEN_SETTINGS = [
|
|
|
917
917
|
category: "agents",
|
|
918
918
|
label: "Hub",
|
|
919
919
|
description:
|
|
920
|
-
"Every agent gets Hub's tools: search Hub, publish docs and mocks there. Its team
|
|
921
|
-
keywords: [
|
|
920
|
+
"Connect this install to your company's Hub: sign in there and approve it. Every agent then gets Hub's tools: search Hub, publish docs and mocks there. Its team comes from this install's token, its project from the workspace's repos.",
|
|
921
|
+
keywords: [
|
|
922
|
+
"hub",
|
|
923
|
+
"den hub",
|
|
924
|
+
"connect",
|
|
925
|
+
"disconnect",
|
|
926
|
+
"mcp",
|
|
927
|
+
"team",
|
|
928
|
+
"company",
|
|
929
|
+
"token",
|
|
930
|
+
"docs",
|
|
931
|
+
"mocks",
|
|
932
|
+
],
|
|
922
933
|
scope: "user",
|
|
923
934
|
control: { type: "custom", render: renderHub },
|
|
924
935
|
keys: ["hub_allow_tools"],
|
|
@@ -1459,20 +1470,82 @@ export function rotationHtml(b) {
|
|
|
1459
1470
|
);
|
|
1460
1471
|
}
|
|
1461
1472
|
|
|
1462
|
-
/** HUB-1.
|
|
1463
|
-
|
|
1464
|
-
|
|
1473
|
+
/** HUB-1.7: what came back from Hub's redirect (?hub=…), when the server has no words for it. */
|
|
1474
|
+
const HUB_FLASH = {
|
|
1475
|
+
connected: [true, "Connected to Hub."],
|
|
1476
|
+
denied: [false, "Not connected: the request was declined on Hub."],
|
|
1477
|
+
expired: [
|
|
1478
|
+
false,
|
|
1479
|
+
"That connect link was used or too old: press Connect again.",
|
|
1480
|
+
],
|
|
1481
|
+
failed: [false, "Connect didn't finish: press Connect again."],
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* HUB-1.5/1.7: Settings › Agents › Hub (b: GET /api/hub; flash: what the
|
|
1486
|
+
* last Connect came to). Not connected: Hub's address and Connect.
|
|
1487
|
+
* Connected: the company (a link), role, who signed in, this install,
|
|
1488
|
+
* Disconnect. A token Hub refuses offers Reconnect; a role that no longer
|
|
1489
|
+
* may connect says to ask an admin (never a retry).
|
|
1490
|
+
*/
|
|
1491
|
+
export function hubHtml(b, flash) {
|
|
1492
|
+
b = b || { configured: false };
|
|
1493
|
+
const state = b.configured
|
|
1494
|
+
? b.state || (b.connected ? "connected" : "no_token")
|
|
1495
|
+
: "no_token";
|
|
1496
|
+
const note = (t, attr) => `<p class="set-note" ${attr}>${escR(t)}</p>`;
|
|
1497
|
+
const flashHtml = flash
|
|
1498
|
+
? `<p class="set-note hub-flash ${flash.ok ? "ok" : "warn"}" role="status" data-hub-flash="${flash.ok ? "ok" : "failed"}">${escR(flash.message)}</p>`
|
|
1499
|
+
: "";
|
|
1500
|
+
if (state === "bad_config")
|
|
1501
|
+
return (
|
|
1502
|
+
flashHtml +
|
|
1503
|
+
note(b.problem || "hub in user.yml is not valid", "data-hub-problem")
|
|
1504
|
+
);
|
|
1505
|
+
if (state === "no_token")
|
|
1506
|
+
return (
|
|
1507
|
+
flashHtml +
|
|
1508
|
+
`<form class="hub-connect" data-hub-connect>` +
|
|
1509
|
+
`<label for="hub-url">Hub's address</label> ` +
|
|
1510
|
+
`<input type="url" id="hub-url" class="den-mono" spellcheck="false" autocomplete="url" placeholder="https://hub.example.com" value="${escR(b.url || "")}">` +
|
|
1511
|
+
` <button type="submit" class="btn sm primary" data-hub-action="connect">Connect</button>` +
|
|
1512
|
+
`</form>` +
|
|
1513
|
+
`<p class="set-note">Connect opens Hub. Sign in there and approve this install; you come back here connected. The token is kept in this install's secrets store, never shown.</p>` +
|
|
1514
|
+
(b.problem && b.url && !/^not connected yet/.test(b.problem)
|
|
1515
|
+
? note(b.problem, "data-hub-problem")
|
|
1516
|
+
: "") +
|
|
1517
|
+
`<div class="rt-err" role="alert" hidden></div>`
|
|
1518
|
+
);
|
|
1519
|
+
const status = {
|
|
1520
|
+
connected: ["ok", "Connected"],
|
|
1521
|
+
rejected: ["warn", "Token not accepted"],
|
|
1522
|
+
forbidden: ["warn", "Not allowed"],
|
|
1523
|
+
unreachable: ["warn", "Can't reach Hub"],
|
|
1524
|
+
}[state] || ["warn", "Not connected"];
|
|
1525
|
+
const team = b.team
|
|
1526
|
+
? b.teamUrl && /^https?:\/\//i.test(b.teamUrl)
|
|
1527
|
+
? `<a href="${escR(b.teamUrl)}" target="_blank" rel="noopener noreferrer">${escR(b.team)}</a>`
|
|
1528
|
+
: escR(b.team)
|
|
1529
|
+
: "";
|
|
1465
1530
|
const rows = [
|
|
1466
|
-
["
|
|
1531
|
+
["Hub", `<span class="den-mono">${escR(b.url || "")}</span>`],
|
|
1467
1532
|
[
|
|
1468
1533
|
"Status",
|
|
1469
|
-
|
|
1470
|
-
? `<span class="set-tag ok" data-hub-state="connected">Connected</span>`
|
|
1471
|
-
: `<span class="set-tag warn" data-hub-state="down">Not connected</span>`,
|
|
1534
|
+
`<span class="set-tag ${status[0]}" data-hub-state="${escR(state)}">${status[1]}</span>`,
|
|
1472
1535
|
],
|
|
1473
|
-
...(
|
|
1536
|
+
...(team ? [["Company", team]] : []),
|
|
1537
|
+
...(b.role ? [["Role", escR(b.role)]] : []),
|
|
1538
|
+
...(b.login ? [["Signed in as", escR(b.login)]] : []),
|
|
1474
1539
|
...(b.label ? [["This install", escR(b.label)]] : []),
|
|
1475
1540
|
];
|
|
1541
|
+
const why =
|
|
1542
|
+
state === "rejected"
|
|
1543
|
+
? "Hub no longer accepts this install's token (revoked or expired). Reconnect signs you in again."
|
|
1544
|
+
: state === "forbidden"
|
|
1545
|
+
? "Your role on Hub no longer allows Den installs. Ask a Hub admin to change your role, then Reconnect."
|
|
1546
|
+
: state === "unreachable"
|
|
1547
|
+
? b.problem || "Hub didn't answer."
|
|
1548
|
+
: "";
|
|
1476
1549
|
const tools =
|
|
1477
1550
|
`<label class="hub-tools"><input type="checkbox" id="hub-allow-tools"${b.allowTools !== false ? " checked" : ""}> Agents use Hub's tools without asking</label>` +
|
|
1478
1551
|
`<p class="set-note" data-hub-tools-note>${
|
|
@@ -1481,46 +1554,172 @@ export function hubHtml(b) {
|
|
|
1481
1554
|
: "Agents ask before each Hub tool runs."
|
|
1482
1555
|
}</p>`;
|
|
1483
1556
|
return (
|
|
1557
|
+
flashHtml +
|
|
1484
1558
|
`<dl class="hub-st" data-hub>` +
|
|
1485
1559
|
rows.map(([k, v]) => `<dt>${escR(k)}</dt><dd>${v}</dd>`).join("") +
|
|
1486
1560
|
`</dl>` +
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1561
|
+
(why ? note(why, "data-hub-problem") : "") +
|
|
1562
|
+
`<div class="hub-acts">` +
|
|
1563
|
+
(state === "rejected"
|
|
1564
|
+
? `<button type="button" class="btn sm primary" data-hub-action="connect">Reconnect</button> `
|
|
1565
|
+
: "") +
|
|
1566
|
+
`<button type="button" class="btn sm" data-hub-action="disconnect">Disconnect</button>` +
|
|
1567
|
+
`</div>` +
|
|
1568
|
+
// Disconnect asks first, saying what it does (Hub revokes the key).
|
|
1569
|
+
`<div class="hub-confirm" data-hub-confirm hidden>` +
|
|
1570
|
+
`<p class="set-note set-warn">This revokes this Den's Hub key on Hub. Agents lose Hub until you Connect again.</p>` +
|
|
1571
|
+
`<button type="button" class="btn sm primary" data-hub-action="disconnect-yes">Disconnect</button> ` +
|
|
1572
|
+
`<button type="button" class="btn sm" data-hub-action="disconnect-no">Cancel</button>` +
|
|
1573
|
+
`</div>` +
|
|
1574
|
+
`<div class="rt-err" role="alert" hidden></div>` +
|
|
1575
|
+
tools
|
|
1491
1576
|
);
|
|
1492
1577
|
}
|
|
1493
1578
|
|
|
1494
|
-
/**
|
|
1579
|
+
/** Read and clear ?hub=… (set by Hub's redirect back through /hub/callback). */
|
|
1580
|
+
function takeHubFlash() {
|
|
1581
|
+
try {
|
|
1582
|
+
const u = new URL(location.href);
|
|
1583
|
+
const v = u.searchParams.get("hub");
|
|
1584
|
+
if (!v) return null;
|
|
1585
|
+
u.searchParams.delete("hub");
|
|
1586
|
+
history.replaceState(history.state, "", u.pathname + u.search + u.hash);
|
|
1587
|
+
return v;
|
|
1588
|
+
} catch {
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/** HUB-1.5/1.7: the Hub row: always shown, so a person can Connect. */
|
|
1495
1594
|
function renderHub(host, ctx) {
|
|
1496
|
-
|
|
1497
|
-
|
|
1595
|
+
return drawHub(host, ctx, takeHubFlash(), null);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/** Draw the row; `back` is ?hub=… from Hub's redirect, `flash` words to show. */
|
|
1599
|
+
function drawHub(host, ctx, back, flash) {
|
|
1498
1600
|
// Under an invalid config /api/hub answers 422: ask nothing, show nothing.
|
|
1499
|
-
if (ctx.shell?.configInvalid?.() === true)
|
|
1601
|
+
if (ctx.shell?.configInvalid?.() === true) {
|
|
1602
|
+
host.innerHTML = "";
|
|
1603
|
+
return Promise.resolve();
|
|
1604
|
+
}
|
|
1500
1605
|
return ctx.shell
|
|
1501
1606
|
.api("GET", "/api/hub")
|
|
1502
1607
|
.then((b) => {
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
if (
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
box.disabled = true;
|
|
1510
|
-
ctx.shell
|
|
1511
|
-
.api("POST", "/api/settings", { hub_allow_tools: box.checked })
|
|
1512
|
-
.then(() => renderHub(host, ctx))
|
|
1513
|
-
.catch(() => {
|
|
1514
|
-
box.checked = !box.checked;
|
|
1515
|
-
box.disabled = false;
|
|
1516
|
-
});
|
|
1517
|
-
});
|
|
1608
|
+
let f = flash;
|
|
1609
|
+
if (!f && b && b.connect) f = b.connect;
|
|
1610
|
+
else if (!f && back && HUB_FLASH[back])
|
|
1611
|
+
f = { ok: HUB_FLASH[back][0], message: HUB_FLASH[back][1] };
|
|
1612
|
+
host.innerHTML = hubHtml(b, f);
|
|
1613
|
+
wireHub(host, ctx);
|
|
1518
1614
|
})
|
|
1519
1615
|
.catch(() => {
|
|
1520
1616
|
host.innerHTML = "";
|
|
1521
1617
|
});
|
|
1522
1618
|
}
|
|
1523
1619
|
|
|
1620
|
+
function wireHub(host, ctx) {
|
|
1621
|
+
const err = (msg, retry) => {
|
|
1622
|
+
const e = host.querySelector(".rt-err");
|
|
1623
|
+
if (!e) return;
|
|
1624
|
+
e.hidden = !msg;
|
|
1625
|
+
e.textContent = msg || "";
|
|
1626
|
+
if (retry) {
|
|
1627
|
+
const btn = document.createElement("button");
|
|
1628
|
+
btn.type = "button";
|
|
1629
|
+
btn.className = "btn sm";
|
|
1630
|
+
btn.dataset.hubAction = "force";
|
|
1631
|
+
btn.textContent = "Remove it here anyway";
|
|
1632
|
+
btn.addEventListener("click", () => disconnect(true, btn));
|
|
1633
|
+
e.append(" ", btn);
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
const busy = (on) =>
|
|
1637
|
+
host.querySelectorAll("button, input").forEach((x) => (x.disabled = on));
|
|
1638
|
+
const connect = () => {
|
|
1639
|
+
const typed = host.querySelector("#hub-url");
|
|
1640
|
+
busy(true);
|
|
1641
|
+
err("");
|
|
1642
|
+
ctx.shell
|
|
1643
|
+
.api("POST", "/api/hub/connect", typed ? { url: typed.value } : {})
|
|
1644
|
+
.then((r) => {
|
|
1645
|
+
// Off to Hub: sign in and approve there; Hub sends the person back.
|
|
1646
|
+
location.assign(r.location);
|
|
1647
|
+
})
|
|
1648
|
+
.catch((e) => {
|
|
1649
|
+
busy(false);
|
|
1650
|
+
err(e?.message || "Connect didn't start");
|
|
1651
|
+
});
|
|
1652
|
+
};
|
|
1653
|
+
const disconnect = (force, btn) => {
|
|
1654
|
+
busy(true);
|
|
1655
|
+
err("");
|
|
1656
|
+
ctx.shell
|
|
1657
|
+
.api("POST", "/api/hub/disconnect", force ? { force: true } : {})
|
|
1658
|
+
.then((r) => drawHub(host, ctx, null, { ok: true, message: r.message }))
|
|
1659
|
+
.catch((e) => {
|
|
1660
|
+
busy(false);
|
|
1661
|
+
if (btn) btn.remove();
|
|
1662
|
+
err(e?.message || "Not disconnected", !force);
|
|
1663
|
+
});
|
|
1664
|
+
};
|
|
1665
|
+
const form = host.querySelector("[data-hub-connect]");
|
|
1666
|
+
if (form)
|
|
1667
|
+
form.addEventListener("submit", (ev) => {
|
|
1668
|
+
ev.preventDefault();
|
|
1669
|
+
connect();
|
|
1670
|
+
});
|
|
1671
|
+
host
|
|
1672
|
+
.querySelectorAll('button[data-hub-action="connect"]')
|
|
1673
|
+
.forEach(
|
|
1674
|
+
(x) => x.type === "button" && x.addEventListener("click", connect),
|
|
1675
|
+
);
|
|
1676
|
+
const acts = host.querySelector(".hub-acts");
|
|
1677
|
+
const ask = host.querySelector("[data-hub-confirm]");
|
|
1678
|
+
const asking = (on) => {
|
|
1679
|
+
if (!acts || !ask) return;
|
|
1680
|
+
acts.hidden = on;
|
|
1681
|
+
ask.hidden = !on;
|
|
1682
|
+
host
|
|
1683
|
+
.querySelector(
|
|
1684
|
+
on
|
|
1685
|
+
? '[data-hub-action="disconnect-no"]'
|
|
1686
|
+
: '[data-hub-action="disconnect"]',
|
|
1687
|
+
)
|
|
1688
|
+
?.focus();
|
|
1689
|
+
};
|
|
1690
|
+
host
|
|
1691
|
+
.querySelector('[data-hub-action="disconnect"]')
|
|
1692
|
+
?.addEventListener("click", () => asking(true));
|
|
1693
|
+
host
|
|
1694
|
+
.querySelector('[data-hub-action="disconnect-no"]')
|
|
1695
|
+
?.addEventListener("click", () => asking(false));
|
|
1696
|
+
ask?.addEventListener("keydown", (ev) => {
|
|
1697
|
+
if (ev.key === "Escape") {
|
|
1698
|
+
ev.preventDefault();
|
|
1699
|
+
asking(false);
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
host
|
|
1703
|
+
.querySelector('[data-hub-action="disconnect-yes"]')
|
|
1704
|
+
?.addEventListener("click", () => {
|
|
1705
|
+
if (ask) ask.hidden = true;
|
|
1706
|
+
if (acts) acts.hidden = false;
|
|
1707
|
+
disconnect(false);
|
|
1708
|
+
});
|
|
1709
|
+
const box = host.querySelector("#hub-allow-tools");
|
|
1710
|
+
if (box)
|
|
1711
|
+
box.addEventListener("change", () => {
|
|
1712
|
+
box.disabled = true;
|
|
1713
|
+
ctx.shell
|
|
1714
|
+
.api("POST", "/api/settings", { hub_allow_tools: box.checked })
|
|
1715
|
+
.then(() => drawHub(host, ctx, null, null))
|
|
1716
|
+
.catch(() => {
|
|
1717
|
+
box.checked = !box.checked;
|
|
1718
|
+
box.disabled = false;
|
|
1719
|
+
});
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1524
1723
|
/** The Rotation row: this install's values, the overrides, + Add override. */
|
|
1525
1724
|
function renderRotation(host, ctx) {
|
|
1526
1725
|
const err = () => host.querySelector(".rt-err");
|
|
@@ -473,6 +473,29 @@
|
|
|
473
473
|
min-width: 0;
|
|
474
474
|
overflow-wrap: anywhere;
|
|
475
475
|
}
|
|
476
|
+
/* HUB-1.7: Connect / Disconnect. */
|
|
477
|
+
.hub-connect {
|
|
478
|
+
display: flex;
|
|
479
|
+
flex-wrap: wrap;
|
|
480
|
+
gap: 6px 8px;
|
|
481
|
+
align-items: center;
|
|
482
|
+
}
|
|
483
|
+
.hub-connect input {
|
|
484
|
+
flex: 1 1 240px;
|
|
485
|
+
min-width: 0;
|
|
486
|
+
}
|
|
487
|
+
.hub-acts {
|
|
488
|
+
display: flex;
|
|
489
|
+
flex-wrap: wrap;
|
|
490
|
+
gap: 8px;
|
|
491
|
+
margin-top: 10px;
|
|
492
|
+
}
|
|
493
|
+
.hub-flash.ok {
|
|
494
|
+
color: var(--ok, inherit);
|
|
495
|
+
}
|
|
496
|
+
.hub-flash.warn {
|
|
497
|
+
color: var(--warn, inherit);
|
|
498
|
+
}
|
|
476
499
|
.hub-tools {
|
|
477
500
|
display: flex;
|
|
478
501
|
gap: 8px;
|
|
@@ -309,6 +309,7 @@
|
|
|
309
309
|
"lock.checking": "Checking…",
|
|
310
310
|
"lock.note": "Den uses it only for sudo. Agents can't enter it.",
|
|
311
311
|
"lock.saved": "Admin password kept for this session",
|
|
312
|
+
"lock.savedTo": "Admin password kept for this session and saved to {s}",
|
|
312
313
|
"lock.saveFailed": "Kept for this session, but saving to {s} failed: {err}",
|
|
313
314
|
"lock.wrong": "Incorrect password — try again",
|
|
314
315
|
"lock.reqFailed": "Request failed — is the server running?",
|
|
@@ -864,6 +865,45 @@
|
|
|
864
865
|
"ag.sub.one": "{n} session · {w} waiting · {scope}",
|
|
865
866
|
"ag.all": "all workspaces",
|
|
866
867
|
"ag.waiting": "waiting",
|
|
868
|
+
"tx.asked": "Asked",
|
|
869
|
+
"tx.result": "Result",
|
|
870
|
+
"tx.error": "Error",
|
|
871
|
+
"tx.omitted": "{n} earlier lines not shown",
|
|
872
|
+
"tx.omitted.one": "{n} earlier line not shown",
|
|
873
|
+
"tx.empty": "Nothing yet.",
|
|
874
|
+
"tx.close": "Close",
|
|
875
|
+
"tx.loading": "Loading…",
|
|
876
|
+
"tx.failed": "Couldn't read it: {err}",
|
|
877
|
+
"hp.title": "Helpers ({n})",
|
|
878
|
+
"hp.bar": "Helpers: {n}",
|
|
879
|
+
"hp.barRunning": "Helpers: {n}, {r} running",
|
|
880
|
+
"hp.all": "All helpers ({n})",
|
|
881
|
+
"hp.runningN": "{n} running",
|
|
882
|
+
"hp.groupOf": "Helpers of {name}",
|
|
883
|
+
"hp.open": "{what}, {status}: open its transcript",
|
|
884
|
+
"hp.dlg": "{name}'s helpers",
|
|
885
|
+
"hp.earlier": "Earlier conversations ({n})",
|
|
886
|
+
"hp.none": "No helpers in this conversation.",
|
|
887
|
+
"hp.unsupported":
|
|
888
|
+
"This agent's runtime can't tell which helpers it started.",
|
|
889
|
+
"hp.workflow": "workflow {name}",
|
|
890
|
+
"hp.note":
|
|
891
|
+
"Helpers run inside this agent's own session, so Den shows them but can't stop or steer them. To change what they do, tell the agent.",
|
|
892
|
+
"hp.st.running": "running",
|
|
893
|
+
"hp.st.idle": "idle",
|
|
894
|
+
"hp.st.done": "done",
|
|
895
|
+
"hp.st.failed": "failed",
|
|
896
|
+
"hp.st.stopped": "stopped",
|
|
897
|
+
"b.helpers": "{n} helpers running",
|
|
898
|
+
"b.helpers.one": "{n} helper running",
|
|
899
|
+
"hub.rail": "Hub",
|
|
900
|
+
"hub.railTip": "{team} on Hub (opens in a new tab)",
|
|
901
|
+
"hub.railTipPlain": "Hub (opens in a new tab)",
|
|
902
|
+
"hub.railUnread": "{n} waiting on you",
|
|
903
|
+
"hub.railRejected":
|
|
904
|
+
"Hub doesn't accept this install's token. Reconnect in Settings › Hub.",
|
|
905
|
+
"hub.railForbidden":
|
|
906
|
+
"Hub doesn't allow this install. Ask a Hub admin (Settings › Hub).",
|
|
867
907
|
"ag.session": "session",
|
|
868
908
|
"ag.empty":
|
|
869
909
|
"Pick a session on the left to open its terminal. Open more to tile them (up to 4).",
|
|
@@ -1058,6 +1098,8 @@
|
|
|
1058
1098
|
// ON-6: the server agent, a speech bubble with a spark.
|
|
1059
1099
|
assistant:
|
|
1060
1100
|
'<path d="M5 5h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-8l-4 3v-3H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/><path d="M12 8v4M10 10h4"/>',
|
|
1101
|
+
// Hub: a node joined to three (the company's shared place).
|
|
1102
|
+
hub: '<circle cx="12" cy="12" r="2.5"/><circle cx="5" cy="6" r="2"/><circle cx="19" cy="6" r="2"/><circle cx="12" cy="20" r="2"/><path d="M6.6 7.2l3.5 3.2M17.4 7.2l-3.5 3.2M12 14.5V18"/>',
|
|
1061
1103
|
// SEC-2a: who this browser is (the rail footer's identity chip).
|
|
1062
1104
|
person:
|
|
1063
1105
|
'<circle cx="12" cy="8" r="3.5"/><path d="M5 20c.8-3.6 3.6-5.5 7-5.5s6.2 1.9 7 5.5"/>',
|
|
@@ -1340,6 +1382,9 @@
|
|
|
1340
1382
|
health: {},
|
|
1341
1383
|
down: 0,
|
|
1342
1384
|
openLogs: 0,
|
|
1385
|
+
helpersRunning: 0,
|
|
1386
|
+
// The rail's Hub link: GET /api/hub?rail=1, or null (not connected).
|
|
1387
|
+
hub: null,
|
|
1343
1388
|
pins: [],
|
|
1344
1389
|
collapsed: false,
|
|
1345
1390
|
hidden: [],
|
|
@@ -1917,6 +1962,15 @@
|
|
|
1917
1962
|
order: 17,
|
|
1918
1963
|
href: "/agents",
|
|
1919
1964
|
keywords: ["sessions", "terminal"],
|
|
1965
|
+
// D32: helpers the agents started that run now.
|
|
1966
|
+
badge: function (s) {
|
|
1967
|
+
return s.helpersRunning > 0
|
|
1968
|
+
? {
|
|
1969
|
+
n: s.helpersRunning,
|
|
1970
|
+
label: t("b.helpers", { n: s.helpersRunning }),
|
|
1971
|
+
}
|
|
1972
|
+
: null;
|
|
1973
|
+
},
|
|
1920
1974
|
},
|
|
1921
1975
|
// ON-6: the server agent, this Den's own: one click away, after Agents.
|
|
1922
1976
|
{
|
|
@@ -2492,6 +2546,7 @@
|
|
|
2492
2546
|
);
|
|
2493
2547
|
});
|
|
2494
2548
|
html += '<div class="den-rail-grow"></div>';
|
|
2549
|
+
html += hubItem();
|
|
2495
2550
|
html += item(
|
|
2496
2551
|
{
|
|
2497
2552
|
id: SETTINGS.id,
|
|
@@ -2515,6 +2570,75 @@
|
|
|
2515
2570
|
return html;
|
|
2516
2571
|
}
|
|
2517
2572
|
|
|
2573
|
+
/**
|
|
2574
|
+
* The rail's Hub link, above Settings: only once this install is
|
|
2575
|
+
* connected. It opens the company's page on Hub in a new tab, with a
|
|
2576
|
+
* count of what waits on the person (whoami's unread). A token Hub
|
|
2577
|
+
* rejects, or a role it forbids, greys it, and it goes to Settings ›
|
|
2578
|
+
* Hub, which says what to do.
|
|
2579
|
+
*/
|
|
2580
|
+
function hubTarget(u) {
|
|
2581
|
+
try {
|
|
2582
|
+
var x = new URL(u);
|
|
2583
|
+
return x.protocol === "https:" ||
|
|
2584
|
+
(x.protocol === "http:" &&
|
|
2585
|
+
(x.hostname === "localhost" || x.hostname === "127.0.0.1"))
|
|
2586
|
+
? x.href
|
|
2587
|
+
: "";
|
|
2588
|
+
} catch (e) {
|
|
2589
|
+
return "";
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
function hubItem() {
|
|
2593
|
+
var h = state.hub;
|
|
2594
|
+
if (!h || !h.configured) return "";
|
|
2595
|
+
var label = t("hub.rail");
|
|
2596
|
+
if (h.state === "rejected" || h.state === "forbidden") {
|
|
2597
|
+
var why = t(
|
|
2598
|
+
h.state === "rejected" ? "hub.railRejected" : "hub.railForbidden",
|
|
2599
|
+
);
|
|
2600
|
+
return (
|
|
2601
|
+
'<a class="den-ni den-hub den-off" href="/settings#agents.hub" data-nav="hub" data-hub="' +
|
|
2602
|
+
esc(h.state) +
|
|
2603
|
+
'" title="' +
|
|
2604
|
+
esc(why) +
|
|
2605
|
+
'" aria-label="' +
|
|
2606
|
+
esc(label + ": " + why) +
|
|
2607
|
+
'">' +
|
|
2608
|
+
icon("hub") +
|
|
2609
|
+
'<span class="den-lbl">' +
|
|
2610
|
+
esc(label) +
|
|
2611
|
+
"</span></a>"
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
var href = h.state === "connected" ? hubTarget(h.teamUrl || "") : "";
|
|
2615
|
+
if (!href) return "";
|
|
2616
|
+
var tip = h.team
|
|
2617
|
+
? t("hub.railTip", { team: h.team })
|
|
2618
|
+
: t("hub.railTipPlain");
|
|
2619
|
+
var n = typeof h.unread === "number" && h.unread > 0 ? h.unread : 0;
|
|
2620
|
+
var waiting = n ? t("hub.railUnread", { n: n }) : "";
|
|
2621
|
+
return (
|
|
2622
|
+
'<a class="den-ni den-hub" href="' +
|
|
2623
|
+
esc(href) +
|
|
2624
|
+
'" target="_blank" rel="noopener noreferrer" data-nav="hub" data-hub="connected" title="' +
|
|
2625
|
+
esc(tip) +
|
|
2626
|
+
'" aria-label="' +
|
|
2627
|
+
esc(label + ", " + tip + (waiting ? ", " + waiting : "")) +
|
|
2628
|
+
'">' +
|
|
2629
|
+
icon("hub") +
|
|
2630
|
+
'<span class="den-lbl">' +
|
|
2631
|
+
esc(label) +
|
|
2632
|
+
' <span class="den-ext" aria-hidden="true">↗</span></span>' +
|
|
2633
|
+
(n
|
|
2634
|
+
? '<span class="den-bdg" aria-hidden="true">' +
|
|
2635
|
+
esc(n > 99 ? "99+" : n) +
|
|
2636
|
+
"</span>"
|
|
2637
|
+
: "") +
|
|
2638
|
+
"</a>"
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2518
2642
|
/**
|
|
2519
2643
|
* SEC-2a: the rail footer's identity chip: "Local" (this machine, no
|
|
2520
2644
|
* sign-in), "Signed in · this device" (a one-time link or a code) or
|
|
@@ -4709,6 +4833,80 @@
|
|
|
4709
4833
|
document.title = title.slice(0, at) + " · " + t(label);
|
|
4710
4834
|
})();
|
|
4711
4835
|
|
|
4836
|
+
// D32: the rail's Agents badge on every page: helpers running now
|
|
4837
|
+
// (GET /api/sessions/helpers, a count). The Agents page sets it itself
|
|
4838
|
+
// from its own list. Quiet on any failure: the badge just isn't there.
|
|
4839
|
+
(function () {
|
|
4840
|
+
if (page === "agents" || typeof rawFetch !== "function") return;
|
|
4841
|
+
function poll() {
|
|
4842
|
+
// A refusal page or a bad config: no polls (each would be refused).
|
|
4843
|
+
if (configInvalid()) return;
|
|
4844
|
+
if (document.hidden) return void setTimeout(poll, 15000);
|
|
4845
|
+
rawFetch("/api/sessions/helpers", {
|
|
4846
|
+
cache: "no-store",
|
|
4847
|
+
credentials: "same-origin",
|
|
4848
|
+
})
|
|
4849
|
+
.then(function (r) {
|
|
4850
|
+
return r.ok ? r.json() : null;
|
|
4851
|
+
})
|
|
4852
|
+
.then(
|
|
4853
|
+
function (b) {
|
|
4854
|
+
var n = b && typeof b.running === "number" ? b.running : 0;
|
|
4855
|
+
if (n !== state.helpersRunning) setState({ helpersRunning: n });
|
|
4856
|
+
},
|
|
4857
|
+
function () {},
|
|
4858
|
+
)
|
|
4859
|
+
.then(function () {
|
|
4860
|
+
setTimeout(poll, 15000);
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
setTimeout(poll, 1500);
|
|
4864
|
+
})();
|
|
4865
|
+
|
|
4866
|
+
// The rail's Hub link (hubItem): GET /api/hub?rail=1 (which leaves
|
|
4867
|
+
// Connect's one-time result for Settings) at load and every minute while
|
|
4868
|
+
// the page shows. Quiet on any failure: the link just isn't there.
|
|
4869
|
+
(function () {
|
|
4870
|
+
if (typeof rawFetch !== "function") return;
|
|
4871
|
+
var last = "";
|
|
4872
|
+
function poll() {
|
|
4873
|
+
// A refusal page or a bad config: no polls (each would be refused).
|
|
4874
|
+
if (configInvalid()) return;
|
|
4875
|
+
if (document.hidden) return void setTimeout(poll, 60000);
|
|
4876
|
+
rawFetch("/api/hub?rail=1", {
|
|
4877
|
+
cache: "no-store",
|
|
4878
|
+
credentials: "same-origin",
|
|
4879
|
+
})
|
|
4880
|
+
.then(function (r) {
|
|
4881
|
+
return r.ok ? r.json() : null;
|
|
4882
|
+
})
|
|
4883
|
+
.then(
|
|
4884
|
+
function (b) {
|
|
4885
|
+
var h =
|
|
4886
|
+
b && b.configured
|
|
4887
|
+
? {
|
|
4888
|
+
configured: true,
|
|
4889
|
+
state: b.state,
|
|
4890
|
+
team: b.team,
|
|
4891
|
+
teamUrl: b.teamUrl,
|
|
4892
|
+
unread: b.unread,
|
|
4893
|
+
}
|
|
4894
|
+
: null;
|
|
4895
|
+
var k = JSON.stringify(h);
|
|
4896
|
+
if (k !== last) {
|
|
4897
|
+
last = k;
|
|
4898
|
+
setState({ hub: h });
|
|
4899
|
+
}
|
|
4900
|
+
},
|
|
4901
|
+
function () {},
|
|
4902
|
+
)
|
|
4903
|
+
.then(function () {
|
|
4904
|
+
setTimeout(poll, 60000);
|
|
4905
|
+
});
|
|
4906
|
+
}
|
|
4907
|
+
setTimeout(poll, 800);
|
|
4908
|
+
})();
|
|
4909
|
+
|
|
4712
4910
|
window.denShell = {
|
|
4713
4911
|
registerGroup: registerGroup,
|
|
4714
4912
|
registerNav: registerNav,
|
|
@@ -5113,6 +5311,9 @@
|
|
|
5113
5311
|
" text-align: left; background: none; border: 0; padding: 6px 10px; border-radius: var(--radius-button);",
|
|
5114
5312
|
" cursor: pointer; color: var(--muted); text-decoration: none; position: relative; font: inherit; }",
|
|
5115
5313
|
".den-ni:hover { background: var(--sunken); color: var(--text); }",
|
|
5314
|
+
// The rail's Hub link: ↗ (a new tab); greyed when Hub refuses it.
|
|
5315
|
+
".den-ni .den-ext { color: var(--faint); font-size: 12px; }",
|
|
5316
|
+
".den-ni.den-off { opacity: .55; }",
|
|
5116
5317
|
".den-ni[aria-current='page'] { background: color-mix(in srgb, var(--accent) 13%, transparent);",
|
|
5117
5318
|
" color: var(--text); font-weight: 500; }",
|
|
5118
5319
|
".den-ni[aria-current='page']::before { content: ''; position: absolute; left: -8px; top: 6px;",
|
|
@@ -1459,7 +1459,11 @@ function openLock() {
|
|
|
1459
1459
|
s: sudo.vault.label || sudo.vault.backend,
|
|
1460
1460
|
err: res.vaultError,
|
|
1461
1461
|
})
|
|
1462
|
-
:
|
|
1462
|
+
: saveToVault
|
|
1463
|
+
? t("lock.savedTo", {
|
|
1464
|
+
s: sudo.vault.label || sudo.vault.backend,
|
|
1465
|
+
})
|
|
1466
|
+
: t("lock.saved"),
|
|
1463
1467
|
);
|
|
1464
1468
|
settleSudo(true);
|
|
1465
1469
|
return;
|