@cmgmyr/hive 1.9.1 → 1.10.0

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/dist/dashboard.js CHANGED
@@ -49,7 +49,7 @@ function formatDate(d) {
49
49
  return (`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ` +
50
50
  `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`);
51
51
  }
52
- function formatLocal(utc) {
52
+ export function formatLocal(utc) {
53
53
  if (!utc)
54
54
  return "-";
55
55
  const d = new Date(`${utc.replace(" ", "T")}Z`);
@@ -559,7 +559,7 @@ function renderCappedList(shownCount, total, pluralNoun, listClass, rowsHtml, em
559
559
  ? `<ul class="rows ${listClass}"${listId ? ` id="${listId}"` : ""}>${rowsHtml}</ul>`
560
560
  : emptyHtml));
561
561
  }
562
- function chevron() {
562
+ export function chevron() {
563
563
  return `<span class="chev">${icon("chevron")}</span>`;
564
564
  }
565
565
  const DEFAULT_OPEN_SECTIONS = new Set(["throughput", "board", "todos"]);
@@ -583,7 +583,7 @@ function renderSectionNav(metas) {
583
583
  .join("");
584
584
  return `<nav class="secnav" aria-label="dashboard sections"><ul>${links}</ul></nav>`;
585
585
  }
586
- const SPRITE = `<svg class="sprite" aria-hidden="true" focusable="false"><defs>
586
+ export const SPRITE = `<svg class="sprite" aria-hidden="true" focusable="false"><defs>
587
587
  <symbol id="i-chevron" viewBox="0 0 16 16"><path d="M4 6.5 8 10.5l4-4"/></symbol>
588
588
  <symbol id="i-users" viewBox="0 0 16 16"><circle cx="6" cy="5.2" r="2.4"/><path d="M1.9 13.4a4.1 4.1 0 0 1 8.2 0"/><path d="M11 3.2a2.4 2.4 0 0 1 0 4.4"/><path d="M12.1 9.6a4.1 4.1 0 0 1 2 3.8"/></symbol>
589
589
  <symbol id="i-clock" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.2"/><path d="M8 4.4V8l2.6 1.7"/></symbol>
@@ -598,6 +598,123 @@ const SPRITE = `<svg class="sprite" aria-hidden="true" focusable="false"><defs>
598
598
  <symbol id="i-inbox" viewBox="0 0 16 16"><path d="M1.9 9.2h3.3l1 2h3.6l1-2h3.3"/><path d="M3.6 2.9h8.8l1.7 6.3v3.3a1.6 1.6 0 0 1-1.6 1.6H3.5a1.6 1.6 0 0 1-1.6-1.6V9.2Z"/></symbol>
599
599
  <symbol id="i-info" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.2"/><path d="M8 7.4v3.6M8 5.2v.1"/></symbol>
600
600
  </defs></svg>`;
601
+ export const SECTION_STYLE = `
602
+ [hidden] { display: none !important; }
603
+ svg.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
604
+ .icon { width: 1em; height: 1em; flex: 0 0 auto; fill: none; stroke: currentColor;
605
+ stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
606
+ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
607
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
608
+ .mono { font-family: var(--font-mono); font-size: 0.9em; font-variant-numeric: tabular-nums; }
609
+ .prose { overflow-wrap: anywhere; }
610
+
611
+ .section { margin: 0 0 0.6rem; background: var(--panel); border: 1px solid var(--card-border);
612
+ border-radius: var(--r-card); box-shadow: var(--shadow);
613
+ scroll-margin-top: calc(var(--header-offset) + 0.75rem); }
614
+ summary { cursor: pointer; list-style: none; }
615
+ summary::-webkit-details-marker { display: none; }
616
+ .section > summary { display: flex; align-items: center; gap: 0.55rem; padding: 0.85rem 1rem;
617
+ border-radius: var(--r-card); font-size: 0.9375rem;
618
+ transition: background-color 130ms ease; }
619
+ .sec-icon { display: inline-flex; color: var(--accent); font-size: 1.0625rem; }
620
+ .section > summary .pb-label { font-weight: 600; letter-spacing: -0.006em; }
621
+ .pb-count { color: var(--fg-subtle); font-size: 0.78125rem; font-variant-numeric: tabular-nums;
622
+ margin-left: auto; text-align: right; }
623
+ .chev { display: inline-flex; color: var(--fg-subtle); font-size: 1rem; margin-left: 0.15rem;
624
+ transition: transform 180ms var(--ease-out); }
625
+ details[open] > summary .chev { transform: rotate(180deg); }
626
+ summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; border-radius: var(--r-ctl); }
627
+ .section-body { padding: 0 1rem 1rem; }
628
+ .section > .section-body { border-top: 1px solid var(--border); padding-top: 0.9rem; margin-top: -0.15rem; }
629
+
630
+ .row-summary { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.6rem;
631
+ border-radius: var(--r-ctl); font-size: 0.875rem;
632
+ transition: background-color 130ms ease; }
633
+ .row-summary .pb-label { display: flex; align-items: center; gap: 0.42rem; flex-wrap: wrap;
634
+ min-width: 0; }
635
+ .row-summary .icon { color: var(--fg-subtle); }
636
+ details.pad-item, details.todo-item { margin: 0; }
637
+ details.pad-item > .section-body, details.todo-item > .section-body { padding: 0.1rem 0.6rem 0.7rem 0.6rem; }
638
+ .todo-body { overflow-wrap: anywhere; white-space: pre-wrap; color: var(--fg-muted);
639
+ font-size: 0.875rem; }
640
+ .todo-body + .todo-body { margin-top: 0.55rem; }
641
+
642
+ .meta, .muted { color: var(--fg-muted); }
643
+ .meta { font-size: 0.8125rem; margin: 0 0 0.75rem; max-width: 72ch; }
644
+ .muted { font-size: 0.8125rem; }
645
+ .cap-note { display: flex; align-items: center; gap: 0.4rem; color: var(--fg-subtle);
646
+ font-size: 0.78125rem; margin: 0 0 0.6rem; }
647
+ pre.board { white-space: pre-wrap; overflow-wrap: anywhere; overflow: auto; max-height: 60vh; background: var(--panel-sunken);
648
+ border: 1px solid var(--border); border-radius: var(--r-ctl); padding: 0.85rem;
649
+ font-family: var(--font-mono); font-size: 0.8125rem; line-height: 1.5; margin: 0;
650
+ overscroll-behavior: contain; }
651
+
652
+ ul.rows { list-style: none; margin: 0; padding: 0; }
653
+ ul.rows > li { padding: 0.35rem 0; border-bottom: 1px solid var(--border);
654
+ display: flex; flex-direction: column; gap: 0.15rem; }
655
+ ul.rows > li:last-child { border-bottom: none; }
656
+ ul.rows > li.todo, ul.rows > li:has(> details) { padding: 0.1rem 0; }
657
+ .row-main { display: flex; align-items: center; gap: 0.42rem; flex-wrap: wrap; }
658
+ .row-meta { font-size: 0.78125rem; color: var(--fg-subtle); }
659
+ .kind { color: var(--fg-subtle); font-size: 0.78125rem; }
660
+
661
+ .status { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.71875rem;
662
+ font-weight: 600; letter-spacing: 0.01em; padding: 0.12rem 0.5rem; border-radius: var(--r-pill);
663
+ white-space: nowrap; }
664
+ .status::before { content: ""; width: 0.4rem; height: 0.4rem; border-radius: 50%;
665
+ background: currentColor; flex: 0 0 auto; }
666
+ .status-ok { color: var(--ok); background: var(--ok-bg); }
667
+ .status-warn { color: var(--warn); background: var(--warn-bg); }
668
+ .status-fail { color: var(--fail); background: var(--fail-bg); }
669
+ .status-live { color: var(--live); background: var(--live-bg); }
670
+
671
+ .chip { display: inline-flex; align-items: center; gap: 0.28rem; font-size: 0.71875rem;
672
+ font-weight: 600; padding: 0.12rem 0.48rem; border-radius: var(--r-pill); white-space: nowrap; }
673
+ .chip-blocked { color: var(--warn); background: var(--warn-bg); }
674
+ .chip-blocked .icon { stroke-width: 1.6; }
675
+
676
+ .tag { font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
677
+ padding: 0.1rem 0.4rem; border-radius: 5px; background: var(--panel-sunken); color: var(--fg-subtle); }
678
+
679
+ .priority { font-size: 0.6875rem; font-weight: 650; letter-spacing: 0.04em; text-transform: uppercase; }
680
+ .priority-high { color: var(--fail); }
681
+ .priority-medium { color: var(--fg-muted); }
682
+ .priority-low { color: var(--fg-subtle); }
683
+ .blockers { font-size: 0.8125rem; color: var(--fg-muted); margin: 0 0 0.45rem 0.6rem; }
684
+ .wake-body { color: var(--fg); font-size: 0.875rem; overflow-wrap: anywhere; }
685
+ .activity-body { font-size: 0.8125rem; color: var(--fg-muted); white-space: pre-wrap;
686
+ overflow-wrap: anywhere; margin-top: 0.1rem; }
687
+
688
+ .filter { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
689
+ .filter-field { display: flex; align-items: center; gap: 0.45rem; flex: 1 1 18rem;
690
+ background: var(--panel-sunken); border: 1px solid var(--border); border-radius: var(--r-ctl);
691
+ padding: 0.4rem 0.65rem; color: var(--fg-subtle);
692
+ transition: border-color 130ms ease, background-color 130ms ease; }
693
+ .filter-field:focus-within { border-color: var(--accent); background: var(--panel); }
694
+ .filter-field input { flex: 1 1 auto; min-width: 0; border: 0; background: transparent;
695
+ font: inherit; font-size: 0.875rem; color: var(--fg); outline: none; padding: 0; }
696
+ .filter-field input::placeholder { color: var(--fg-subtle); }
697
+ .filter-field input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
698
+ .filter-count { color: var(--fg-subtle); font-size: 0.78125rem; font-variant-numeric: tabular-nums;
699
+ margin: 0; flex: 0 0 auto; }
700
+ .filter-none { color: var(--fg-muted); font-size: 0.875rem; margin: 0.5rem 0; }
701
+
702
+ .empty { text-align: center; padding: 1.75rem 1rem 1.85rem; color: var(--fg-muted); }
703
+ .empty .icon { font-size: 1.5rem; color: var(--fg-subtle); stroke-width: 1.2; }
704
+ .empty-title { margin: 0.55rem 0 0.2rem; font-weight: 600; color: var(--fg); font-size: 0.9375rem; }
705
+ .empty-hint { margin: 0; font-size: 0.8125rem; color: var(--fg-subtle); max-width: 42ch;
706
+ margin-inline: auto; }
707
+
708
+ @media (hover: hover) and (pointer: fine) {
709
+ .section > summary:hover { background: var(--hover); }
710
+ .row-summary:hover { background: var(--hover); }
711
+ }
712
+ .section > summary:active, .row-summary:active { transform: scale(0.995); }
713
+ @media (max-width: 40rem) { .pb-count { font-size: 0.75rem; } }
714
+ @media (prefers-reduced-motion: reduce) {
715
+ .section > summary:active, .row-summary:active { transform: none; }
716
+ }
717
+ `;
601
718
  const STYLE = `
602
719
  :root {
603
720
  color-scheme: light dark;
@@ -665,7 +782,6 @@ const STYLE = `
665
782
  }
