@monster-orange/den 0.4.27 → 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.
@@ -84,6 +84,13 @@
84
84
  * as a tag.
85
85
  */
86
86
  import { readiness, runtimeName, runtimeOptions } from "/agent-start.js";
87
+ import { focusBack, refindKey } from "/transcript-view.js";
88
+ import {
89
+ helperBarButtonHtml,
90
+ helperRowsHtml,
91
+ openHelpers,
92
+ wireHelperRows,
93
+ } from "/agent-helpers.js";
87
94
  import {
88
95
  commandProblem,
89
96
  filterBarHtml,
@@ -496,7 +503,8 @@ function stoppedHtml(r) {
496
503
  `<span class="why">${esc(stopReason(r))}</span>` +
497
504
  `</button>` +
498
505
  `<div class="agacts">${callout}${cant}${resume}${fresh}${rename}${remove}</div>` +
499
- `</div>`
506
+ `</div>` +
507
+ helperRowsHtml(r)
500
508
  );
501
509
  }
502
510
 
@@ -540,15 +548,49 @@ function rowHtml(r) {
540
548
  // D28: a running agent whose rotation is due: its callout, with buttons
541
549
  // (before, only a tag: nothing to press).
542
550
  const callout = r.kind === "agent" ? rotCalloutHtml(r) : "";
543
- if (!callout) return button;
551
+ // D32: the helpers it started in its own session, under it.
552
+ const helpers = r.kind === "agent" ? helperRowsHtml(r) : "";
553
+ if (!callout) return button + helpers;
544
554
  return (
545
555
  `<div class="agst" data-agent="${esc(r.agent)}" data-state="${esc(r.state)}" data-ws="${esc(r.workspace)}" data-rotation="${rot}">` +
546
556
  button +
547
557
  `<div class="agacts">${callout}</div>` +
548
- `</div>`
558
+ `</div>` +
559
+ helpers
549
560
  );
550
561
  }
551
562
 
