@monster-orange/den 0.4.23 → 0.4.25

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.
@@ -451,9 +451,115 @@
451
451
  min-width: 0;
452
452
  max-width: 100%;
453
453
  }
454
+ /* A select never cuts its own words: it wraps to its own line. */
455
+ .rot-f select {
456
+ min-width: min(100%, max-content);
457
+ }
454
458
  .rot-add {
455
459
  margin-top: 10px;
456
460
  }
461
+ /* HUB-1.5: Settings › Agents › Hub. */
462
+ .hub-st {
463
+ display: grid;
464
+ grid-template-columns: max-content 1fr;
465
+ gap: 4px 12px;
466
+ margin: 0;
467
+ }
468
+ .hub-st dt {
469
+ color: var(--muted);
470
+ }
471
+ .hub-st dd {
472
+ margin: 0;
473
+ min-width: 0;
474
+ overflow-wrap: anywhere;
475
+ }
476
+ .hub-tools {
477
+ display: flex;
478
+ gap: 8px;
479
+ align-items: center;
480
+ margin-top: 10px;
481
+ }
482
+ /* D30: the add form: each control with its label, wrapping. */
483
+ .rot-pick {
484
+ display: inline-flex;
485
+ align-items: center;
486
+ gap: 6px;
487
+ min-width: 0;
488
+ flex: 1 1 200px;
489
+ flex-wrap: wrap;
490
+ }
491
+ .rot-pick[hidden] {
492
+ display: none;
493
+ }
494
+ .rot-pick input[type="number"] {
495
+ width: 6em;
496
+ flex: 0 0 auto;
497
+ }
498
+ .rot-never {
499
+ display: inline-flex;
500
+ align-items: center;
501
+ gap: 4px;
502
+ }
503
+ .rot-gone {
504
+ color: var(--muted);
505
+ }
506
+ /* The shared picker (combobox.js). */
507
+ .den-combo {
508
+ display: inline-flex;
509
+ align-items: center;
510
+ gap: 6px;
511
+ min-width: 0;
512
+ flex: 1 1 200px;
513
+ flex-wrap: wrap;
514
+ }
515
+ .den-combo-box {
516
+ position: relative;
517
+ flex: 1 1 160px;
518
+ min-width: 0;
519
+ }
520
+ .den-combo-box input {
521
+ width: 100%;
522
+ box-sizing: border-box;
523
+ }
524
+ .den-combo-list {
525
+ position: absolute;
526
+ z-index: 20;
527
+ left: 0;
528
+ right: 0;
529
+ top: calc(100% + 2px);
530
+ max-height: 240px;
531
+ overflow-y: auto;
532
+ margin: 0;
533
+ padding: 4px;
534
+ list-style: none;
535
+ background: var(--surface);
536
+ border: 1px solid var(--border);
537
+ border-radius: 8px;
538
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
539
+ }
540
+ .den-combo-list[hidden] {
541
+ display: none;
542
+ }
543
+ .den-combo-list li {
544
+ display: flex;
545
+ flex-wrap: wrap;
546
+ justify-content: space-between;
547
+ gap: 2px 8px;
548
+ padding: 5px 8px;
549
+ border-radius: 6px;
550
+ cursor: pointer;
551
+ }
552
+ .den-combo-list li[aria-selected="true"] {
553
+ background: var(--sunken);
554
+ }
555
+ .den-combo-list li[aria-disabled="true"] {
556
+ color: var(--muted);
557
+ cursor: default;
558
+ }
559
+ .den-combo-s {
560
+ color: var(--muted);
561
+ font-size: 12px;
562
+ }
457
563
  .rot-ov caption {
458
564
  text-align: left;
459
565
  color: var(--muted);
@@ -464,7 +570,49 @@
464
570
  .rot-ov td {
465
571
  padding: 4px 10px 4px 0;
466
572
  text-align: left;
467
- overflow-wrap: anywhere;
573
+ /* Break a key at its "_", never mid-word. */
574
+ overflow-wrap: normal;
575
+ word-break: normal;
576
+ }
577
+ /* Narrow: each override is a card of label/value lines, so no
578
+ column is squeezed into breaking words mid-letter. */
579
+ @media (max-width: 560px) {
580
+ .rot-ov thead {
581
+ position: absolute;
582
+ width: 1px;
583
+ height: 1px;
584
+ overflow: hidden;
585
+ clip: rect(0 0 0 0);
586
+ }
587
+ .rot-ov,
588
+ .rot-ov tbody,
589
+ .rot-ov tr,
590
+ .rot-ov td {
591
+ display: block;
592
+ width: 100%;
593
+ }
594
+ .rot-ov caption {
595
+ display: block;
596
+ }
597
+ .rot-ov tr[data-rot-row] {
598
+ padding: 6px 0;
599
+ border-top: 1px solid var(--border);
600
+ }
601
+ .rot-ov td {
602
+ display: flex;
603
+ gap: 8px;
604
+ padding: 2px 0;
605
+ overflow-wrap: anywhere;
606
+ }
607
+ .rot-ov td[data-l]::before {
608
+ content: attr(data-l);
609
+ flex: 0 0 64px;
610
+ color: var(--muted);
611
+ font-family: inherit;
612
+ }
613
+ .rot-ov td.rot-act {
614
+ padding-top: 4px;
615
+ }
468
616
  }
469
617
  .rt-fl {
470
618
  flex-basis: 100%;
@@ -426,9 +426,10 @@
426
426
  "err.signin.claimOrg":
427
427
  "Members of the {org} GitHub org can claim it, in a browser on {host}.",
428
428
  "err.signin.claimHere": "This is my computer: connect GitHub",
429
+ "err.signin.claimHereOn": "On {host} itself: connect GitHub",
430
+ "err.signin.claimOrgOnly":
431
+ "Members of the {org} GitHub org can claim it, from a browser on {host}. Open this page on {host}, or set web.owner_github to sign in from anywhere.",
429
432
  "err.signin.claimAs": "Sign in as @{login}",
430
- "err.signin.claimHereNote":
431
- "This is my computer works only in a browser on {host}. Anywhere else it opens nothing.",
432
433
  "err.signin.claimChecking": "Checking this is {host}…",
433
434
  "err.signin.githubRefused":
434
435
  "@{login} isn't the owner of this {name}. Only the owner's account can use Sign in with GitHub. Use Approve this computer instead.",
@@ -855,6 +856,7 @@
855
856
  "ver.usedLatest": "{n} now tracks the latest version",
856
857
  "set.agents.personas": "Personas",
857
858
  "set.agents.rotation": "Rotation",
859
+ "set.agents.hub": "Hub",
858
860
  // The Agents tab (AG-4a-term, agents-page.js).
859
861
  "n.agents": "Agents",
860
862
  "kind.agent": "Agent",
@@ -2320,28 +2322,31 @@
2320
2322
  var down = Object.prototype.hasOwnProperty.call(off, id)
2321
2323
  ? !!off[id]
2322
2324
  : state.health[id] === "down";
2325
+ // D27: a real link (never popup-blocked, as window.open was in Safari).
2326
+ var live = !down && /^https?:\/\//i.test(url);
2323
2327
  return (
2324
- '<button type="button" class="den-pvisit" tabindex="-1" data-visit="' +
2328
+ '<a class="den-pvisit" tabindex="-1" data-visit="' +
2325
2329
  esc(id) +
2326
2330
  '" data-url="' +
2327
2331
  esc(url) +
2328
- '" aria-label="' +
2332
+ '"' +
2333
+ (live
2334
+ ? ' href="' + esc(url) + '" target="_blank" rel="noopener noreferrer"'
2335
+ : ' role="link" aria-disabled="true"') +
2336
+ ' aria-label="' +
2329
2337
  esc(t("act.visitX", { s: id })) +
2330
2338
  '" title="' +
2331
2339
  esc(down ? t("svp.notRunning") : url) +
2332
- '"' +
2333
- (down ? ' aria-disabled="true"' : "") +
2334
- '><span aria-hidden="true">↗</span></button>'
2340
+ '"><span aria-hidden="true">↗</span></a>'
2335
2341
  );
2336
2342
  }
2343
+ /** A pin's Visit ↗ clicked: true when the link opens by itself. */
2337
2344
  function visitPin(b) {
2338
- if (b.getAttribute("aria-disabled") === "true") {
2345
+ if (b.getAttribute("aria-disabled") === "true" || !b.hasAttribute("href")) {
2339
2346
  toast(t("svp.notRunning"));
2340
- return;
2347
+ return false;
2341
2348
  }
2342
- var url = b.getAttribute("data-url");
2343
- if (url && /^https?:\/\//i.test(url))
2344
- window.open(url, "_blank", "noopener");
2349
+ return true;
2345
2350
  }
2346
2351
  // 0.4.18: the service panel (svc-panel.js registers it).
2347
2352
  var servicePanel = null;
@@ -2653,8 +2658,7 @@
2653
2658
  var tgt = e.target;
2654
2659
  var v = tgt && tgt.closest ? tgt.closest("[data-visit]") : null;
2655
2660
  if (v) {
2656
- e.preventDefault();
2657
- visitPin(v);
2661
+ if (!visitPin(v)) e.preventDefault();
2658
2662
  return;
2659
2663
  }
2660
2664
  // 0.4.18: a plain click (or Enter) on a pinned row opens its panel;
@@ -5018,6 +5022,9 @@
5018
5022
  ".den-btn.primary { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--on-accent); }",
5019
5023
  ".den-btn.sm { padding: 2px 9px; font-size: 12.5px; }",
5020
5024
  ".den-btn:disabled { opacity: .6; cursor: default; }",
5025
+ // D27: Visit is a link styled as a button.
5026
+ "a.den-btn, a.den-pvisit { text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }",
5027
+ "a.den-pvisit { display: grid; }",
5021
5028
  // Status drawer (.scrim, .drawer, .prob-card, .grp, .svl: R15)
5022
5029
  ".den-scrim { position: fixed; inset: 0; background: rgb(0 0 0 / 0.35); z-index: 40; }",
5023
5030
  ".den-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(420px, 100%); z-index: 41;",
@@ -67,6 +67,9 @@ const S = {
67
67
  testing: new Set(),
68
68
  /** The open panel: { id, el, opener, scrim, timer }. */
69
69
  open: null,
70
+ /** D27: a pointer is down inside the panel: repaint after it is up. */
71
+ pressed: false,
72
+ syncLater: false,
70
73
  };
71
74
 
72
75
  /** Start or Restart from here: "starting…" until health says, or the timeout. */
@@ -228,14 +231,21 @@ function testsHtml(s) {
228
231
  return `<span class="den-svp-${ok ? "ok" : "bad"}">${esc(t(ok ? "svp.testsOk" : "svp.testsBad", { t: at }))}</span>`;
229
232
  }
230
233
 
231
- /** A Visit button (the rail row's ↗, or the panel's primary). */
234
+ /**
235
+ * Visit: a real link, styled as a button (D27). A native target=_blank link
236
+ * is never popup-blocked (Safari blocked window.open from the delegated
237
+ * click) and works with no script. Off (down, starting): no href,
238
+ * aria-disabled, and a click says why (visit()).
239
+ */
232
240
  export function visitHtml(s, cls, label) {
233
241
  if (!s.url) return "";
234
- const on = visitable(s);
242
+ const on = visitable(s) && /^https?:\/\//i.test(s.url);
235
243
  return (
236
- `<button type="button" class="${cls}" data-visit="${esc(s.id)}" data-url="${esc(s.url)}"` +
237
- ` aria-label="${esc(t("act.visitX", { s: s.id }))}" title="${esc(on ? s.url : t("svp.notRunning"))}"` +
238
- `${on ? "" : ' aria-disabled="true"'}>${label}</button>`
244
+ `<a class="${cls}" data-visit="${esc(s.id)}" data-url="${esc(s.url)}"` +
245
+ (on
246
+ ? ` href="${esc(s.url)}" target="_blank" rel="noopener noreferrer"`
247
+ : ` role="link" tabindex="0" aria-disabled="true"`) +
248
+ ` aria-label="${esc(t("act.visitX", { s: s.id }))}" title="${esc(on ? s.url : t("svp.notRunning"))}">${label}</a>`
239
249
  );
240
250
  }
241
251
 
@@ -335,6 +345,12 @@ function sync() {
335
345
  publishUrls();
336
346
  const o = S.open;
337
347
  if (!o) return;
348
+ // D27: never repaint under a press: the button pressed would be replaced
349
+ // between pointerdown and click, and the click would land on the panel.
350
+ if (S.pressed) {
351
+ S.syncLater = true;
352
+ return;
353
+ }
338
354
  const a = document.activeElement;
339
355
  const mine = a && o.el.contains(a) && a !== o.el;
340
356
  const key = mine
@@ -451,15 +467,20 @@ export function panelOpen() {
451
467
  const env = () => S.deps.currentEnv?.() ?? "local";
452
468
  const runs = () => window.denShell?.runs;
453
469
 
454
- /** Visit: the service's URL in a new tab, or why not. */
470
+ /**
471
+ * Visit, clicked: a live link opens by itself (the browser follows it, so
472
+ * true: leave the click alone); an off one says why (false: the caller
473
+ * prevents the click).
474
+ */
455
475
  export function visit(btn) {
456
- if (btn.getAttribute("aria-disabled") === "true") {
476
+ if (
477
+ btn.getAttribute("aria-disabled") === "true" ||
478
+ !btn.hasAttribute("href")
479
+ ) {
457
480
  window.denShell?.toast?.(t("svp.notRunning"));
458
- return;
481
+ return false;
459
482
  }
460
- const url = btn.dataset.url;
461
- if (!url || !/^https?:\/\//i.test(url)) return;
462
- window.open(url, "_blank", "noopener");
483
+ return true;
463
484
  }
464
485
 
465
486
  /** A stream into a runs-dock tab: Start, Stop, Restart (exec), Logs, tests. */
@@ -635,6 +656,27 @@ export function initServicePanel(deps = {}) {
635
656
  sync();
636
657
  });
637
658
  window.addEventListener("den:pins", () => sync());
659
+ // D27: a press inside the open panel holds its repaint until it ends.
660
+ document.addEventListener(
661
+ "pointerdown",
662
+ (e) => {
663
+ if (S.open?.el.contains(e.target)) S.pressed = true;
664
+ },
665
+ true,
666
+ );
667
+ const released = () => {
668
+ if (!S.pressed) return;
669
+ S.pressed = false;
670
+ // After the click this press makes, then the repaint it held.
671
+ setTimeout(() => {
672
+ if (S.syncLater) {
673
+ S.syncLater = false;
674
+ sync();
675
+ }
676
+ }, 0);
677
+ };
678
+ document.addEventListener("pointerup", released, true);
679
+ document.addEventListener("pointercancel", released, true);
638
680
  document.addEventListener(
639
681
  "click",
640
682
  (e) => {
@@ -643,10 +685,7 @@ export function initServicePanel(deps = {}) {
643
685
  // The panel's Visit here; the rail's ↗ is shell.js's (the same visit()).
644
686
  const v = tgt.closest?.("[data-visit]");
645
687
  if (v) {
646
- if (o?.el.contains(v)) {
647
- e.preventDefault();
648
- visit(v);
649
- }
688
+ if (o?.el.contains(v) && !visit(v)) e.preventDefault();
650
689
  return;
651
690
  }
652
691
  const opener = tgt.closest?.("[data-svpanel-open]");
@@ -388,6 +388,10 @@ export const ROUTES: readonly RouteEntry[] = [
388
388
  "/api/agents/rotation",
389
389
  "D28: rotation settings, the overrides and an agent's effective values",
390
390
  ),
391
+ R(
392
+ "/api/hub",
393
+ "HUB-1.5: Den Hub: its URL, connected or not, the team and the install's label (never the token)",
394
+ ),
391
395
  M(
392
396
  ["PUT"],
393
397
  "/api/agents/rotation",
@@ -52,6 +52,8 @@ export const UI_MODULES: readonly string[] = [
52
52
  "/server-agent-page.js",
53
53
  // 0.4.18: the service panel (rail pins, Graph ⋯, Services names).
54
54
  "/svc-panel.js",
55
+ // D30: the shared searchable picker (Settings › Agents › Rotation).
56
+ "/combobox.js",
55
57
  // 0.4.18: where a service's live log comes from (Logs page and panel).
56
58
  "/log-source.js",
57
59
  ];