@monster-orange/den 0.4.16 → 0.4.18

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +94 -1
  4. package/package.json +1 -1
  5. package/src/ui/cli/agent-ops.ts +50 -0
  6. package/src/ui/cli/agents-command.ts +5 -5
  7. package/src/ui/server/agent-routes.ts +13 -1
  8. package/src/ui/server/check-pool.ts +72 -0
  9. package/src/ui/server/index.ts +105 -10
  10. package/src/ui/server/public/agents-page.js +98 -16
  11. package/src/ui/server/public/agents.html +22 -0
  12. package/src/ui/server/public/environments.html +223 -140
  13. package/src/ui/server/public/graph.css +40 -0
  14. package/src/ui/server/public/graph.js +47 -6
  15. package/src/ui/server/public/header.js +7 -0
  16. package/src/ui/server/public/i18n/es.js +46 -0
  17. package/src/ui/server/public/i18n/ja.js +46 -0
  18. package/src/ui/server/public/index.html +6 -0
  19. package/src/ui/server/public/local-dev.html +199 -27
  20. package/src/ui/server/public/log-source.js +26 -0
  21. package/src/ui/server/public/logs.html +7 -20
  22. package/src/ui/server/public/panel.js +1 -1
  23. package/src/ui/server/public/release.html +19 -1
  24. package/src/ui/server/public/services.html +18 -3
  25. package/src/ui/server/public/shell.js +360 -32
  26. package/src/ui/server/public/svc-panel.js +672 -0
  27. package/src/ui/server/public/topbar.js +34 -1
  28. package/src/ui/server/public/view-scope.js +61 -0
  29. package/src/ui/server/public/workspace-picker.js +2 -0
  30. package/src/ui/server/route-classes.ts +2 -0
  31. package/src/ui/server/run-history.ts +181 -0
  32. package/src/ui/server/session-routes.ts +94 -17
  33. package/src/ui/server/ui-assets.ts +6 -0
@@ -62,8 +62,9 @@
62
62
  * setOrgs([{ id, title, workspaces? }], currentId, onSelect?)
63
63
  * two or more: the Org ▾ menu; a pick
64
64
  * calls onSelect(id), fires `den:org`
65
- * setWorkspaces({ label, title, items, focus, onSelect })
65
+ * setWorkspaces({ label, title, items, focus, names, onSelect })
66
66
  * the Workspace ▾ menu (header.js)
67
+ * workspaceNames() every workspace's name, from it
67
68
  * syncEnv() repaint Env ▾ from #env-sel
68
69
  * registerCommand({ id, label, keywords?, kind?, run })
69
70
  * a ⌘K palette row (R17, dev.20). label
@@ -77,6 +78,8 @@
77
78
  * unregisterCommand(id), commands()
78
79
  * openMenu(anchor, menu) a .pop menu below anchor (the scope
79
80
  * parts' and the user menu's)
81
+ * topLayer.show(el, anchor, opts) a page's own menu in the top layer
82
+ * (0.4.17), placed by anchor; hide(el)
80
83
  *
81
84
  * Loading before shell.js: push functions onto window.denShellQueue (an
82
85
  * array); each runs with denShell once it exists, and a later push runs at
@@ -160,10 +163,47 @@
160
163
  "b.down.one": "{n} down",
161
164
  "act.pin": "Pin",
162
165
  "act.unpin": "Unpin",
166
+ "act.visit": "Visit",
167
+ "act.visitX": "Visit {s} (opens a new tab)",
163
168
  "act.pinX": "Pin {s}",
164
169
  "act.unpinX": "Unpin {s}",
165
170
  "toast.pinned": "Pinned {s}",
166
171
  "toast.unpinned": "Unpinned {s}",