666
783
 
667
784
  *, *::before, *::after { box-sizing: border-box; }
668
- [hidden] { display: none !important; }
669
785
  html { -webkit-text-size-adjust: 100%; }
670
786
  body {
671
787
  font-family: var(--font-sans);
@@ -678,13 +794,6 @@ const STYLE = `
678
794
  accent-color: var(--accent);
679
795
  }
680
796
  ::selection { background: var(--live-bg); color: var(--fg); }
681
- svg.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
682
- .icon { width: 1em; height: 1em; flex: 0 0 auto; fill: none; stroke: currentColor;
683
- stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
684
- .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
685
- overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
686
- .mono { font-family: var(--font-mono); font-size: 0.9em; font-variant-numeric: tabular-nums; }
687
- .prose { overflow-wrap: anywhere; }
688
797
 
689
798
  .skip { position: absolute; left: -999px; top: 0; z-index: 10;
690
799
  background: var(--panel); color: var(--fg); padding: 0.6rem 0.9rem; border-radius: var(--r-ctl); }
@@ -763,102 +872,6 @@ const STYLE = `
763
872
  .pulse-bar { fill: var(--series-1); }
764
873
  .pulse-none { fill: var(--border-strong); }
765
874
 
766
- .section { margin: 0 0 0.6rem; background: var(--panel); border: 1px solid var(--card-border);
767
- border-radius: var(--r-card); box-shadow: var(--shadow);
768
- scroll-margin-top: calc(var(--header-offset) + 0.75rem); }
769
- summary { cursor: pointer; list-style: none; }
770
- summary::-webkit-details-marker { display: none; }
771
- .section > summary { display: flex; align-items: center; gap: 0.55rem; padding: 0.85rem 1rem;
772
- border-radius: var(--r-card); font-size: 0.9375rem;
773
- transition: background-color 130ms ease; }
774
- .sec-icon { display: inline-flex; color: var(--accent); font-size: 1.0625rem; }
775
- .section > summary .pb-label { font-weight: 600; letter-spacing: -0.006em; }
776
- .pb-count { color: var(--fg-subtle); font-size: 0.78125rem; font-variant-numeric: tabular-nums;
777
- margin-left: auto; text-align: right; }
778
- .chev { display: inline-flex; color: var(--fg-subtle); font-size: 1rem; margin-left: 0.15rem;
779
- transition: transform 180ms var(--ease-out); }
780
- details[open] > summary .chev { transform: rotate(180deg); }
781
- summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; border-radius: var(--r-ctl); }
782
- .section-body { padding: 0 1rem 1rem; }
783
- .section > .section-body { border-top: 1px solid var(--border); padding-top: 0.9rem; margin-top: -0.15rem; }
784
-
785
- .row-summary { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.6rem;
786
- border-radius: var(--r-ctl); font-size: 0.875rem;
787
- transition: background-color 130ms ease; }
788
- .row-summary .pb-label { display: flex; align-items: center; gap: 0.42rem; flex-wrap: wrap;
789
- min-width: 0; }
790
- .row-summary .icon { color: var(--fg-subtle); }
791
- details.pad-item, details.todo-item { margin: 0; }
792
- details.pad-item > .section-body, details.todo-item > .section-body { padding: 0.1rem 0.6rem 0.7rem 0.6rem; }
793
- .todo-body { overflow-wrap: anywhere; white-space: pre-wrap; color: var(--fg-muted);
794
- font-size: 0.875rem; }
795
- .todo-body + .todo-body { margin-top: 0.55rem; }
796
-
797
- .meta, .muted { color: var(--fg-muted); }
798
- .meta { font-size: 0.8125rem; margin: 0 0 0.75rem; max-width: 72ch; }
799
- .muted { font-size: 0.8125rem; }
800
- .cap-note { display: flex; align-items: center; gap: 0.4rem; color: var(--fg-subtle);
801
- font-size: 0.78125rem; margin: 0 0 0.6rem; }
802
- pre.board { white-space: pre-wrap; overflow-wrap: anywhere; overflow: auto; max-height: 60vh; background: var(--panel-sunken);
803
- border: 1px solid var(--border); border-radius: var(--r-ctl); padding: 0.85rem;
804
- font-family: var(--font-mono); font-size: 0.8125rem; line-height: 1.5; margin: 0;
805
- overscroll-behavior: contain; }
806
-
807
- ul.rows { list-style: none; margin: 0; padding: 0; }
808
- ul.rows > li { padding: 0.35rem 0; border-bottom: 1px solid var(--border);
809
- display: flex; flex-direction: column; gap: 0.15rem; }
810
- ul.rows > li:last-child { border-bottom: none; }
811
- ul.rows > li.todo, ul.rows > li:has(> details) { padding: 0.1rem 0; }
812
- .row-main { display: flex; align-items: center; gap: 0.42rem; flex-wrap: wrap; }
813
- .row-meta { font-size: 0.78125rem; color: var(--fg-subtle); }
814
- .kind { color: var(--fg-subtle); font-size: 0.78125rem; }
815
-
816
- .status { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.71875rem;
817
- font-weight: 600; letter-spacing: 0.01em; padding: 0.12rem 0.5rem; border-radius: var(--r-pill);
818
- white-space: nowrap; }
819
- .status::before { content: ""; width: 0.4rem; height: 0.4rem; border-radius: 50%;
820
- background: currentColor; flex: 0 0 auto; }
821
- .status-ok { color: var(--ok); background: var(--ok-bg); }
822
- .status-warn { color: var(--warn); background: var(--warn-bg); }
823
- .status-fail { color: var(--fail); background: var(--fail-bg); }
824
- .status-live { color: var(--live); background: var(--live-bg); }
825
-
826
- .chip { display: inline-flex; align-items: center; gap: 0.28rem; font-size: 0.71875rem;
827
- font-weight: 600; padding: 0.12rem 0.48rem; border-radius: var(--r-pill); white-space: nowrap; }
828
- .chip-blocked { color: var(--warn); background: var(--warn-bg); }
829
- .chip-blocked .icon { stroke-width: 1.6; }
830
-
831
- .tag { font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
832
- padding: 0.1rem 0.4rem; border-radius: 5px; background: var(--panel-sunken); color: var(--fg-subtle); }
833
-
834
- .priority { font-size: 0.6875rem; font-weight: 650; letter-spacing: 0.04em; text-transform: uppercase; }
835
- .priority-high { color: var(--fail); }
836
- .priority-medium { color: var(--fg-muted); }
837
- .priority-low { color: var(--fg-subtle); }
838
- .blockers { font-size: 0.8125rem; color: var(--fg-muted); margin: 0 0 0.45rem 0.6rem; }
839
- .wake-body { color: var(--fg); font-size: 0.875rem; overflow-wrap: anywhere; }
840
- .activity-body { font-size: 0.8125rem; color: var(--fg-muted); white-space: pre-wrap;
841
- overflow-wrap: anywhere; margin-top: 0.1rem; }
842
-
843
- .filter { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
844
- .filter-field { display: flex; align-items: center; gap: 0.45rem; flex: 1 1 18rem;
845
- background: var(--panel-sunken); border: 1px solid var(--border); border-radius: var(--r-ctl);
846
- padding: 0.4rem 0.65rem; color: var(--fg-subtle);
847
- transition: border-color 130ms ease, background-color 130ms ease; }
848
- .filter-field:focus-within { border-color: var(--accent); background: var(--panel); }
849
- .filter-field input { flex: 1 1 auto; min-width: 0; border: 0; background: transparent;
850
- font: inherit; font-size: 0.875rem; color: var(--fg); outline: none; padding: 0; }
851
- .filter-field input::placeholder { color: var(--fg-subtle); }
852
- .filter-field input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
853
- .filter-count { color: var(--fg-subtle); font-size: 0.78125rem; font-variant-numeric: tabular-nums;
854
- margin: 0; flex: 0 0 auto; }
855
- .filter-none { color: var(--fg-muted); font-size: 0.875rem; margin: 0.5rem 0; }
856
-
857
- .empty { text-align: center; padding: 1.75rem 1rem 1.85rem; color: var(--fg-muted); }
858
- .empty .icon { font-size: 1.5rem; color: var(--fg-subtle); stroke-width: 1.2; }
859
- .empty-title { margin: 0.55rem 0 0.2rem; font-weight: 600; color: var(--fg); font-size: 0.9375rem; }
860
- .empty-hint { margin: 0; font-size: 0.8125rem; color: var(--fg-subtle); max-width: 42ch;
861
- margin-inline: auto; }
862
875
 
863
876
  svg.chart { width: 100%; height: auto; display: block; margin: 0 0 0.9rem;
864
877
  color: var(--fg-muted); }
@@ -896,29 +909,24 @@ const STYLE = `
896
909
  .swatch-2 { background: var(--series-2); }
897
910
 
898
911
  @media (hover: hover) and (pointer: fine) {
899
- .section > summary:hover { background: var(--hover); }
900
- .row-summary:hover { background: var(--hover); }
901
912
  .navchip:hover { background: var(--hover); color: var(--fg); }
902
913
  }
903
- .section > summary:active, .row-summary:active { transform: scale(0.995); }
904
914
  .navchip:active { transform: scale(0.97); }
905
915
 
906
916
  @media (max-width: 40rem) {
907
917
  .wrap { padding: 0 0.9rem; }
908
918
  .now { grid-template-columns: 1fr; }
909
- .pb-count { font-size: 0.75rem; }
910
919
  .navchip-label { position: absolute; width: 1px; height: 1px; padding: 0;
911
920
  margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
912
921
  }
913
922
 
914
923
  @media (prefers-reduced-motion: reduce) {
915
924
  * { transition: none !important; animation: none !important; }
916
- .section > summary:active, .row-summary:active, .navchip:active { transform: none; }
925
+ .navchip:active { transform: none; }
917
926
  }
918
- `;
919
- const SCRIPT = `
920
- (function () {
921
- var KEY = "hive-dashboard-state";
927
+ ${SECTION_STYLE}`;
928
+ export const sectionScript = (key, between = "") => `
929
+ var KEY = "${key}";
922
930
  function load() {
923
931
  try { return JSON.parse(sessionStorage.getItem(KEY) || "{}"); } catch (e) { return {}; }
924
932
  }
@@ -999,8 +1007,6 @@ const SCRIPT = `
999
1007
  s.filterCaret = filterInput.selectionStart;
1000
1008
  save(s);
1001
1009
  applyFilter(filterInput.value);
1002
- paintStamp();
1003
- deferReload();
1004
1010
  });
