@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.
@@ -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 and project come from this install's token and the workspace's repos. Set hub.url and hub.token (a secret ref in this install's store) in config/local/user.yml.",
921
- keywords: ["hub", "den hub", "mcp", "team", "token", "docs", "mocks"],
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.5: Settings › Agents › Den Hub (b: GET /api/hub). */
1463
- export function hubHtml(b) {
1464
- if (!b || !b.configured) return "";
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
- ["URL", `<span class="den-mono">${escR(b.url || "not set")}</span>`],
1531
+ ["Hub", `<span class="den-mono">${escR(b.url || "")}</span>`],
1467
1532
  [
1468
1533
  "Status",
1469
- b.connected
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
- ...(b.team ? [["Team", escR(b.team)]] : []),
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
- tools +
1488
- (!b.connected && b.problem
1489
- ? `<p class="set-note" data-hub-problem>${escR(b.problem)}</p>`
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
- /** HUB-1.5: the Den Hub row; with no hub.url, the row isn't shown at all. */
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
- const row = host.closest(".set-row");
1497
- if (row) row.hidden = true;
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) return Promise.resolve();
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
- const html = hubHtml(b);
1504
- host.innerHTML = html;
1505
- if (row) row.hidden = !html;
1506
- const box = host.querySelector("#hub-allow-tools");
1507
- if (box)
1508
- box.addEventListener("change", () => {
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
- : t("lock.saved"),
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;