172
+ // 0.4.18: the service panel (svc-panel.js) and Start's Folder.
173
+ "svp.label": "{s}: quick panel",
174
+ "svp.more": "Quick panel for {s}",
175
+ "svp.notRunning": "Not running",
176
+ "svp.up": "up {d}",
177
+ "svp.upNow": "up",
178
+ "svp.down": "down: {c}",
179
+ "svp.downNone": "down",
180
+ "svp.starting": "starting…",
181
+ "svp.stopped": "stopped",
182
+ "svp.noUrl": "No URL",
183
+ "svp.start": "Start",
184
+ "svp.stop": "Stop",
185
+ "svp.restart": "Restart",
186
+ "svp.logs": "Logs",
187
+ "svp.stopping": "Stopping {s}",
188
+ "svp.tests": "Last test run",
189
+ "svp.testsOk": "✓ {t}",
190
+ "svp.testsBad": "✗ failed {t}",
191
+ "svp.testsRun": "running…",
192
+ "svp.testsNone": "—",
193
+ "svp.runTests": "Run tests",
194
+ "svp.testsDone": "Tests passed: {s}",
195
+ "svp.testsFailed": "Tests failed: {s}",
196
+ "svp.agents": "Agents",
197
+ "svp.noAgents": "No agents in this repo.",
198
+ "svp.open": "Open",
199
+ "svp.openAg": "Open {s}",
200
+ "svp.startHere": "Start agent here",
201
+ "svp.openSvc": "Open in Services",
202
+ "svp.openGraph": "Open in Graph",
203
+ "svp.close": "Close",
204
+ "ag.new.folder": "Folder (optional)",
205
+ "ag.new.folderHelp":
206
+ "Inside the workspace's folder. Empty: the workspace's own.",
167
207
  collapse: "Collapse",
168
208
  expand: "Expand",
169
209
  menu: "Menu",
@@ -220,6 +260,8 @@
220
260
  "kind.app": "App",
221
261
  "kind.action": "Action",
222
262
  "kind.setting": "Setting",
263
+ "kind.workspace": "Workspace",
264
+ "pal.goWs": "Go to workspace {ws}",
223
265
  "pal.repos": "{n} repos",
224
266
  "pal.repos.one": "{n} repo",
225
267
  "pal.restart": "Restart {s}",
@@ -625,6 +667,10 @@
625
667
  "ag.pc.busy": "It's working. Change after this turn, or stop it now?",
626
668
  "ag.pc.after": "After this turn",
627
669
  "ag.pc.now": "Stop it now",
670
+ "ag.pc.fresh":
671
+ "{id} is now {p}: nothing to resume yet; started fresh (same conversation id, folder and scope)",
672
+ "ag.freshLine":
673
+ "Nothing to resume yet; started fresh (same conversation id, folder and scope)",
628
674
  "ag.pc.done": "{id} is now {p}",
629
675
  "ag.pc.pending": "{id} becomes {p} when this turn ends",
630
676
  "ag.more": "More for {id}",
@@ -776,6 +822,9 @@
776
822
  "This session has ended. Its record stays until it is resumed or removed.",
777
823
  "ag.switched": "Switched to {p}",
778
824
  "ag.exitedLine": "[the session exited{code}]",
825
+ "ag.refusedUnknown": "the server refused the terminal",
826
+ "ag.refusedStatus": "the server refused the terminal ({status})",
827
+ "ag.goWs": "Open {ws} on Workspaces",
779
828
  "ag.lost": "[disconnected: {why}]",
780
829
  "ag.reconnect": "Reconnect",
781
830
  "ag.noBackend":
@@ -1200,6 +1249,12 @@
1200
1249
  // SEC-2a: who this browser is (GET /api/session `identity`): local,
1201
1250
  // a signed-in device, or a GitHub login. Null until known.
1202
1251
  identity: null,
1252
+ // 0.4.18: each pin's URL (svc-panel.js, from the health feed) for its
1253
+ // Visit ↗, whether that Visit is off (down or starting: the panel's own
1254
+ // live state), and the pin whose service panel is open.
1255
+ pinUrls: {},
1256
+ pinOff: {},
1257
+ svpOpen: null,
1203
1258
  };
1204
1259
  try {
1205
1260
  var cached = JSON.parse(localStorage.getItem(LS) || "{}");
@@ -1411,6 +1466,141 @@
1411
1466
  return el;
1412
1467
  }
1413
1468
 