1005
1011
  }
1006
1012
 
@@ -1016,6 +1022,46 @@ const SCRIPT = `
1016
1022
  }
1017
1023
  measureHeader();
1018
1024
 
1025
+ ${between} function absTop(el) { return el.getBoundingClientRect().top + window.scrollY; }
1026
+ var anchorEl = state.anchor && typeof state.anchor.id === "string"
1027
+ ? document.getElementById(state.anchor.id) : null;
1028
+ if (anchorEl && typeof state.anchor.offset === "number") {
1029
+ window.scrollTo(0, absTop(anchorEl) + state.anchor.offset);
1030
+ } else if (typeof state.scrollY === "number") {
1031
+ window.scrollTo(0, state.scrollY);
1032
+ }
1033
+ var itemSel = "details.pad-item, details.todo-item";
1034
+ var padItems = document.querySelectorAll("details.pad-item");
1035
+ for (var pi = 0; pi < padItems.length; pi++) {
1036
+ var saved = state.padScroll && state.padScroll[padItems[pi].id];
1037
+ var padPre = padItems[pi].querySelector("pre.board");
1038
+ if (padItems[pi].open && padPre && typeof saved === "number") padPre.scrollTop = saved;
1039
+ }
1040
+
1041
+ window.addEventListener("beforeunload", function () {
1042
+ var s = load();
1043
+ s.scrollY = window.scrollY;
1044
+ s.anchor = null;
1045
+ var secs = document.querySelectorAll("details.section");
1046
+ for (var k = 0; k < secs.length; k++) {
1047
+ if (secs[k].getBoundingClientRect().top <= headerOffset) {
1048
+ s.anchor = { id: secs[k].id, offset: window.scrollY - absTop(secs[k]) };
1049
+ }
1050
+ }
1051
+ s.padScroll = {};
1052
+ var openPads = document.querySelectorAll("details.pad-item");
1053
+ for (var m = 0; m < openPads.length; m++) {
1054
+ var pre = openPads[m].querySelector("pre.board");
1055
+ if (openPads[m].open && pre && pre.scrollTop > 0) s.padScroll[openPads[m].id] = pre.scrollTop;
1056
+ }
1057
+ if (filterInput) {
1058
+ s.filterFocused = document.activeElement === filterInput;
1059
+ s.filterCaret = filterInput.selectionStart;
1060
+ }
1061
+ save(s);
1062
+ });
1063
+ `;
1064
+ const NAV_SCRIPT = `
1019
1065
  // Section nav: mark the section the reader is currently in.
1020
1066
  var navLinks = document.querySelectorAll(".navchip");
1021
1067
  function paintNav() {
@@ -1040,22 +1086,10 @@ const SCRIPT = `
1040
1086
  window.addEventListener("resize", function () { measureHeader(); paintNav(); });