563
+ /** Where focus was in the list, as a selector that finds it again once
564
+ * the list is redrawn: a helper row or button, an agent's row, a link. */
565
+ function listFocusKey(el) {
566
+ const key = refindKey(el);
567
+ if (key) return key;
568
+ const q = (v) => CSS.escape(String(v ?? ""));
569
+ if (el.matches("button.agr[data-id]"))
570
+ return `#agl button.agr[data-id="${q(el.dataset.id)}"]`;
571
+ if (el.matches("[data-act]")) {
572
+ const row = el.closest(".agst[data-agent]");
573
+ const act = `[data-act="${q(el.dataset.act)}"]`;
574
+ return row
575
+ ? `#agl .agst[data-agent="${q(row.dataset.agent)}"][data-ws="${q(row.dataset.ws)}"] ${act}`
576
+ : `#agl ${act}`;
577
+ }
578
+ return "";
579
+ }
580
+
581
+ /** The list's HTML, written only when it changed; focus inside it is kept
582
+ * (the refresh poll redraws it every few seconds). */
583
+ function setList(el, html) {
584
+ if (el.denHtml === html) return;
585
+ el.denHtml = html;
586
+ const had = el.contains(document.activeElement)
587
+ ? document.activeElement
588
+ : null;
589
+ const key = had ? listFocusKey(had) : "";
590
+ el.innerHTML = html;
591
+ if (had) focusBack(had, key);
592
+ }
593
+
552
594
  function paintList() {
553
595
  const el = st.root.querySelector("#agl");
554
596
  el.removeAttribute("aria-busy");
@@ -562,7 +604,7 @@ function paintList() {
562
604
  `<div class="agnote" data-note="backend">${esc(t("ag.noBackend", { cli: CLI }))}</div>`,
563
605
  );
564
606
  if (!st.data) {
565
- el.innerHTML = notes.join("");
607
+ setList(el, notes.join(""));
566
608
  return;
567
609
  }
568
610
  // ON-6: the server agent's group, "Server", first, whatever the view.
@@ -615,10 +657,12 @@ function paintList() {
615
657
  )
616
658
  .join("")
617
659
  : `<div class="none">${esc(t("ag.none.scope", { scope: st.scopeLabel }))}</div>`;
618
- el.innerHTML =
660
+ setList(
661
+ el,
619
662
  notes.join("") +
620
- body +
621
- `<div class="aghint" id="agl-hint">${esc(t(`ag.hint.${shownLayout()}`))} ${esc(t("ag.hint.keys"))}</div>`;
663
+ body +
664
+ `<div class="aghint" id="agl-hint">${esc(t(`ag.hint.${shownLayout()}`))} ${esc(t("ag.hint.keys"))}</div>`,
665
+ );
622
666
  const clear = st.root.querySelector("#ag-clear");
623
667
  if (clear) clear.hidden = !visibleRows().some(isStopped);
624
668
  // No such workspace: nothing to start an agent in.
@@ -697,6 +741,13 @@ async function refresh() {
697
741
  /* the last list stays; the next ask may answer */
698
742
  }
699
743
  followAgents();
744
+ // D32: the rail's Agents badge: helpers running now.
745
+ window.denShell?.setState?.({
746
+ helpersRunning: (st.data?.sessions ?? []).reduce(
747
+ (n, r) => n + (r.helpers?.running ?? 0),
748
+ 0,
749
+ ),
750
+ });
700
751
  st.scopeLabel = scopeLabelFor(viewWorkspaces());
701
752
  paintList();
702
753
  paintSub();
@@ -1098,7 +1149,7 @@ function paintBar(p) {
1098
1149
  (r?.kind === "agent" && rotActionsHtml(r)
1099
1150
  ? `<span class="agrot" data-rot-bar title="${esc(rotMessage(r))}"><span class="tagx warn" data-rotation="${rotationOf(r)}">${esc(t(rotationOf(r) === "ready" ? "ag.rot.ready" : "ag.rot.due", { why: rotationWhy(r.rotation) }))}</span>${rotActionsHtml(r)}</span>`
1100
1151
  : "") +
1101
- `<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>`;
1152
+ `<span class="x">${r?.kind === "agent" ? helperBarButtonHtml(r) : ""}${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>`;
1102
1153
  // Tabs: the tab's × closes it; the pane's bar has none.
1103
1154
  const x = bar.querySelector('[data-act="close"]');
1104
1155
  if (x) x.hidden = shownLayout() === "tabs";
@@ -2947,6 +2998,13 @@ export async function mountAgentsPage(root) {
2947
2998
  startPrefill = { ws: q.get("ws") || "", folder: q.get("folder") || "" };
2948
2999
  }
2949
3000
 
3001
+ // D32: helper rows open their transcript; "All helpers" the full list.
3002
+ wireHelperRows(root.querySelector("#agl"), (agent, ws) => {
3003
+ const r = visibleRows().find(
3004
+ (x) => x.agent === agent && x.workspace === ws,
3005
+ );
3006
+ return r ? displayName(r) : agent;
3007
+ });
2950
3008
  root.querySelector("#agl").addEventListener("click", (e) => {
2951
3009
  const act = e.target.closest("button[data-act]");
2952
3010
  if (act) {
@@ -3015,7 +3073,16 @@ export async function mountAgentsPage(root) {
3015
3073
  const r = rowOf(id);
3016
3074
  if (r) void restartRow(r, "resume", b);
3017
3075
  } else if (b.dataset.act === "reconnect") void connect(p);
3018
- else if (b.dataset.act === "more") {
3076
+ else if (b.dataset.act === "helpers") {
3077
+ const r = rowOf(id);
3078
+ if (r?.kind === "agent")
3079
+ void openHelpers({
3080
+ ws: r.workspace,
3081
+ agent: r.agent,
3082
+ name: paneName(r),
3083
+ opener: b,
3084
+ });
3085
+ } else if (b.dataset.act === "more") {
3019
3086
  const r = rowOf(id);
3020
3087
  if (!r) return;
3021
3088
  window.denShell?.openMenu?.(b, {
@@ -758,6 +758,45 @@
758
758
  "ag.sub.one": "{n} sesión · {w} en espera · {scope}",
759
759
  "ag.all": "todos los espacios",
760
760
  "ag.waiting": "en espera",
761
+ "tx.asked": "Pedido",
762
+ "tx.result": "Resultado",
763
+ "tx.error": "Error",
764
+ "tx.omitted": "{n} líneas anteriores no se muestran",
765
+ "tx.omitted.one": "{n} línea anterior no se muestra",
766
+ "tx.empty": "Nada todavía.",
767
+ "tx.close": "Cerrar",
768
+ "tx.loading": "Cargando…",
769
+ "tx.failed": "No se pudo leer: {err}",
770
+ "hp.title": "Ayudantes ({n})",
771
+ "hp.bar": "Ayudantes: {n}",
772
+ "hp.barRunning": "Ayudantes: {n}, {r} en marcha",
773
+ "hp.all": "Todos los ayudantes ({n})",
774
+ "hp.runningN": "{n} en marcha",
775
+ "hp.groupOf": "Ayudantes de {name}",
776
+ "hp.open": "{what}, {status}: abrir su transcripción",
777
+ "hp.dlg": "Ayudantes de {name}",
778
+ "hp.earlier": "Conversaciones anteriores ({n})",
779
+ "hp.none": "Sin ayudantes en esta conversación.",
780
+ "hp.unsupported":
781
+ "El runtime de este agente no puede decir qué ayudantes inició.",
782
+ "hp.workflow": "flujo {name}",
783
+ "hp.note":
784
+ "Los ayudantes corren dentro de la sesión del propio agente: Den los muestra, pero no puede detenerlos ni dirigirlos. Para cambiar lo que hacen, díselo al agente.",
785
+ "hp.st.running": "en marcha",
786
+ "hp.st.idle": "inactivo",
787
+ "hp.st.done": "terminado",
788
+ "hp.st.failed": "falló",
789
+ "hp.st.stopped": "detenido",
790
+ "b.helpers": "{n} ayudantes en marcha",
791
+ "b.helpers.one": "{n} ayudante en marcha",
792
+ "hub.rail": "Hub",
793
+ "hub.railTip": "{team} en Hub (se abre en otra pestaña)",
794
+ "hub.railTipPlain": "Hub (se abre en otra pestaña)",
795
+ "hub.railUnread": "{n} esperan tu respuesta",
796
+ "hub.railRejected":
797
+ "Hub no acepta el token de esta instalación. Vuelve a conectar en Ajustes › Hub.",
798
+ "hub.railForbidden":
799
+ "Hub no permite esta instalación. Pídeselo a un administrador de Hub (Ajustes › Hub).",
761
800
  "ag.session": "sesión",
762
801
  "ag.empty":
763
802
  "Elige una sesión a la izquierda para abrir su terminal. Abre más para verlas en mosaico (hasta 4).",
@@ -739,6 +739,43 @@
739
739
  "ag.sub": "{n} セッション · 承認待ち {w} · {scope}",
740
740
  "ag.all": "すべてのワークスペース",
741
741
  "ag.waiting": "承認待ち",
742
+ "tx.asked": "依頼",
743
+ "tx.result": "結果",
744
+ "tx.error": "エラー",
745
+ "tx.omitted": "以前の {n} 行は表示していません",
746
+ "tx.empty": "まだ何もありません。",
747
+ "tx.close": "閉じる",
748
+ "tx.loading": "読み込み中…",
749
+ "tx.failed": "読み取れませんでした: {err}",
750
+ "hp.title": "ヘルパー ({n})",
751
+ "hp.bar": "ヘルパー: {n}",
752
+ "hp.barRunning": "ヘルパー: {n}、実行中 {r}",
753
+ "hp.all": "すべてのヘルパー ({n})",
754
+ "hp.runningN": "実行中 {n}",
755
+ "hp.groupOf": "{name} のヘルパー",
756
+ "hp.open": "{what}、{status}: トランスクリプトを開く",
757
+ "hp.dlg": "{name} のヘルパー",
758
+ "hp.earlier": "以前の会話 ({n})",
759
+ "hp.none": "この会話にヘルパーはいません。",
760
+ "hp.unsupported":
761
+ "このエージェントのランタイムは、起動したヘルパーを示せません。",
762
+ "hp.workflow": "ワークフロー {name}",
763
+ "hp.note":
764
+ "ヘルパーはこのエージェント自身のセッションで動くため、Den は表示できますが、停止や指示はできません。変えたいときはエージェントに伝えてください。",
765
+ "hp.st.running": "実行中",
766
+ "hp.st.idle": "待機",
767
+ "hp.st.done": "完了",
768
+ "hp.st.failed": "失敗",
769
+ "hp.st.stopped": "停止",
770
+ "b.helpers": "実行中のヘルパー {n}",
771
+ "hub.rail": "Hub",
772
+ "hub.railTip": "Hub の {team}(新しいタブで開く)",
773
+ "hub.railTipPlain": "Hub(新しいタブで開く)",
774
+ "hub.railUnread": "対応待ち {n} 件",
775
+ "hub.railRejected":
776
+ "Hub がこのインストールのトークンを受け付けません。設定 › Hub から再接続してください。",
777
+ "hub.railForbidden":
778
+ "Hub がこのインストールを許可していません。Hub の管理者に依頼してください(設定 › Hub)。",
742
779
  "ag.session": "セッション",
743
780
  "ag.empty":
744
781
  "左のセッションを選ぶとターミナルが開きます。複数開くと並べて表示します(最大 4)。",
@@ -1565,6 +1565,12 @@ export function hubHtml(b, flash) {
1565
1565
  : "") +
1566
1566
  `<button type="button" class="btn sm" data-hub-action="disconnect">Disconnect</button>` +
1567
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>` +
1568
1574
  `<div class="rt-err" role="alert" hidden></div>` +
1569
1575
  tools
1570
1576
  );
@@ -1667,9 +1673,39 @@ function wireHub(host, ctx) {
1667
1673
  .forEach(
1668
1674
  (x) => x.type === "button" && x.addEventListener("click", connect),
1669
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
+ };
1670
1690
  host
1671
1691
  .querySelector('[data-hub-action="disconnect"]')
1672
- ?.addEventListener("click", () => disconnect(false));
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
+ });
1673
1709
  const box = host.querySelector("#hub-allow-tools");
1674
1710
  if (box)
1675
1711
  box.addEventListener("change", () => {
@@ -865,6 +865,45 @@
865
865
  "ag.sub.one": "{n} session · {w} waiting · {scope}",
866
866
  "ag.all": "all workspaces",
867
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).",
868
907
  "ag.session": "session",
869
908
  "ag.empty":
870
909
  "Pick a session on the left to open its terminal. Open more to tile them (up to 4).",
@@ -1059,6 +1098,8 @@
1059
1098
  // ON-6: the server agent, a speech bubble with a spark.
1060
1099
  assistant:
1061
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"/>',
1062
1103
  // SEC-2a: who this browser is (the rail footer's identity chip).
1063
1104
  person:
1064
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"/>',
@@ -1341,6 +1382,9 @@
1341
1382
  health: {},
1342
1383
  down: 0,
1343
1384
  openLogs: 0,
1385
+ helpersRunning: 0,
1386
+ // The rail's Hub link: GET /api/hub?rail=1, or null (not connected).
1387
+ hub: null,
1344
1388
  pins: [],
1345
1389
  collapsed: false,
1346
1390
  hidden: [],
@@ -1918,6 +1962,15 @@
1918
1962
  order: 17,
1919
1963
  href: "/agents",
1920
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
+ },
1921
1974
  },
1922
1975
  // ON-6: the server agent, this Den's own: one click away, after Agents.
1923
1976
  {
@@ -2493,6 +2546,7 @@
2493
2546
  );
2494
2547
  });
2495
2548
  html += '<div class="den-rail-grow"></div>';
2549
+ html += hubItem();
2496
2550
  html += item(
2497
2551
  {
2498
2552
  id: SETTINGS.id,
@@ -2516,6 +2570,75 @@
2516
2570
  return html;
2517
2571
  }
2518
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
+
2519
2642
  /**
2520
2643
  * SEC-2a: the rail footer's identity chip: "Local" (this machine, no
2521
2644
  * sign-in), "Signed in · this device" (a one-time link or a code) or
@@ -4710,6 +4833,80 @@
4710
4833
  document.title = title.slice(0, at) + " · " + t(label);
4711
4834
  })();
4712
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
+
4713
4910
  window.denShell = {
4714
4911
  registerGroup: registerGroup,
4715
4912
  registerNav: registerNav,
@@ -5114,6 +5311,9 @@
5114
5311
  " text-align: left; background: none; border: 0; padding: 6px 10px; border-radius: var(--radius-button);",
5115
5312
  " cursor: pointer; color: var(--muted); text-decoration: none; position: relative; font: inherit; }",
5116
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; }",
5117
5317
  ".den-ni[aria-current='page'] { background: color-mix(in srgb, var(--accent) 13%, transparent);",
5118
5318
  " color: var(--text); font-weight: 500; }",
5119
5319
  ".den-ni[aria-current='page']::before { content: ''; position: absolute; left: -8px; top: 6px;",