1469
+ // ── the top layer (0.4.17): one placement for every menu ─────────────
1470
+ // A menu shown with topShow is in the browser's top layer (a manual
1471
+ // popover), placed by its anchor: below it, above when there's more room
1472
+ // there, kept inside the window. No overflow or stacking ancestor can
1473
+ // clip or cover it, and it stays where it is in the page, so the page's
1474
+ // own click handlers still reach it. It closes when the page hides it
1475
+ // (display: none), its anchor goes, or topHide.
1476
+ var topOpen = [];
1477
+ var TOP_MARGIN = 8;
1478
+ function topSupported(el) {
1479
+ return typeof el.showPopover === "function";
1480
+ }
1481
+ function topPlace(el, anchor, opts) {
1482
+ var r = anchor.getBoundingClientRect();
1483
+ var gap = opts.gap != null ? opts.gap : 4;
1484
+ el.style.maxHeight = "";
1485
+ // 0.4.18: a bottom sheet is placed by its CSS (bottom: 0), not here.
1486
+ if (opts.sheet) {
1487
+ el.style.removeProperty("top");
1488
+ el.style.removeProperty("left");
1489
+ return;
1490
+ }
1491
+ // 0.4.18: beside the anchor (the rail), level with a row in it.
1492
+ if (opts.side === "right") {
1493
+ var row = typeof opts.row === "function" ? opts.row() : null;
1494
+ var rr = row ? row.getBoundingClientRect() : r;
1495
+ var bw = el.offsetWidth;
1496
+ var bh = el.offsetHeight;
1497
+ var bl = Math.min(r.right + gap, window.innerWidth - bw - TOP_MARGIN);
1498
+ var bt = Math.min(
1499
+ rr.top + (opts.dy || 0),
1500
+ window.innerHeight - bh - TOP_MARGIN,
1501
+ );
1502
+ el.style.setProperty("top", Math.max(TOP_MARGIN, bt) + "px", "important");
1503
+ el.style.setProperty(
1504
+ "left",
1505
+ Math.max(TOP_MARGIN, bl) + "px",
1506
+ "important",
1507
+ );
1508
+ return;
1509
+ }
1510
+ var w = el.offsetWidth;
1511
+ var h = el.offsetHeight;
1512
+ var vw = window.innerWidth;
1513
+ var vh = window.innerHeight;
1514
+ var below = vh - r.bottom - gap - TOP_MARGIN;
1515
+ var above = r.top - gap - TOP_MARGIN;
1516
+ var up = h > below && above > below;
1517
+ var room = Math.max(80, up ? above : below);
1518
+ if (h > room) {
1519
+ el.style.maxHeight = room + "px";
1520
+ h = room;
1521
+ }
1522
+ var top = up ? r.top - gap - h : r.bottom + gap;
1523
+ var left = opts.align === "right" ? r.right - w : r.left;
1524
+ left = Math.max(TOP_MARGIN, Math.min(left, vw - w - TOP_MARGIN));
1525
+ // Important, so no page or UA rule (a popover's inset) moves it.
1526
+ el.style.setProperty("top", Math.max(TOP_MARGIN, top) + "px", "important");
1527
+ el.style.setProperty("left", left + "px", "important");
1528
+ }
1529
+ function topIndex(el) {
1530
+ for (var i = 0; i < topOpen.length; i++) if (topOpen[i].el === el) return i;
1531
+ return -1;
1532
+ }
1533
+ function topShow(el, anchor, opts) {
1534
+ if (!el || !anchor) return;
1535
+ opts = opts || {};
1536
+ var i = topIndex(el);
1537
+ if (i >= 0) {
1538
+ topOpen[i].anchor = anchor;
1539
+ topOpen[i].opts = opts;
1540
+ } else {
1541
+ if (topSupported(el)) {
1542
+ if (!el.hasAttribute("popover")) {
1543
+ el.setAttribute("popover", "manual");
1544
+ el.setAttribute("data-den-top-added", "");
1545
+ }
1546
+ try {
1547
+ if (!el.matches(":popover-open")) el.showPopover();
1548
+ } catch (e) {
1549
+ /* not in the document yet: placed as fixed */
1550
+ }
1551
+ }
1552
+ var o = { el: el, anchor: anchor, opts: opts, mo: null };
1553
+ if (typeof MutationObserver === "function") {
1554
+ o.mo = new MutationObserver(function () {
1555
+ topCheck(o);
1556
+ });
1557
+ o.mo.observe(el, {
1558
+ attributes: true,
1559
+ attributeFilter: ["style", "class", "hidden"],
1560
+ });
1561
+ }
1562
+ topOpen.push(o);
1563
+ }
1564
+ el.classList.add("den-toplayer");
1565
+ topPlace(el, anchor, opts);
1566
+ }
1567
+ function topHide(el) {
1568
+ var i = topIndex(el);
1569
+ if (i < 0) return;
1570
+ var o = topOpen.splice(i, 1)[0];
1571
+ if (o.mo) o.mo.disconnect();
1572
+ try {
1573
+ if (el.matches && el.matches(":popover-open")) el.hidePopover();
1574
+ } catch (e) {
1575
+ /* already gone */
1576
+ }
1577
+ if (el.hasAttribute("data-den-top-added")) {
1578
+ el.removeAttribute("popover");
1579
+ el.removeAttribute("data-den-top-added");
1580
+ }
1581
+ el.classList.remove("den-toplayer");
1582
+ el.style.removeProperty("top");
1583
+ el.style.removeProperty("left");
1584
+ }
1585
+ /** The page hid it, or its anchor went: it's closed. */
1586
+ function topCheck(o) {
1587
+ if (
1588
+ !o.el.isConnected ||
1589
+ !o.anchor.isConnected ||
1590
+ o.el.hidden ||
1591
+ getComputedStyle(o.el).display === "none"
1592
+ )
1593
+ topHide(o.el);
1594
+ }
1595
+ function topReplace() {
1596
+ topOpen.slice().forEach(function (o) {
1597
+ topCheck(o);
1598
+ if (topIndex(o.el) >= 0) topPlace(o.el, o.anchor, o.opts);
1599
+ });
1600
+ }
1601
+ window.addEventListener("resize", topReplace);
1602
+ document.addEventListener("scroll", topReplace, true);
1603
+
1414
1604
  // ── menus (.pop, R18): the scope parts' popovers ─────────────────────