1041
1087
  }
1042
1088
 
1043
- function absTop(el) { return el.getBoundingClientRect().top + window.scrollY; }
1044
- var anchorEl = state.anchor && typeof state.anchor.id === "string"
1045
- ? document.getElementById(state.anchor.id) : null;
1046
- if (anchorEl && typeof state.anchor.offset === "number") {
1047
- window.scrollTo(0, absTop(anchorEl) + state.anchor.offset);
1048
- } else if (typeof state.scrollY === "number") {
1049
- window.scrollTo(0, state.scrollY);
1050
- }
1051
- var itemSel = "details.pad-item, details.todo-item";
1052
- var padItems = document.querySelectorAll("details.pad-item");
1053
- for (var pi = 0; pi < padItems.length; pi++) {
1054
- var saved = state.padScroll && state.padScroll[padItems[pi].id];
1055
- var padPre = padItems[pi].querySelector("pre.board");
1056
- if (padItems[pi].open && padPre && typeof saved === "number") padPre.scrollTop = saved;
1057
- }
1058
-
1089
+ `;
1090
+ const SCRIPT = `
1091
+ (function () {
1092
+ ${sectionScript("hive-dashboard-state", NAV_SCRIPT)}
1059
1093
  // Live toggle. Replaces <meta http-equiv="refresh">, which the browser
1060
1094
  // schedules at PARSE TIME - removing the tag afterward does not cancel it,
1061
1095
  // so a toggle could never turn it off. A setTimeout can be cleared, so this
@@ -1102,6 +1136,9 @@ const SCRIPT = `
1102
1136
  if (toggle) toggle.checked = live;
1103
1137
  paintStamp();
1104
1138
  deferReload();
1139
+ if (filterInput) {
1140
+ filterInput.addEventListener("input", function () { paintStamp(); deferReload(); });
1141
+ }
1105
1142
  var items = document.querySelectorAll(itemSel);
1106
1143
  for (var ii = 0; ii < items.length; ii++) {
1107
1144
  items[ii].addEventListener("toggle", function () { paintStamp(); deferReload(); });
@@ -1124,28 +1161,6 @@ const SCRIPT = `
1124
1161
  if (reloadTimer !== null) deferReload();
1125
1162
  }, { passive: true });