1415
1605
  // A menu is { label, items: [{ heading } | { hr: true } | { label, tag?,
1416
1606
  // checked?, href?, onSelect? }] }. Rows with `checked` are radios (✓ when
@@ -1422,6 +1612,7 @@
1422
1612
  if (!openPop) return;
1423
1613
  var o = openPop;
1424
1614
  openPop = null;
1615
+ topHide(o.el);
1425
1616
  if (o.el.remove) o.el.remove();
1426
1617
  o.anchor.setAttribute("aria-expanded", "false");
1427
1618
  // aria-controls names the menu only while it exists.
@@ -1497,15 +1688,14 @@
1497
1688
  }
1498
1689
  });
1499
1690
  pop.innerHTML = html;
1500
- document.body.appendChild(pop);
1691
+ // Inside a modal, in it: anything outside a modal is inert.
1692
+ var host =
1693
+ (anchor.closest && anchor.closest("dialog[open]")) || document.body;
1694
+ host.appendChild(pop);
1501
1695
  openPop = { el: pop, anchor: anchor };
1502
1696
  anchor.setAttribute("aria-expanded", "true");
1503
1697
  anchor.setAttribute("aria-controls", pop.id);
1504
- var r = anchor.getBoundingClientRect();
1505
- var w = pop.offsetWidth;
1506
- pop.style.top = r.bottom + 6 + "px";
1507
- pop.style.left =
1508
- Math.max(8, Math.min(r.left, window.innerWidth - w - 8)) + "px";
1698
+ topShow(pop, anchor, { gap: 6 });
1509
1699
  pop.addEventListener("click", function (e) {
1510
1700
  var row = e.target.closest ? e.target.closest(".den-mi") : null;
1511
1701
  if (!row) return;
@@ -1943,7 +2133,14 @@
1943
2133
  * a link to manage them. onSelect(value, focus): choosing a workspace
1944
2134
  * also makes it the CLI default (header.js).
1945
2135
  */
2136
+ // Every workspace's name, from the Workspace menu's model (header.js).
2137
+ var workspaceNameList = [];
2138
+ function workspaceNames() {
2139
+ return workspaceNameList.slice();
2140
+ }
1946
2141
  function setWorkspaces(model) {
2142
+ workspaceNameList =
2143
+ model && Array.isArray(model.names) ? model.names.slice() : [];
1947
2144
  var btn = document.getElementById("ws-pill");
1948
2145
  if (!btn) return;
1949
2146
  var sep = btn.nextElementSibling;
@@ -1992,6 +2189,47 @@
1992
2189
  : " off";
1993
2190
  return '<span class="den-dot' + cls + '" aria-hidden="true"></span>';
1994
2191
  }
2192
+ /**
2193
+ * 0.4.18: a pin's Visit ↗: none without a URL, greyed out while it is
2194
+ * down. Down is the service panel's live state (pinOff), the same as its
2195
+ * dot, so it holds for a pin outside the View too; the rail's rollup
2196
+ * only without it.
2197
+ */
2198
+ function pinVisit(id) {
2199
+ var url = state.pinUrls && state.pinUrls[id];
2200
+ if (!url) return "";
2201
+ var off = state.pinOff || {};
2202
+ var down = Object.prototype.hasOwnProperty.call(off, id)
2203
+ ? !!off[id]
2204
+ : state.health[id] === "down";
2205
+ return (
2206
+ '<button type="button" class="den-pvisit" tabindex="-1" data-visit="' +
2207
+ esc(id) +
2208
+ '" data-url="' +
2209
+ esc(url) +
2210
+ '" aria-label="' +
2211
+ esc(t("act.visitX", { s: id })) +
2212
+ '" title="' +
2213
+ esc(down ? t("svp.notRunning") : url) +
2214
+ '"' +
2215
+ (down ? ' aria-disabled="true"' : "") +
2216
+ '><span aria-hidden="true">↗</span></button>'
2217
+ );
2218
+ }
2219
+ function visitPin(b) {
2220
+ if (b.getAttribute("aria-disabled") === "true") {
2221
+ toast(t("svp.notRunning"));
2222
+ return;
2223
+ }
2224
+ var url = b.getAttribute("data-url");
2225
+ if (url && /^https?:\/\//i.test(url))
2226
+ window.open(url, "_blank", "noopener");
2227
+ }
2228
+ // 0.4.18: the service panel (svc-panel.js registers it).
2229
+ var servicePanel = null;
2230
+ function setServicePanel(p) {
2231
+ servicePanel = p && typeof p.open === "function" ? p : null;
2232
+ }
1995
2233
  function pinHref(id) {
1996
2234
  var repo = id.split("/")[0];
1997
2235
  return "/services#repo-" + encodeURIComponent(repo);
@@ -2060,9 +2298,12 @@
2060
2298
  null,
2061
2299
  state.pins
2062
2300
  .map(function (p) {
2063
- // The row is the link (named by the id alone, Delete unpins
2064
- // it); the × beside it is its own control, out of the tab
2065
- // order, named "Unpin <id>", its glyph hidden from readers.
2301
+ // 0.4.18: the row (named by the id alone) opens the service's
2302
+ // panel in place (svc-panel.js); it is still the link to
2303
+ // Services, so ⌘-click and middle-click open that in a new
2304
+ // tab. Delete unpins it. Visit ↗ beside it opens the service's
2305
+ // URL, out of the tab order: the panel's first button is the
2306
+ // keyboard's Visit. Unpin is in the panel's footer.
2066
2307
  return (
2067
2308
  '<div class="den-pin-row">' +
2068
2309
  item(
@@ -2074,17 +2315,17 @@
2074
2315
  mono: true,
2075
2316
  },
2076
2317
  false,
2077
- // Holds the ×'s place in the row, as in the prototype.
2078
- '<span class="den-pin-sp" aria-hidden="true">×</span>',
2079
- ' data-pin="' + esc(p) + '" aria-keyshortcuts="Delete"',
2318
+ "",
2319
+ ' data-pin="' +
2320
+ esc(p) +
2321
+ '" data-svpanel="' +
2322
+ esc(p) +
2323
+ '" aria-haspopup="dialog" aria-expanded="' +
2324
+ (state.svpOpen === p ? "true" : "false") +
2325
+ '" aria-keyshortcuts="Delete"',
2080
2326
  ) +
2081
- '<button type="button" class="den-pin-x" tabindex="-1" data-unpin="' +
2082
- esc(p) +
2083
- '" aria-label="' +
2084
- esc(t("act.unpinX", { s: p })) +
2085
- '" title="' +
2086
- esc(t("act.unpin")) +
2087
- '"><span aria-hidden="true">×</span></button></div>'
2327
+ pinVisit(p) +
2328
+ "</div>"
2088
2329
  );
2089
2330
  })
2090
2331
  .join(""),
@@ -2292,10 +2533,28 @@
2292
2533
  });
2293
2534
  nav.addEventListener("click", function (e) {
2294
2535
  var tgt = e.target;
2295
- var x = tgt && tgt.closest ? tgt.closest("[data-unpin]") : null;
2296
- if (x) {
2536
+ var v = tgt && tgt.closest ? tgt.closest("[data-visit]") : null;
2537
+ if (v) {
2297
2538
  e.preventDefault();
2298
- unpinFromRail(x.getAttribute("data-unpin"));
2539
+ visitPin(v);
2540
+ return;
2541
+ }
2542
+ // 0.4.18: a plain click (or Enter) on a pinned row opens its panel;
2543
+ // ⌘/Ctrl-, Shift- and middle-click keep the link (Services).
2544
+ var sp = tgt && tgt.closest ? tgt.closest("[data-svpanel]") : null;
2545
+ if (
2546
+ sp &&
2547
+ servicePanel &&
2548
+ e.button === 0 &&
2549
+ !e.metaKey &&
2550
+ !e.ctrlKey &&
2551
+ !e.shiftKey &&
2552
+ !e.altKey
2553
+ ) {
2554
+ e.preventDefault();
2555
+ servicePanel.open(sp.getAttribute("data-svpanel"), sp, {
2556
+ fromRail: true,
2557
+ });
2299
2558
  return;
2300
2559
  }
2301
2560
  var c = tgt && tgt.closest ? tgt.closest('[data-act="collapse"]') : null;
@@ -4377,6 +4636,7 @@
4377
4636
  setOrg: setOrg,
4378
4637
  setOrgs: setOrgs,
4379
4638
  setWorkspaces: setWorkspaces,
4639
+ workspaceNames: workspaceNames,
4380
4640
  /** Repaint the Env part from #env-sel (after setting its value). */
4381
4641
  syncEnv: syncEnv,
4382
4642
  syncPinToggles: syncPinToggles,
@@ -4385,6 +4645,20 @@
4385
4645
  commands: commands,
4386
4646
  /** A .pop menu below `anchor` (the model as the scope menus'). */
4387
4647
  openMenu: openMenu,
4648
+ /**
4649
+ * The top layer (0.4.17): show(el, anchor, { align: "left"|"right",
4650
+ * gap }) puts a page's own menu above everything, placed by its
4651
+ * anchor; hide(el). The page hiding it (display: none) closes it too.
4652
+ */
4653
+ topLayer: { show: topShow, hide: topHide },
4654
+ /**
4655
+ * 0.4.18: the service panel. svc-panel.js registers { open, close,
4656
+ * visit }; openServicePanel(id, opener) opens it beside opener.
4657
+ */
4658
+ setServicePanel: setServicePanel,
4659
+ openServicePanel: function (id, opener) {
4660
+ if (servicePanel) servicePanel.open(id, opener);
4661
+ },
4388
4662
  closeMenu: function () {
4389
4663
  closeMenu(false);
4390
4664
  },
@@ -4544,6 +4818,13 @@
4544
4818
  " max-height: calc(100vh - 72px); overflow-y: auto; box-sizing: border-box;",
4545
4819
  " font: 14px/1.45 var(--sans); color: var(--text); }",
4546
4820
  ".den-pop:focus { outline: none; }",
4821
+ // The top layer (0.4.17): a page's menu placed by its anchor (topShow), not
4822
+ // where the UA puts a popover.
4823
+ // No inset here: top and left are topPlace's (an !important inset beat them,
4824
+ // and every menu sat at the window's corner, 0.4.17-dev.0).
4825
+ ".den-toplayer { position: fixed !important; margin: 0 !important;",
4826
+ " right: auto !important; bottom: auto !important; z-index: 1000; overflow-y: auto;",
4827
+ " color: var(--text); }",
4547
4828
  // LK-1: the admin-password prompt is a <dialog> in the top layer (a popover, or
4548
4829
  // a modal over another modal): placed by its top/left, no UA box.
4549
4830
  "dialog.den-pop { margin: 0; inset: auto; max-width: none; }",
@@ -4720,18 +5001,65 @@
4720
5001
  " display: inline-block; margin: 0 5px; }",
4721
5002
  ".den-dot.bad { background: var(--bad); } .den-dot.warn { background: var(--warn); }",
4722
5003
  ".den-dot.off { background: var(--faint); }",
4723
- ".den-pin-row { position: relative; }",
4724
- ".den-pin-sp { visibility: hidden; font-size: 12px; }",
4725
- ".den-pin-x { position: absolute; top: 0; bottom: 0; right: 10px; display: flex; align-items: center;",
4726
- " padding: 0; border: 0; background: none; cursor: pointer; font: 12px var(--sans);",
4727
- " opacity: 0; color: var(--faint); }",
4728
- ".den-pin-x:hover { color: var(--text); }",
4729
- ".den-pin-row:hover .den-pin-x, .den-pin-row:focus-within .den-pin-x { opacity: 1; }",
5004
+ // 0.4.18: a pinned row is its name (opens the service panel) and Visit ↗.
5005
+ ".den-pin-row { display: flex; align-items: center; gap: 2px; min-width: 0; }",
5006
+ ".den-pin-row .den-ni { flex: 1; min-width: 0; }",
5007
+ ".den-pin-row .den-ni[aria-expanded='true'] { background: var(--sunken); color: var(--text); }",
5008
+ ".den-pvisit { flex: none; width: 26px; height: 26px; padding: 0; display: grid; place-items: center;",
5009
+ " background: none; border: 1px solid transparent; border-radius: var(--radius-button);",
5010
+ " color: var(--muted); cursor: pointer; font: 13px/1 var(--sans); }",
5011
+ ".den-pvisit:hover { background: var(--sunken); color: var(--accent-text); border-color: var(--border); }",
5012
+ ".den-pvisit[aria-disabled='true'], .den-svp .den-btn[aria-disabled='true'] { opacity: .45; cursor: not-allowed; }",
5013
+ ".den-pvisit[aria-disabled='true']:hover { background: none; color: var(--muted); border-color: transparent; }",
5014
+ "body.den-rail-collapsed .den-rail .den-pvisit { display: none; }",
5015
+ "@media (max-width: 720px) { .den-pvisit { width: 32px; height: 32px; }",
5016
+ " body.den-rail-collapsed .den-rail .den-pvisit { display: grid; } }",
5017
+ // The service panel (svc-panel.js): beside its row, a bottom sheet at phone width.
5018
+ ".den-pop.den-svp { width: 340px; max-width: calc(100vw - 16px); box-sizing: border-box; padding: 0;",
5019
+ " max-height: calc(100dvh - 16px); overflow-y: auto; min-width: 0; }",
5020
+ ".den-svp-h { display: grid; gap: 4px; padding: 12px 12px 10px; border-bottom: 1px solid var(--border); }",
5021
+ ".den-svp-t { display: flex; align-items: center; gap: 8px; min-width: 0; }",
5022
+ ".den-svp-t .den-dot, .den-svp-ag .den-dot, .den-svp-line .den-dot { margin: 0; }",
5023
+ // Line heights pinned to the mock's (its mono font's normal), whatever the theme's font.
5024
+ ".den-svp-t b { flex: 1; min-width: 0; font: 600 14px/21px var(--mono); overflow: hidden;",
5025
+ " text-overflow: ellipsis; white-space: nowrap; }",
5026
+ ".den-svp-x { padding: 2px 6px; margin: -4px -6px -4px 0; }",
5027
+ ".den-svp-meta { display: flex; align-items: center; gap: 4px 8px; flex-wrap: wrap; font-size: 12.5px; color: var(--muted); }",
5028
+ ".den-wsp { font: 11px/16px var(--mono); border-radius: 999px; padding: 0 7px; white-space: nowrap;",
5029
+ " border: 1px solid var(--border); color: var(--muted); }",
5030
+ ".den-svp-st { color: var(--ok); } .den-svp-st.bad { color: var(--bad); }",
5031
+ ".den-svp-st.off { color: var(--faint); } .den-svp-st.run { color: var(--accent-text); }",
5032
+ ".den-svp-ms { font: 11.5px var(--mono); color: var(--faint); }",
5033
+ ".den-svp-url { font: 12px/17px var(--mono); color: var(--faint); overflow-wrap: anywhere; }",
5034
+ ".den-svp-acts { display: flex; gap: 6px; flex-wrap: wrap; padding: 10px 12px; }",
5035
+ ".den-svp-sec { display: grid; gap: 6px; padding: 8px 12px 10px; border-top: 1px solid var(--border); }",
5036
+ ".den-svp-k { font: 11px/16px var(--mono); color: var(--faint); text-transform: uppercase; letter-spacing: .07em; }",
5037
+ ".den-svp-line { display: flex; align-items: center; gap: 8px; font-size: 13px; }",
5038
+ ".den-svp-line .den-svp-sp { margin-left: auto; }",
5039
+ ".den-svp-ok { color: var(--ok); } .den-svp-bad { color: var(--bad); } .den-svp-none { color: var(--faint); }",
5040
+ ".den-svp-ag { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; gap: 0 8px;",
5041
+ " align-items: center; font-size: 13px; }",
5042
+ ".den-svp-nm { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
5043
+ ".den-svp-as { font: 11px var(--mono); color: var(--faint); white-space: nowrap; }",
5044
+ ".den-wbdg { font: 10.5px var(--mono); color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);",
5045
+ " border-radius: 999px; padding: 0 6px; }",
5046
+ ".den-svp-help { font-size: 12px; color: var(--faint); line-height: 1.4; }",
5047
+ ".den-svp-foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 8px 12px 10px;",
5048
+ " border-top: 1px solid var(--border); }",
5049
+ ".den-svp-grow { margin-left: auto; }",
5050
+ ".den-lnk { color: var(--accent-text); cursor: pointer; font: inherit; font-size: 12.5px; text-decoration: underline;",
5051
+ " background: none; border: 0; padding: 0; }",
5052
+ ".den-scrim.den-svp-scrim { z-index: 43; }",
5053
+ ".den-pop.den-svp.den-sheet.den-toplayer { top: auto !important; left: 0 !important; right: 0 !important;",
5054
+ " bottom: 0 !important; width: auto; max-width: none; border-bottom: 0; border-radius: 14px 14px 0 0;",
5055
+ " max-height: 85dvh; padding-bottom: env(safe-area-inset-bottom, 0px); }",
5056
+ ".den-pop.den-svp.den-sheet::before { content: ''; display: block; width: 36px; height: 4px; border-radius: 2px;",
5057
+ " background: var(--border-strong); margin: 8px auto 0; }",
5058
+ ".den-pop.den-svp.den-sheet .den-svp-acts .den-btn { flex: 1 1 auto; padding: 9px 12px; }",
4730
5059
  // Collapsed: icons only. The prototype hides headings and labels; they
4731
5060
  // stay here for screen readers (visually hidden, no space taken).
4732
5061
  "body.den-rail-collapsed .den-rail .den-lbl, body.den-rail-collapsed .den-rail .den-gh,",
4733
- "body.den-rail-collapsed .den-rail .den-bdg, body.den-rail-collapsed .den-rail .den-pin-x,",
4734
- "body.den-rail-collapsed .den-rail .den-pin-sp {",
5062
+ "body.den-rail-collapsed .den-rail .den-bdg {",
4735
5063
  " position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden;",
4736
5064
  " clip: rect(0 0 0 0); white-space: nowrap; border: 0; }",
4737
5065
  // Toast (.toast)