1126
1163
 
1127
- window.addEventListener("beforeunload", function () {
1128
- var s = load();
1129
- s.scrollY = window.scrollY;
1130
- s.anchor = null;
1131
- var secs = document.querySelectorAll("details.section");
1132
- for (var k = 0; k < secs.length; k++) {
1133
- if (secs[k].getBoundingClientRect().top <= headerOffset) {
1134
- s.anchor = { id: secs[k].id, offset: window.scrollY - absTop(secs[k]) };
1135
- }
1136
- }
1137
- s.padScroll = {};
1138
- var openPads = document.querySelectorAll("details.pad-item");
1139
- for (var m = 0; m < openPads.length; m++) {
1140
- var pre = openPads[m].querySelector("pre.board");
1141
- if (openPads[m].open && pre && pre.scrollTop > 0) s.padScroll[openPads[m].id] = pre.scrollTop;
1142
- }
1143
- if (filterInput) {
1144
- s.filterFocused = document.activeElement === filterInput;
1145
- s.filterCaret = filterInput.selectionStart;
1146
- }
1147
- save(s);
1148
- });
1149
1164
  })();
1150
1165
  `;
1151
1166
  const BRAND_MARK = `<svg class="mark" viewBox="0 0 32 32" role="img" aria-label="hive">
@@ -1207,3 +1222,11 @@ export function renderDashboardForWrite(projectId, procs = []) {
1207
1222
  const contentHash = createHash("sha256").update(projectName).update("\0").update(sections).digest("hex");
1208
1223
  return { html, contentHash };
1209
1224
  }
1225
+ export function renderBoardPadsTodos(projectId) {
1226
+ const index = [];
1227
+ return [
1228
+ renderBoardSection(index, projectId),
1229
+ renderTodosSection(index, projectId),
1230
+ renderPadsSection(index, projectId),
1231
+ ].join("\n");
1232
+ }
package/dist/db.js CHANGED
@@ -429,6 +429,7 @@ ALTER TABLE agent_messages ADD COLUMN target_identity TEXT;
429
429
  ALTER TABLE agent_messages ADD COLUMN sender_tag TEXT;
430
430
  CREATE INDEX idx_agent_messages_delivery ON agent_messages(delivery_status, id) WHERE delivery_status IS NOT NULL;
431
431
  `,
432
+ `ALTER TABLE agents ADD COLUMN todo_id INTEGER REFERENCES todos(id) ON DELETE SET NULL;`,
432
433
  ];
433
434
  export function storeSchemaAhead(database) {
434
435
  try {
@@ -0,0 +1,17 @@
1
+ import { HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, isLeadRowClosedHold, isUnclassifiablePaneHold, isUnsubmittedInputHold, wasHeldForPaneIdentity, } from "./scheduler.js";
2
+ // test/docs.test.mjs fails if a label here is missing from docs/install.md. It checks
3
+ // presence only: docs/install.md also counts the set in prose, and nothing guards that number.
4
+ export const HELD_REASON_LABELS = ["typing", "talking", "needs you", "blocked"];
5
+ export function heldReasonLabel(heldReason) {
6
+ if (isUnsubmittedInputHold(heldReason))
7
+ return "typing";
8
+ if (heldReason === HELD_REASON_LEAD_PANE_DEAD ||
9
+ wasHeldForPaneIdentity(heldReason) ||
10
+ isLeadRowClosedHold(heldReason) ||
11
+ isUnclassifiablePaneHold(heldReason)) {
12
+ return "needs you";
13
+ }
14
+ if (heldReason === HELD_REASON_CONVERSATION)
15
+ return "talking";
16
+ return "blocked";
17
+ }
package/dist/leadState.js CHANGED
@@ -3,6 +3,12 @@ import { FIRST_MESSAGE_SHA_ENV, firstMessageDigest } from "./firstMessage.js";
3
3
  export function readLeadTurnState(agentId) {
4
4
  return db.prepare("SELECT * FROM lead_turn_state WHERE agent_id = ?").get(agentId) ?? null;
5
5
  }
6
+ export function leadSessionEnded(row, ownership) {
7
+ if ((ownership !== "gone" && ownership !== "reissued") || row.pane_pid === "")
8
+ return null;
9
+ const turn = readLeadTurnState(row.id);
10
+ return turn?.last_event === "session_end" && turn.pane_pid === row.pane_pid ? { ended_at: turn.changed_at } : null;
11
+ }
6
12
  function isHiveFirstMessage(payload) {
7
13
  if (typeof payload.prompt !== "string")
8
14
  return false;
@@ -44,7 +50,8 @@ const applyInTransaction = db.transaction((agentId, panePid, event, payload, sub
44
50
  cur.pane_pid === next.pane_pid &&
45
51
  cur.session_id === next.session_id &&
46
52
  cur.state === next.state &&
47
- cur.idle_seq === next.idle_seq) {
53
+ cur.idle_seq === next.idle_seq &&
54
+ (event !== "session_end" || cur.last_event === "session_end")) {
48
55
  return;
49
56
  }
50
57
  db.prepare(`INSERT INTO lead_turn_state (agent_id, pane_pid, session_id, state, idle_seq, last_event, changed_at)
package/dist/portfolio.js CHANGED
@@ -3,7 +3,7 @@ import { listProjects } from "./context.js";
3
3
  import { existsSync } from "node:fs";
4
4
  import { parseTags } from "./result.js";
5
5
  import { OPEN_BLOCKERS_SQL } from "./tools/todos.js";
6
- import { readLeadTurnState } from "./leadState.js";
6
+ import { leadSessionEnded, readLeadTurnState } from "./leadState.js";
7
7
  import { liveTargets, ownershipLiveness, rowOwnership } from "./tmux.js";
8
8
  export const PORTFOLIO_REASONS = [
9
9
  "needs_human",
@@ -88,14 +88,17 @@ function projectRow(project, now, snapshot) {
88
88
  const workers = { working: 0, idle: 0, needs_input: 0, other: 0, unreachable: 0, unconfirmed: 0 };
89
89
  let liveWorking = 0;
90
90
  let liveWaiting = 0;
91
- let lead = { state: "none", agent_id: null, turn: "unknown" };
91
+ let lead = { state: "none", agent_id: null, turn: "unknown", ended_at: null };
92
92
  for (const a of agents) {
93
93
  if (a.kind === "lead") {
94
- const live = liveness(a);
94
+ const ownership = rowOwnership(a, snapshot);
95
+ const live = ownershipLiveness(ownership);
96
+ const ended = leadSessionEnded(a, ownership);
95
97
  lead = {
96
- state: live === true ? "alive" : live === false ? "dead_pane" : "unknown",
98
+ state: live === true ? "alive" : live === false ? (ended ? "dormant" : "dead_pane") : "unknown",
97
99
  agent_id: a.id,
98
100
  turn: live === true ? leadTurnFor(a.id, a.pane_pid) : "unknown",
101
+ ended_at: ended?.ended_at ?? null,
99
102
  };
100
103
  continue;
101
104
  }
@@ -135,7 +138,7 @@ function projectRow(project, now, snapshot) {
135
138
  const found = new Set();
136
139
  if (tagged.length > 0)
137
140
  found.add("needs_human");
138
- if (lead.state === "dead_pane" && (todos.in_progress > 0 || wakeCounts.pending > 0))
141
+ if ((lead.state === "dead_pane" || lead.state === "dormant") && (todos.in_progress > 0 || wakeCounts.pending > 0))
139
142
  found.add("dead_lead_pane");
140
143
  if (!rootExists && (activeTodos > 0 || wakeCounts.pending > 0))
141
144
  found.add("missing_root_with_work");
@@ -18,6 +18,7 @@ export const BUILT_IN_PROJECT_YML = Object.freeze({
18
18
  context_checkpoint_percent: null,
19
19
  lead_turn_budget: null,
20
20
  dashboard: false,
21
+ lead_sidebar: false,
21
22
  quiet_messaging: false,
22
23
  vars: Object.freeze({}),
23
24
  processes: Object.freeze({}),
@@ -199,6 +200,19 @@ function applyLayer(root, path, source, config, sources, warnings) {
199
200
  else
200
201
  warn(`dashboard must be true or false; ignoring "${String(value)}".`);
201
202
  }
203
+ if (has("lead_sidebar")) {
204
+ const value = root.lead_sidebar;
205
+ if (value == null) {
206
+ config.lead_sidebar = false;
207
+ sources.lead_sidebar = source;
208
+ }
209
+ else if (typeof value === "boolean") {
210
+ config.lead_sidebar = value;
211
+ sources.lead_sidebar = source;
212
+ }
213
+ else
214
+ warn(`lead_sidebar must be true or false; ignoring "${String(value)}".`);
215
+ }
202
216
  if (has("quiet_messaging")) {
203
217
  const value = root.quiet_messaging;
204
218
  if (value == null) {