@dadado/agent-kit-cli 4.8.9 → 5.1.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/LICENSE +75 -21
- package/README.md +19 -0
- package/dashboard/dashboard-data.mjs +103 -0
- package/dashboard/dashboard.html +539 -172
- package/dashboard/lib/broadcast-share.mjs +251 -0
- package/dashboard/lib/guards.d.mts +10 -0
- package/dashboard/lib/guards.mjs +71 -4
- package/dashboard/lib/open-browser.d.mts +31 -0
- package/dashboard/lib/open-browser.mjs +298 -0
- package/dashboard/lib/semantic-model.mjs +215 -9
- package/dashboard/open.html +213 -0
- package/dashboard/serve.mjs +6 -2
- package/dashboard/start-broadcast.mjs +75 -31
- package/dashboard/start.mjs +23 -26
- package/dist/index.js +1837 -447
- package/package.json +4 -2
package/dashboard/dashboard.html
CHANGED
|
@@ -19,6 +19,21 @@
|
|
|
19
19
|
var fav = document.getElementById('favicon');
|
|
20
20
|
if (fav) fav.setAttribute('href', skin === 'cursor' ? '/logo-cursor.svg' : '/logo.svg');
|
|
21
21
|
})();
|
|
22
|
+
/* Restore Crew Monitor row density before first paint; auto-pick by viewport
|
|
23
|
+
when the operator has no stored preference. Same namespaced-key + blocked-
|
|
24
|
+
storage tolerance as the skin restore above. */
|
|
25
|
+
(function () {
|
|
26
|
+
var key = 'agent-kit:monitor-density';
|
|
27
|
+
var density = null;
|
|
28
|
+
try {
|
|
29
|
+
var stored = localStorage.getItem(key);
|
|
30
|
+
if (stored === 'compact' || stored === 'comfortable') density = stored;
|
|
31
|
+
} catch (e) { /* private mode / blocked storage */ }
|
|
32
|
+
if (!density) {
|
|
33
|
+
density = (window.innerWidth || 0) >= 900 ? 'comfortable' : 'compact';
|
|
34
|
+
}
|
|
35
|
+
document.documentElement.setAttribute('data-monitor-density', density);
|
|
36
|
+
})();
|
|
22
37
|
</script>
|
|
23
38
|
<style>
|
|
24
39
|
/* ===== Reset & Base ===== */
|
|
@@ -34,7 +49,7 @@ html[data-dashboard-skin="legacy"] {
|
|
|
34
49
|
--border-active: #2a3a5a;
|
|
35
50
|
--text-primary: #e2e8f0;
|
|
36
51
|
--text-secondary: #8899aa;
|
|
37
|
-
--text-muted: #6d8094;
|
|
52
|
+
--text-muted: #6d8094; /* landing uses #7f93a8 (AA over marketing gradient); see docs/design-system.md */
|
|
38
53
|
--green: #22c55e;
|
|
39
54
|
--green-bg: rgba(34,197,94,0.12);
|
|
40
55
|
--yellow: #eab308;
|
|
@@ -1026,7 +1041,6 @@ body.mc-fullscreen .top-tabs-row {
|
|
|
1026
1041
|
.health-item-sev[data-sev="ok"] { color: var(--green); }
|
|
1027
1042
|
.health-item-sev[data-sev="warning"] { color: var(--yellow); }
|
|
1028
1043
|
.health-item-sev[data-sev="degraded"] { color: var(--orange); }
|
|
1029
|
-
.health-item-sev[data-sev="error"] { color: var(--red); }
|
|
1030
1044
|
.health-item-chevron {
|
|
1031
1045
|
color: var(--text-muted);
|
|
1032
1046
|
font-size: 10px;
|
|
@@ -1700,11 +1714,20 @@ body.mc-fullscreen .top-tabs-row {
|
|
|
1700
1714
|
white-space: nowrap;
|
|
1701
1715
|
}
|
|
1702
1716
|
|
|
1703
|
-
/* ===== Monitor
|
|
1717
|
+
/* ===== Crew Monitor row (design v2, 2026-08-05) =====
|
|
1718
|
+
Ported from the Claude Design project `Mission Control — Crew Monitor`
|
|
1719
|
+
(verdict: .cursor/context/mission-control-design/remote/v1/ACCEPTANCE.md).
|
|
1720
|
+
|
|
1721
|
+
The layout contract that replaced the equal-shrink segment row: exactly ONE
|
|
1722
|
+
flexible field per row. `.monitor-row-primary` is the only field allowed to
|
|
1723
|
+
ellipsis; every other column is fixed-width or fixed-content, and the ref
|
|
1724
|
+
chips hide WHOLE at a breakpoint rather than truncating. That is what stops
|
|
1725
|
+
`revi…` / `8f…` / `P…` from coming back — a field that cannot show one whole
|
|
1726
|
+
token is removed instead of shrunk. */
|
|
1704
1727
|
.live-activity-feed .monitor-row {
|
|
1705
1728
|
display: flex;
|
|
1706
1729
|
align-items: center;
|
|
1707
|
-
gap:
|
|
1730
|
+
gap: 8px;
|
|
1708
1731
|
padding: 6px 12px;
|
|
1709
1732
|
font-size: 12px;
|
|
1710
1733
|
color: var(--text-secondary);
|
|
@@ -1716,90 +1739,195 @@ body.mc-fullscreen .top-tabs-row {
|
|
|
1716
1739
|
.live-activity-feed .monitor-row:last-child {
|
|
1717
1740
|
padding-bottom: 10px;
|
|
1718
1741
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1742
|
+
/* Team badge at row start (design v2 identity pass). Replaces the kind glyph:
|
|
1743
|
+
the glyph's two jobs split, so colour lands here (kind tint + solid semantic
|
|
1744
|
+
initials) and meaning lands on the badge's tooltip gloss. Unlike the avatar
|
|
1745
|
+
box removed in #631/#637-#639, this is not a decorative identity chip — it is
|
|
1746
|
+
the kind cue and the actor cue in one 18px square, which is why `info.bg`
|
|
1747
|
+
renders on a Crew row again. Stays at --mc-radius-sm while the status pills
|
|
1748
|
+
sit at --mc-radius-chrome, so a badge never reads as a status pill. */
|
|
1749
|
+
.live-activity-feed .monitor-row .monitor-row-badge {
|
|
1750
|
+
flex: 0 0 18px;
|
|
1723
1751
|
width: 18px;
|
|
1724
|
-
min-width: 18px;
|
|
1725
1752
|
height: 18px;
|
|
1726
|
-
|
|
1727
|
-
border-radius: 4px;
|
|
1728
|
-
font-size: 10px;
|
|
1729
|
-
font-weight: 700;
|
|
1730
|
-
flex-shrink: 0;
|
|
1731
|
-
line-height: 1;
|
|
1732
|
-
box-sizing: border-box;
|
|
1733
|
-
/* Solid *-bg fill only; no stroke / inset ring / left rail */
|
|
1734
|
-
border: none;
|
|
1735
|
-
box-shadow: none;
|
|
1736
|
-
}
|
|
1737
|
-
.live-activity-feed .monitor-row .monitor-row-icon {
|
|
1738
|
-
font-size: 11px;
|
|
1739
|
-
line-height: 1;
|
|
1740
|
-
width: 1em;
|
|
1741
|
-
text-align: center;
|
|
1742
|
-
}
|
|
1743
|
-
.live-activity-feed .monitor-row .monitor-row-avatar {
|
|
1753
|
+
border-radius: var(--mc-radius-sm);
|
|
1744
1754
|
display: inline-flex;
|
|
1745
1755
|
align-items: center;
|
|
1746
1756
|
justify-content: center;
|
|
1747
|
-
|
|
1748
|
-
min-width: 18px;
|
|
1749
|
-
height: 18px;
|
|
1750
|
-
border-radius: 4px;
|
|
1751
|
-
font-size: 9px;
|
|
1757
|
+
font-size: 7.5px;
|
|
1752
1758
|
font-weight: 700;
|
|
1759
|
+
letter-spacing: 0.03em;
|
|
1753
1760
|
line-height: 1;
|
|
1754
|
-
|
|
1755
|
-
background: var(--bg-card-hover);
|
|
1756
|
-
color: var(--text-secondary);
|
|
1761
|
+
user-select: none;
|
|
1757
1762
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1763
|
+
/* Actor column: fixed basis, not a min-width floor. The floor let long kit
|
|
1764
|
+
agent ids push the verb and metadata columns off their x; a hard basis keeps
|
|
1765
|
+
the scan column stable across every kind. Sized so the longest mask in the
|
|
1766
|
+
operator lexicon ("Project Manager", "Product Analyst") fits unclipped. */
|
|
1767
|
+
.live-activity-feed .monitor-row .monitor-row-actor {
|
|
1768
|
+
flex: 0 0 9.5em;
|
|
1760
1769
|
min-width: 0;
|
|
1761
1770
|
overflow: hidden;
|
|
1762
1771
|
text-overflow: ellipsis;
|
|
1763
1772
|
white-space: nowrap;
|
|
1764
|
-
|
|
1765
|
-
font-size:
|
|
1766
|
-
|
|
1767
|
-
display: flex;
|
|
1768
|
-
align-items: baseline;
|
|
1769
|
-
gap: 6px;
|
|
1773
|
+
font-family: var(--mc-font-mono);
|
|
1774
|
+
font-size: 10.5px;
|
|
1775
|
+
color: var(--text-muted);
|
|
1770
1776
|
}
|
|
1771
|
-
/*
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1777
|
+
/* Verb column: state icon + word, both inheriting the verb-state colour. */
|
|
1778
|
+
.live-activity-feed .monitor-row .monitor-row-verb {
|
|
1779
|
+
flex: 0 0 6.5em;
|
|
1780
|
+
display: inline-flex;
|
|
1781
|
+
align-items: center;
|
|
1782
|
+
gap: 4px;
|
|
1775
1783
|
white-space: nowrap;
|
|
1776
|
-
|
|
1777
|
-
.live-activity-feed .monitor-row .feed-sep {
|
|
1778
|
-
flex-shrink: 0;
|
|
1784
|
+
font-size: 11px;
|
|
1779
1785
|
color: var(--text-muted);
|
|
1780
1786
|
}
|
|
1781
|
-
.live-activity-feed .monitor-row .
|
|
1782
|
-
|
|
1787
|
+
.live-activity-feed .monitor-row .monitor-row-verb em {
|
|
1788
|
+
font-style: normal;
|
|
1789
|
+
}
|
|
1790
|
+
/* 12px against the chrome set's 16px, so the icon reads as a state mark inside
|
|
1791
|
+
a text column rather than as chrome. The heavier stroke is optical
|
|
1792
|
+
compensation: 1.5 at 12px renders lighter than 1.5 at 16px. */
|
|
1793
|
+
.live-activity-feed .monitor-row .monitor-row-verb-icon {
|
|
1794
|
+
width: 12px;
|
|
1795
|
+
height: 12px;
|
|
1796
|
+
flex-shrink: 0;
|
|
1797
|
+
stroke-width: 1.6;
|
|
1798
|
+
}
|
|
1799
|
+
.live-activity-feed .monitor-row .v-running { color: var(--green); }
|
|
1800
|
+
.live-activity-feed .monitor-row .v-awaiting { color: var(--yellow); }
|
|
1801
|
+
.live-activity-feed .monitor-row .v-failed { color: var(--red); }
|
|
1802
|
+
.live-activity-feed .monitor-row .v-failed em { font-weight: 600; }
|
|
1803
|
+
.live-activity-feed .monitor-row .v-merged { color: var(--cyan); }
|
|
1804
|
+
/* The row's only flexible field, and the only one at --text-primary: the to-do
|
|
1805
|
+
id / commit subject / gate is what distinguishes one row from the next. */
|
|
1806
|
+
.live-activity-feed .monitor-row .monitor-row-primary {
|
|
1807
|
+
flex: 1 1 auto;
|
|
1783
1808
|
min-width: 0;
|
|
1784
1809
|
overflow: hidden;
|
|
1785
1810
|
text-overflow: ellipsis;
|
|
1811
|
+
white-space: nowrap;
|
|
1812
|
+
color: var(--text-primary);
|
|
1786
1813
|
}
|
|
1787
|
-
.live-activity-feed .monitor-row .
|
|
1788
|
-
flex
|
|
1814
|
+
.live-activity-feed .monitor-row .monitor-row-meta {
|
|
1815
|
+
flex: 0 0 auto;
|
|
1816
|
+
display: inline-flex;
|
|
1817
|
+
align-items: center;
|
|
1818
|
+
gap: 6px;
|
|
1789
1819
|
min-width: 0;
|
|
1790
|
-
|
|
1791
|
-
|
|
1820
|
+
}
|
|
1821
|
+
/* Refs left the flexible text and became fixed-content chips (PR #NNN -> #NNN,
|
|
1822
|
+
7-8 char SHA, subagent worker id). A chip is either whole or hidden. */
|
|
1823
|
+
.live-activity-feed .monitor-row .monitor-row-ref {
|
|
1824
|
+
font-family: var(--mc-font-mono);
|
|
1825
|
+
font-size: 10px;
|
|
1792
1826
|
color: var(--text-muted);
|
|
1827
|
+
background: var(--bg-secondary);
|
|
1828
|
+
border: 1px solid var(--border);
|
|
1829
|
+
border-radius: 3px;
|
|
1830
|
+
padding: 1px 5px;
|
|
1831
|
+
line-height: 1.3;
|
|
1832
|
+
white-space: nowrap;
|
|
1793
1833
|
}
|
|
1794
|
-
|
|
1834
|
+
/* Plan ref: flag + basename. Driven by refs.plan, so it renders even when the
|
|
1835
|
+
label omitted the segment (truncation-safe: never sniffed from the string). */
|
|
1836
|
+
.live-activity-feed .monitor-row .monitor-row-plan {
|
|
1837
|
+
display: inline-flex;
|
|
1838
|
+
align-items: center;
|
|
1839
|
+
gap: 4px;
|
|
1795
1840
|
color: var(--text-muted);
|
|
1841
|
+
min-width: 0;
|
|
1842
|
+
}
|
|
1843
|
+
.live-activity-feed .monitor-row .monitor-row-plan-flag {
|
|
1844
|
+
width: 11px;
|
|
1845
|
+
height: 11px;
|
|
1846
|
+
flex-shrink: 0;
|
|
1847
|
+
stroke-width: 1.6;
|
|
1848
|
+
}
|
|
1849
|
+
/* Fixed content, like every other chip: no max-width, no ellipsis. The design
|
|
1850
|
+
capped this at 16em (24em comfortable) and kept ellipsis on it, which clipped
|
|
1851
|
+
real basenames — `crew-monitor-compact-labels-realtime-activity` needs 271px
|
|
1852
|
+
against a 240px cap, so the plan chip was rendering the same fragment the
|
|
1853
|
+
redesign exists to remove. It drops WHOLE at ≤720px instead; above that it
|
|
1854
|
+
takes the width it needs and the primary field absorbs the difference, which
|
|
1855
|
+
is what the row's one flexible field is for. */
|
|
1856
|
+
.live-activity-feed .monitor-row .monitor-row-plan-name {
|
|
1796
1857
|
font-family: var(--mc-font-mono);
|
|
1797
1858
|
font-size: 10px;
|
|
1798
1859
|
white-space: nowrap;
|
|
1799
|
-
|
|
1800
|
-
|
|
1860
|
+
}
|
|
1861
|
+
.live-activity-feed .monitor-row .monitor-row-time {
|
|
1862
|
+
flex: 0 0 44px;
|
|
1863
|
+
text-align: right;
|
|
1864
|
+
font-family: var(--mc-font-mono);
|
|
1865
|
+
font-size: 10px;
|
|
1866
|
+
color: var(--text-muted);
|
|
1867
|
+
white-space: nowrap;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
/* ===== Crew Monitor compaction ladder =====
|
|
1871
|
+
Container queries on the feed, not viewport media queries: the Crew Monitor
|
|
1872
|
+
is a panel inside a 2x2 cockpit grid and a narrow plugin shell, so the width
|
|
1873
|
+
that matters is the feed's own, never the window's.
|
|
1874
|
+
Every step hides a field WHOLE. Nothing here narrows a column and leaves it
|
|
1875
|
+
ellipsising — the actor drops out entirely rather than becoming `Project
|
|
1876
|
+
Man…` (delta A in ACCEPTANCE.md; the design's ladder narrowed it instead).
|
|
1877
|
+
Identity survives the drop: the badge keeps the initials, and the actor's own
|
|
1878
|
+
title plus the row tooltip still name the role in full. */
|
|
1879
|
+
.monitor-cq {
|
|
1880
|
+
container-type: inline-size;
|
|
1881
|
+
}
|
|
1882
|
+
@container (max-width: 720px) {
|
|
1883
|
+
.live-activity-feed .monitor-row .monitor-row-plan-name { display: none; }
|
|
1884
|
+
}
|
|
1885
|
+
@container (max-width: 560px) {
|
|
1886
|
+
.live-activity-feed .monitor-row .monitor-row-ref.ref-sha { display: none; }
|
|
1887
|
+
.live-activity-feed .monitor-row .monitor-row-actor { display: none; }
|
|
1888
|
+
}
|
|
1889
|
+
@container (max-width: 450px) {
|
|
1890
|
+
.live-activity-feed .monitor-row .monitor-row-verb { flex-basis: 12px; }
|
|
1891
|
+
.live-activity-feed .monitor-row .monitor-row-verb em { display: none; }
|
|
1892
|
+
.live-activity-feed .monitor-row .monitor-row-plan { display: none; }
|
|
1893
|
+
}
|
|
1894
|
+
@container (max-width: 360px) {
|
|
1895
|
+
.live-activity-feed .monitor-row .monitor-row-meta { display: none; }
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/* ===== Crew Monitor row density (compact | comfortable) =====
|
|
1899
|
+
Root data attribute set before first paint; auto-picked from viewport width
|
|
1900
|
+
when the operator has expressed no preference. Compact is the single-line row
|
|
1901
|
+
and stays the default on narrow shells.
|
|
1902
|
+
Comfortable is a second layout, not a taller first one: the meta chips stay
|
|
1903
|
+
on line 1 (pushed right), and the primary field reflows to a full-width
|
|
1904
|
+
second line where it gets two clamped lines at 12.5px instead of an ellipsis.
|
|
1905
|
+
The 26px indent aligns it under the badge. The title tooltip stays required
|
|
1906
|
+
in both modes — display `label` is capped at MAX_SEMANTIC_LABEL upstream, so
|
|
1907
|
+
a second line is never a substitute for it. */
|
|
1908
|
+
html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row {
|
|
1909
|
+
flex-wrap: wrap;
|
|
1910
|
+
row-gap: 3px;
|
|
1911
|
+
padding: 9px 12px;
|
|
1912
|
+
}
|
|
1913
|
+
html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monitor-row-meta {
|
|
1801
1914
|
margin-left: auto;
|
|
1802
1915
|
}
|
|
1916
|
+
html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monitor-row-primary {
|
|
1917
|
+
order: 10;
|
|
1918
|
+
flex: 1 0 100%;
|
|
1919
|
+
white-space: normal;
|
|
1920
|
+
overflow: hidden;
|
|
1921
|
+
display: -webkit-box;
|
|
1922
|
+
-webkit-line-clamp: 2;
|
|
1923
|
+
-webkit-box-orient: vertical;
|
|
1924
|
+
font-size: 12.5px;
|
|
1925
|
+
line-height: 1.45;
|
|
1926
|
+
padding-left: 26px;
|
|
1927
|
+
}
|
|
1928
|
+
.monitor-density-btn {
|
|
1929
|
+
margin-left: var(--mc-space-sm);
|
|
1930
|
+
}
|
|
1803
1931
|
.activity-feed-empty {
|
|
1804
1932
|
padding: var(--mc-space-2xl);
|
|
1805
1933
|
text-align: center;
|
|
@@ -1925,17 +2053,6 @@ body.mc-fullscreen .top-tabs-row {
|
|
|
1925
2053
|
vertical-align: middle;
|
|
1926
2054
|
}
|
|
1927
2055
|
|
|
1928
|
-
/* ===== Health Investigation Message (legacy; Healthcenter uses .health-detail) ===== */
|
|
1929
|
-
.health-message {
|
|
1930
|
-
font-size: 11px;
|
|
1931
|
-
color: var(--text-secondary);
|
|
1932
|
-
padding: 6px 10px;
|
|
1933
|
-
margin-top: 4px;
|
|
1934
|
-
border-radius: 4px;
|
|
1935
|
-
background: var(--bg-secondary);
|
|
1936
|
-
border: 1px solid var(--border);
|
|
1937
|
-
animation: fadeIn 0.2s ease;
|
|
1938
|
-
}
|
|
1939
2056
|
@keyframes fadeIn {
|
|
1940
2057
|
from { opacity: 0; transform: translateY(-4px); }
|
|
1941
2058
|
to { opacity: 1; transform: translateY(0); }
|
|
@@ -2518,14 +2635,8 @@ body.mc-fullscreen .top-tabs-row {
|
|
|
2518
2635
|
.skill-grid,
|
|
2519
2636
|
.agent-grid { grid-template-columns: 1fr; }
|
|
2520
2637
|
.live-activity-feed { max-height: 180px; }
|
|
2521
|
-
/*
|
|
2522
|
-
|
|
2523
|
-
.live-activity-feed .monitor-row .feed-seg-verb {
|
|
2524
|
-
flex-shrink: 1;
|
|
2525
|
-
min-width: 0;
|
|
2526
|
-
overflow: hidden;
|
|
2527
|
-
text-overflow: ellipsis;
|
|
2528
|
-
}
|
|
2638
|
+
/* Crew row narrow-width handling moved to the @container ladder on
|
|
2639
|
+
.monitor-cq (design v2): the feed's own width decides, not the window's. */
|
|
2529
2640
|
}
|
|
2530
2641
|
|
|
2531
2642
|
.overview-stack {
|
|
@@ -2758,7 +2869,10 @@ body.mc-fullscreen .top-tabs-row {
|
|
|
2758
2869
|
outline: none;
|
|
2759
2870
|
}
|
|
2760
2871
|
/* Shared solid status/lifecycle pill: filled semantic surface, no outline,
|
|
2761
|
-
slash-command chip geometry.
|
|
2872
|
+
slash-command chip geometry.
|
|
2873
|
+
Radius (design v2, 2026-08-05): capsule -> --mc-radius-chrome. The capsule
|
|
2874
|
+
read as a foreign shape next to the squared chrome it always sits beside;
|
|
2875
|
+
the fill and the mark carry the state, the outline never did. */
|
|
2762
2876
|
.lifecycle-pill {
|
|
2763
2877
|
display: inline-flex;
|
|
2764
2878
|
align-items: center;
|
|
@@ -2767,7 +2881,7 @@ body.mc-fullscreen .top-tabs-row {
|
|
|
2767
2881
|
font-weight: 600;
|
|
2768
2882
|
letter-spacing: 0.02em;
|
|
2769
2883
|
padding: 2px 6px;
|
|
2770
|
-
border-radius: var(--mc-radius-
|
|
2884
|
+
border-radius: var(--mc-radius-chrome);
|
|
2771
2885
|
border: none;
|
|
2772
2886
|
color: var(--text-muted);
|
|
2773
2887
|
background: var(--bg-secondary, transparent);
|
|
@@ -2951,7 +3065,7 @@ body.mc-fullscreen .top-tabs-row {
|
|
|
2951
3065
|
font-weight: 600;
|
|
2952
3066
|
letter-spacing: 0.02em;
|
|
2953
3067
|
padding: 3px var(--mc-space-md);
|
|
2954
|
-
border-radius: var(--mc-radius-
|
|
3068
|
+
border-radius: var(--mc-radius-chrome);
|
|
2955
3069
|
border: none;
|
|
2956
3070
|
color: var(--text-secondary);
|
|
2957
3071
|
background: var(--bg-card);
|
|
@@ -2976,7 +3090,9 @@ body.mc-fullscreen .top-tabs-row {
|
|
|
2976
3090
|
color: var(--green);
|
|
2977
3091
|
background: var(--green-bg);
|
|
2978
3092
|
}
|
|
2979
|
-
/* Idle:
|
|
3093
|
+
/* Idle: hollow mark + unfilled surface. The radius stopped carrying this cue
|
|
3094
|
+
when the pills squared off (design v2); the ○ mark is now the whole non-color
|
|
3095
|
+
signal, which is why it may not be dropped. */
|
|
2980
3096
|
.now-status-idle {
|
|
2981
3097
|
color: var(--text-muted);
|
|
2982
3098
|
background: var(--bg-card);
|
|
@@ -2991,7 +3107,7 @@ body.mc-fullscreen .top-tabs-row {
|
|
|
2991
3107
|
font-weight: var(--mc-chrome-meta-weight);
|
|
2992
3108
|
letter-spacing: 0.02em;
|
|
2993
3109
|
padding: 3px var(--mc-space-md);
|
|
2994
|
-
border-radius: var(--mc-radius-
|
|
3110
|
+
border-radius: var(--mc-radius-chrome);
|
|
2995
3111
|
color: var(--blue);
|
|
2996
3112
|
background: var(--blue-bg);
|
|
2997
3113
|
text-transform: uppercase;
|
|
@@ -3922,6 +4038,61 @@ function setDashboardSkin(skin) {
|
|
|
3922
4038
|
return false;
|
|
3923
4039
|
}
|
|
3924
4040
|
|
|
4041
|
+
// ===== Crew Monitor row density (compact | comfortable; local preference) =====
|
|
4042
|
+
const MONITOR_DENSITY_KEY = 'agent-kit:monitor-density';
|
|
4043
|
+
const MONITOR_DENSITIES = ['compact', 'comfortable'];
|
|
4044
|
+
/** Comfortable needs horizontal room for a two-line label to be worth the height. */
|
|
4045
|
+
const MONITOR_DENSITY_COMFORTABLE_MIN_WIDTH = 900;
|
|
4046
|
+
|
|
4047
|
+
function normalizeMonitorDensity(density) {
|
|
4048
|
+
return MONITOR_DENSITIES.includes(density) ? density : null;
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
/** Viewport auto-pick, used only when the operator stored no preference. */
|
|
4052
|
+
function autoMonitorDensity() {
|
|
4053
|
+
return (window.innerWidth || 0) >= MONITOR_DENSITY_COMFORTABLE_MIN_WIDTH
|
|
4054
|
+
? 'comfortable'
|
|
4055
|
+
: 'compact';
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
function getStoredMonitorDensity() {
|
|
4059
|
+
try {
|
|
4060
|
+
return normalizeMonitorDensity(localStorage.getItem(MONITOR_DENSITY_KEY));
|
|
4061
|
+
} catch (e) {
|
|
4062
|
+
return null;
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
/** Current mode from the root attribute (set pre-paint), else the auto-pick. */
|
|
4067
|
+
function currentMonitorDensity() {
|
|
4068
|
+
return (
|
|
4069
|
+
normalizeMonitorDensity(document.documentElement.getAttribute('data-monitor-density')) ||
|
|
4070
|
+
getStoredMonitorDensity() ||
|
|
4071
|
+
autoMonitorDensity()
|
|
4072
|
+
);
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
/** Apply density via root data attribute; an explicit choice persists and wins. */
|
|
4076
|
+
function applyMonitorDensity(density, opts) {
|
|
4077
|
+
const next = normalizeMonitorDensity(density) || autoMonitorDensity();
|
|
4078
|
+
document.documentElement.setAttribute('data-monitor-density', next);
|
|
4079
|
+
document.querySelectorAll('[data-monitor-density-toggle]').forEach((el) => {
|
|
4080
|
+
el.setAttribute('aria-pressed', next === 'comfortable' ? 'true' : 'false');
|
|
4081
|
+
el.textContent = next === 'comfortable' ? 'Comfortable' : 'Compact';
|
|
4082
|
+
});
|
|
4083
|
+
if (!opts || opts.persist !== false) {
|
|
4084
|
+
try {
|
|
4085
|
+
localStorage.setItem(MONITOR_DENSITY_KEY, next);
|
|
4086
|
+
} catch (e) { /* private mode / blocked storage */ }
|
|
4087
|
+
}
|
|
4088
|
+
return next;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
function toggleMonitorDensity() {
|
|
4092
|
+
applyMonitorDensity(currentMonitorDensity() === 'comfortable' ? 'compact' : 'comfortable');
|
|
4093
|
+
return false;
|
|
4094
|
+
}
|
|
4095
|
+
|
|
3925
4096
|
// ===== More-sections dropdown =====
|
|
3926
4097
|
function isNavMoreOpen() {
|
|
3927
4098
|
const menu = document.getElementById('navMoreMenu');
|
|
@@ -4190,11 +4361,15 @@ function semanticEventInfo(kind, commitType) {
|
|
|
4190
4361
|
// Monitor live kinds: distinct resting color+icon (no kind-tag text, no left rail).
|
|
4191
4362
|
// run_plan follows .now-status-executing green; delivery uses cyan to avoid green wash.
|
|
4192
4363
|
// Gloss strings are default software lexicon display masks (kind ids unchanged).
|
|
4193
|
-
run_plan: { icon: '\u25b6', tag: 'tick', gloss: '
|
|
4194
|
-
handoff: { icon: '\u23f8', tag: 'handoff', gloss: '
|
|
4195
|
-
agent_step: { icon: '\u25c9', tag: 'step', gloss: '
|
|
4364
|
+
run_plan: { icon: '\u25b6', tag: 'tick', gloss: 'Project Manager - live execution', color: 'var(--green)', bg: 'var(--green-bg)' },
|
|
4365
|
+
handoff: { icon: '\u23f8', tag: 'handoff', gloss: 'Project Manager - awaiting gate', color: 'var(--yellow)', bg: 'var(--yellow-bg)' },
|
|
4366
|
+
agent_step: { icon: '\u25c9', tag: 'step', gloss: 'Developer - task unit', color: 'var(--purple)', bg: 'var(--purple-bg)' },
|
|
4196
4367
|
plan_progress: { icon: '\u2691', tag: 'plan', gloss: 'Product Owner - milestone', color: 'var(--orange)', bg: 'var(--orange-bg)' },
|
|
4197
|
-
delivery: { icon: '\u2714', tag: 'delivery', gloss: 'DevOps
|
|
4368
|
+
delivery: { icon: '\u2714', tag: 'delivery', gloss: 'DevOps - merged unit', color: 'var(--cyan)', bg: 'var(--cyan-bg)' },
|
|
4369
|
+
// Live Task-worker lifecycle (running / done / failed) and background
|
|
4370
|
+
// mid-batch review pointers. Both added 2026-08-05; kind ids are additive.
|
|
4371
|
+
subagent: { icon: '\u25cc', tag: 'subagent', gloss: 'Developer - subagent run', color: 'var(--blue)', bg: 'var(--blue-bg)' },
|
|
4372
|
+
plan_review: { icon: '\u2611', tag: 'review', gloss: 'QA - review evidence', color: 'var(--orange)', bg: 'var(--orange-bg)' },
|
|
4198
4373
|
commit: { icon: '\u25cf', tag: 'commit', gloss: 'commit', color: 'var(--orange)', bg: 'var(--orange-bg)' },
|
|
4199
4374
|
agent: { icon: '\u25c6', tag: 'agent', gloss: 'agent', color: 'var(--purple)', bg: 'var(--purple-bg)' },
|
|
4200
4375
|
skill: { icon: '\u2726', tag: 'skill', gloss: 'skill', color: 'var(--orange)', bg: 'var(--orange-bg)' },
|
|
@@ -4206,12 +4381,12 @@ function semanticEventInfo(kind, commitType) {
|
|
|
4206
4381
|
// Locked BMP map (hotfix 2026-07-27): feat✦ fix⚙ docs✎ chore⚒ pr⑂ ship✈
|
|
4207
4382
|
if (kind === 'delivery' && commitType) {
|
|
4208
4383
|
const subtypes = {
|
|
4209
|
-
feat: { icon: '\u2726', tag: 'delivery', gloss: 'DevOps
|
|
4210
|
-
fix: { icon: '\u2699', tag: 'delivery', gloss: 'DevOps
|
|
4211
|
-
docs: { icon: '\u270e', tag: 'delivery', gloss: 'DevOps
|
|
4212
|
-
chore: { icon: '\u2692', tag: 'delivery', gloss: 'DevOps
|
|
4213
|
-
pr: { icon: '\u2442', tag: 'delivery', gloss: 'DevOps
|
|
4214
|
-
ship: { icon: '\u2708', tag: 'delivery', gloss: 'DevOps
|
|
4384
|
+
feat: { icon: '\u2726', tag: 'delivery', gloss: 'DevOps - feat', color: 'var(--purple)', bg: 'var(--purple-bg)' },
|
|
4385
|
+
fix: { icon: '\u2699', tag: 'delivery', gloss: 'DevOps - fix', color: 'var(--red)', bg: 'var(--red-bg)' },
|
|
4386
|
+
docs: { icon: '\u270e', tag: 'delivery', gloss: 'DevOps - docs', color: 'var(--blue)', bg: 'var(--blue-bg)' },
|
|
4387
|
+
chore: { icon: '\u2692', tag: 'delivery', gloss: 'DevOps - chore', color: 'var(--orange)', bg: 'var(--orange-bg)' },
|
|
4388
|
+
pr: { icon: '\u2442', tag: 'delivery', gloss: 'DevOps - PR', color: 'var(--cyan)', bg: 'var(--cyan-bg)' },
|
|
4389
|
+
ship: { icon: '\u2708', tag: 'delivery', gloss: 'DevOps - merged unit', color: 'var(--cyan)', bg: 'var(--cyan-bg)' },
|
|
4215
4390
|
};
|
|
4216
4391
|
if (subtypes[commitType]) return subtypes[commitType];
|
|
4217
4392
|
}
|
|
@@ -4220,7 +4395,7 @@ function semanticEventInfo(kind, commitType) {
|
|
|
4220
4395
|
|
|
4221
4396
|
/** Monitor hero allowlist (curated subset). Inventory kinds stay on the full stream. */
|
|
4222
4397
|
const MONITOR_ACTIVITY_KINDS = new Set([
|
|
4223
|
-
'run_plan', 'handoff', 'delivery', 'agent_step',
|
|
4398
|
+
'run_plan', 'handoff', 'delivery', 'agent_step', 'subagent', 'plan_review',
|
|
4224
4399
|
]);
|
|
4225
4400
|
/**
|
|
4226
4401
|
* Crew Monitor display cap from missionControl.monitorFeedCap
|
|
@@ -4415,15 +4590,165 @@ function crewEventTime(ev, info) {
|
|
|
4415
4590
|
}
|
|
4416
4591
|
|
|
4417
4592
|
/**
|
|
4418
|
-
* Crew feed actor
|
|
4419
|
-
*
|
|
4420
|
-
*
|
|
4593
|
+
* Crew feed actor — WIRE value, not display. Kit agent id, else Eng for
|
|
4594
|
+
* delivery, else SQ when a plan is present (never the full plan filename),
|
|
4595
|
+
* else Eng. Mirrors briefActivityActor() in dashboard/lib/semantic-model.mjs;
|
|
4596
|
+
* the two must not drift, which is why the masks below live in a separate
|
|
4597
|
+
* layer instead of being folded in here.
|
|
4598
|
+
*
|
|
4599
|
+
* `Eng` still collides between the delivery and system fallbacks on the wire.
|
|
4600
|
+
* The row no longer leaves it ambiguous: crewActorRole() splits it by kind.
|
|
4601
|
+
* Used directly only as the aria-label fallback when a row has no label.
|
|
4421
4602
|
*/
|
|
4422
4603
|
function crewEventActor(ev) {
|
|
4423
4604
|
if (ev && ev.agent) return String(ev.agent);
|
|
4424
|
-
if (ev && ev.kind === 'delivery') return '
|
|
4425
|
-
if (ev && ev.refs && ev.refs.plan) return '
|
|
4426
|
-
return '
|
|
4605
|
+
if (ev && ev.kind === 'delivery') return 'Eng';
|
|
4606
|
+
if (ev && ev.refs && ev.refs.plan) return 'SQ';
|
|
4607
|
+
return 'Eng';
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4610
|
+
/**
|
|
4611
|
+
* Operator lexicon (design v2, 2026-08-05): wire actor -> display role.
|
|
4612
|
+
* Display only. The wire format is frozen — `labelFull` keeps the raw seg0
|
|
4613
|
+
* (`Eng`, `docs-repo`, …) and the collectors emit exactly what they emitted
|
|
4614
|
+
* before, so nothing here is a backend change.
|
|
4615
|
+
*/
|
|
4616
|
+
const CREW_ACTOR_MASK = {
|
|
4617
|
+
'docs-repo': 'Tech Writer',
|
|
4618
|
+
explore: 'Product Analyst',
|
|
4619
|
+
generalPurpose: 'Analyst',
|
|
4620
|
+
SQ: 'Scrum Master',
|
|
4621
|
+
Dev: 'Developer',
|
|
4622
|
+
QA: 'QA',
|
|
4623
|
+
PO: 'Product Owner',
|
|
4624
|
+
PM: 'Project Manager',
|
|
4625
|
+
};
|
|
4626
|
+
|
|
4627
|
+
/**
|
|
4628
|
+
* The `Eng` split. The lexicon maps two different roles onto the same wire
|
|
4629
|
+
* token, and six consecutive `Eng · merged` rows were the single worst thing in
|
|
4630
|
+
* the shipped feed. `kind` already carries the distinction, so no new wire
|
|
4631
|
+
* field is needed (ACCEPTANCE.md delta B): a delivery row's `Eng` is the
|
|
4632
|
+
* Platform Engineer, every other `Eng` is the Engineering Manager.
|
|
4633
|
+
*/
|
|
4634
|
+
function crewActorEngRole(ev) {
|
|
4635
|
+
return ev && ev.kind === 'delivery' ? 'DevOps' : 'Tech Lead';
|
|
4636
|
+
}
|
|
4637
|
+
|
|
4638
|
+
/**
|
|
4639
|
+
* Display role for a Crew row. Resolution order (design contract):
|
|
4640
|
+
* per-event `ev.role` -> mask -> raw token. `ev.role` is optional and nothing
|
|
4641
|
+
* renders from it today; it is honoured so a collector can override the split
|
|
4642
|
+
* later without another dashboard change.
|
|
4643
|
+
* @param {object} ev
|
|
4644
|
+
* @param {string} raw seg0 of the row label (falls back to crewEventActor)
|
|
4645
|
+
*/
|
|
4646
|
+
function crewActorRole(ev, raw) {
|
|
4647
|
+
if (ev && ev.role) return String(ev.role);
|
|
4648
|
+
const token = String(raw || crewEventActor(ev) || '').trim();
|
|
4649
|
+
if (token === 'Eng') return crewActorEngRole(ev);
|
|
4650
|
+
return CREW_ACTOR_MASK[token] || token;
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4653
|
+
/**
|
|
4654
|
+
* Badge monogram for a display role. First letter of the first two words,
|
|
4655
|
+
* single word -> first two chars. Two explicit overrides: Developer and DevOps
|
|
4656
|
+
* both start `D`+`e`/`DV`-shaped and would otherwise collide on `DE`.
|
|
4657
|
+
* Distinct from agentInitials(), which monograms a kebab-case agent id for the
|
|
4658
|
+
* Agents card; this one monograms a spaced display role.
|
|
4659
|
+
*/
|
|
4660
|
+
function crewActorInitials(role) {
|
|
4661
|
+
const special = { Developer: 'DV', DevOps: 'DO' };
|
|
4662
|
+
if (special[role]) return special[role];
|
|
4663
|
+
const words = String(role || '').trim().split(/\s+/).filter(Boolean);
|
|
4664
|
+
if (words.length === 0) return '?';
|
|
4665
|
+
if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
|
|
4666
|
+
return (words[0][0] + words[1][0]).toUpperCase();
|
|
4667
|
+
}
|
|
4668
|
+
|
|
4669
|
+
/**
|
|
4670
|
+
* Verb state icons. The design specified Feather geometry on a viewBox-24 grid;
|
|
4671
|
+
* these are the same shapes redrawn on the dashboard's own viewBox-16 grid, so
|
|
4672
|
+
* the file keeps ONE icon geometry (spaceIconSvg, nowMetaIconSvg and the header
|
|
4673
|
+
* refresh control are all 16). Importing a second grid for six row marks would
|
|
4674
|
+
* have split the icon system for no legibility gain.
|
|
4675
|
+
*
|
|
4676
|
+
* They are row state marks, not chrome: they render at 12px inside a text
|
|
4677
|
+
* column, so the CSS overrides stroke-width to 1.6 — the 16-grid equivalent of
|
|
4678
|
+
* the design's 2.4-on-24 optical compensation, since 1.5 at 12px renders
|
|
4679
|
+
* lighter than 1.5 at the chrome set's 16px.
|
|
4680
|
+
* Static markup only — the verb is a map key, never concatenated into the SVG.
|
|
4681
|
+
*/
|
|
4682
|
+
const CREW_VERB_ICON_PATHS = {
|
|
4683
|
+
play: '<polygon points="3.5 2.5 12.5 8 3.5 13.5"/>',
|
|
4684
|
+
pause: '<line x1="6" y1="3.5" x2="6" y2="12.5"/><line x1="10" y1="3.5" x2="10" y2="12.5"/>',
|
|
4685
|
+
check: '<polyline points="13.5 4 6 11.5 2.5 8"/>',
|
|
4686
|
+
x: '<line x1="12" y1="4" x2="4" y2="12"/><line x1="4" y1="4" x2="12" y2="12"/>',
|
|
4687
|
+
'git-merge': '<circle cx="12" cy="12" r="2"/><circle cx="4" cy="4" r="2"/><path d="M4 14V6a6 6 0 0 0 6 6"/>',
|
|
4688
|
+
'circle-slash': '<circle cx="8" cy="8" r="6.5"/><line x1="3.4" y1="3.4" x2="12.6" y2="12.6"/>',
|
|
4689
|
+
info: '<circle cx="8" cy="8" r="6.5"/><line x1="8" y1="11" x2="8" y2="8"/><line x1="8" y1="5" x2="8.01" y2="5"/>',
|
|
4690
|
+
flag: '<path d="M2.7 10s.7-.7 2.7-.7 3.3 1.3 5.3 1.3 2.7-.7 2.7-.7V2s-.7.7-2.7.7-3.3-1.3-5.3-1.3-2.7.7-2.7.7z"/><line x1="2.7" y1="14.7" x2="2.7" y2="10"/>',
|
|
4691
|
+
};
|
|
4692
|
+
|
|
4693
|
+
/** Verb -> state icon. Replaces the 6px state dot (design v2). */
|
|
4694
|
+
const CREW_VERB_ICON = {
|
|
4695
|
+
running: 'play',
|
|
4696
|
+
awaiting: 'pause',
|
|
4697
|
+
done: 'check',
|
|
4698
|
+
failed: 'x',
|
|
4699
|
+
merged: 'git-merge',
|
|
4700
|
+
parked: 'circle-slash',
|
|
4701
|
+
};
|
|
4702
|
+
|
|
4703
|
+
/** Decorative row icon; unknown verbs fall back to `info` rather than nothing. */
|
|
4704
|
+
function crewRowIconSvg(name, className) {
|
|
4705
|
+
const paths = CREW_VERB_ICON_PATHS[name] || CREW_VERB_ICON_PATHS.info;
|
|
4706
|
+
return `<svg class="${className}" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
4707
|
+
}
|
|
4708
|
+
|
|
4709
|
+
/**
|
|
4710
|
+
* Split a Crew row label into the v2 column contract.
|
|
4711
|
+
* Refs (PR, SHA, worker id, plan) leave the flexible text so they can render as
|
|
4712
|
+
* fixed-content chips that hide whole instead of ellipsising. Everything left
|
|
4713
|
+
* over joins the primary field — the row's only wide, only ellipsising column.
|
|
4714
|
+
* Plan detection prefers `refs.plan` over sniffing the rendered string, since
|
|
4715
|
+
* the string is capped at MAX_SEMANTIC_LABEL upstream and may not carry it.
|
|
4716
|
+
*/
|
|
4717
|
+
function parseCrewRow(ev) {
|
|
4718
|
+
const segs = String((ev && ev.label) || '').split(' · ');
|
|
4719
|
+
const planRef = ev && ev.refs && ev.refs.plan ? String(ev.refs.plan) : '';
|
|
4720
|
+
const planBase = planRef.replace(/\.plan\.md$/i, '');
|
|
4721
|
+
const out = {
|
|
4722
|
+
actor: segs[0] || '',
|
|
4723
|
+
verb: segs[1] || '',
|
|
4724
|
+
mids: [],
|
|
4725
|
+
pr: null,
|
|
4726
|
+
sha: null,
|
|
4727
|
+
worker: null,
|
|
4728
|
+
plan: planRef || null,
|
|
4729
|
+
};
|
|
4730
|
+
for (let i = 2; i < segs.length; i++) {
|
|
4731
|
+
const seg = segs[i];
|
|
4732
|
+
if (/\.plan\.md$/i.test(seg) || (planRef && (seg === planRef || seg === planBase))) {
|
|
4733
|
+
out.plan = out.plan || seg;
|
|
4734
|
+
continue;
|
|
4735
|
+
}
|
|
4736
|
+
if (/^PR #\d+/.test(seg)) {
|
|
4737
|
+
out.pr = seg.replace(/^PR /, '');
|
|
4738
|
+
continue;
|
|
4739
|
+
}
|
|
4740
|
+
if (/^[0-9a-f]{7,8}$/.test(seg)) {
|
|
4741
|
+
// A subagent's trailing hex is a worker id, not a commit — same shape,
|
|
4742
|
+
// different meaning, and only the SHA chip is droppable at ≤560px.
|
|
4743
|
+
if (ev && ev.kind === 'subagent') out.worker = seg;
|
|
4744
|
+
else out.sha = seg;
|
|
4745
|
+
continue;
|
|
4746
|
+
}
|
|
4747
|
+
out.mids.push(seg);
|
|
4748
|
+
}
|
|
4749
|
+
// A row with no mid segments still needs something in its wide column.
|
|
4750
|
+
out.primary = out.mids.join(' · ') || planBase || out.verb;
|
|
4751
|
+
return out;
|
|
4427
4752
|
}
|
|
4428
4753
|
|
|
4429
4754
|
/** Avatar-style monogram: first letters of the first two kebab/space parts, else first two chars. */
|
|
@@ -5164,10 +5489,15 @@ const CONFIG_PERSONA_MODES = [
|
|
|
5164
5489
|
];
|
|
5165
5490
|
|
|
5166
5491
|
function personaOptionHtml(selected) {
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5492
|
+
const inheritSel = selected === '' ? ' selected' : '';
|
|
5493
|
+
const inherit = `<option value=""${inheritSel}>Inherit default</option>`;
|
|
5494
|
+
return (
|
|
5495
|
+
inherit +
|
|
5496
|
+
CONFIG_PERSONA_OPTIONS.map((id) => {
|
|
5497
|
+
const sel = id === selected ? ' selected' : '';
|
|
5498
|
+
return `<option value="${escapeAttr(id)}"${sel}>${escapeHtml(id)}</option>`;
|
|
5499
|
+
}).join('')
|
|
5500
|
+
);
|
|
5171
5501
|
}
|
|
5172
5502
|
|
|
5173
5503
|
function renderConfigSection(d) {
|
|
@@ -5201,7 +5531,7 @@ function renderConfigSection(d) {
|
|
|
5201
5531
|
<div class="config-row">
|
|
5202
5532
|
<label for="config-persona-${escapeAttr(mode.id)}">${escapeHtml(mode.label)} persona</label>
|
|
5203
5533
|
<select id="config-persona-${escapeAttr(mode.id)}" name="personaMode:${escapeAttr(mode.id)}" data-focus-key="config-persona-${escapeAttr(mode.id)}">
|
|
5204
|
-
${personaOptionHtml(current
|
|
5534
|
+
${personaOptionHtml(current)}
|
|
5205
5535
|
</select>
|
|
5206
5536
|
</div>`;
|
|
5207
5537
|
}).join('');
|
|
@@ -5276,9 +5606,31 @@ function renderConfigSection(d) {
|
|
|
5276
5606
|
<div class="config-row">
|
|
5277
5607
|
<label for="config-epr-backend">Backend</label>
|
|
5278
5608
|
<select id="config-epr-backend" name="eprBackend" data-focus-key="config-epr-backend">
|
|
5609
|
+
<option value="auto"${epr.backend === 'auto' ? ' selected' : ''}>auto</option>
|
|
5279
5610
|
<option value="claude"${epr.backend === 'claude' || !epr.backend ? ' selected' : ''}>claude</option>
|
|
5611
|
+
<option value="cursor"${epr.backend === 'cursor' ? ' selected' : ''}>cursor</option>
|
|
5280
5612
|
</select>
|
|
5281
|
-
<span class="config-hint">claude
|
|
5613
|
+
<span class="config-hint">auto uses Claude when usable, else Cursor Agent. Pin claude or cursor to force one backend.</span>
|
|
5614
|
+
</div>
|
|
5615
|
+
<div class="config-row">
|
|
5616
|
+
<label for="config-epr-reviewerModel">Reviewer model</label>
|
|
5617
|
+
<input type="text" id="config-epr-reviewerModel" name="eprReviewerModel" value="${escapeAttr(epr.reviewerModel || 'haiku')}" maxlength="64" data-focus-key="config-epr-reviewerModel" />
|
|
5618
|
+
<span class="config-hint">Claude default haiku. Must differ from the implementer stamp (Auto/Auto refused).</span>
|
|
5619
|
+
</div>
|
|
5620
|
+
<div class="config-row">
|
|
5621
|
+
<label for="config-epr-advisorModel">Advisor model</label>
|
|
5622
|
+
<input type="text" id="config-epr-advisorModel" name="eprAdvisorModel" value="${escapeAttr(epr.advisorModel || 'opus')}" maxlength="64" data-focus-key="config-epr-advisorModel" />
|
|
5623
|
+
<span class="config-hint">Opus only when the monitor marks high severity or uncertainty.</span>
|
|
5624
|
+
</div>
|
|
5625
|
+
<div class="config-row">
|
|
5626
|
+
<label for="config-epr-waitSlice">Wait slice (seconds)</label>
|
|
5627
|
+
<input type="number" id="config-epr-waitSlice" name="eprWaitSlice" min="1" max="3600" step="1" value="${escapeAttr(String(epr.waitSliceSeconds || 90))}" data-focus-key="config-epr-waitSlice" />
|
|
5628
|
+
<span class="config-hint">Chat AwaitShell budget per session. Default 90.</span>
|
|
5629
|
+
</div>
|
|
5630
|
+
<div class="config-row">
|
|
5631
|
+
<label for="config-epr-waitTimeout">Wait timeout (seconds)</label>
|
|
5632
|
+
<input type="number" id="config-epr-waitTimeout" name="eprWaitTimeout" min="1" max="86400" step="1" value="${escapeAttr(String(epr.waitTimeoutSeconds || 900))}" data-focus-key="config-epr-waitTimeout" />
|
|
5633
|
+
<span class="config-hint">Total remaining budget across slices and CI. Default 900.</span>
|
|
5282
5634
|
</div>
|
|
5283
5635
|
<div class="config-row">
|
|
5284
5636
|
<label for="config-epr-mode">Arming mode</label>
|
|
@@ -5350,6 +5702,18 @@ function collectMissionConfigPayload() {
|
|
|
5350
5702
|
offerOnExhausted: !!document.getElementById('config-epr-offer')?.checked,
|
|
5351
5703
|
autoRemediate: !!document.getElementById('config-epr-auto')?.checked,
|
|
5352
5704
|
backend: document.getElementById('config-epr-backend')?.value || 'claude',
|
|
5705
|
+
reviewerModel: (document.getElementById('config-epr-reviewerModel')?.value || 'haiku').trim(),
|
|
5706
|
+
advisorModel: (document.getElementById('config-epr-advisorModel')?.value || 'opus').trim(),
|
|
5707
|
+
waitSliceSeconds: (() => {
|
|
5708
|
+
const raw = document.getElementById('config-epr-waitSlice')?.value;
|
|
5709
|
+
const n = Number.parseInt(String(raw || '90'), 10);
|
|
5710
|
+
return Number.isFinite(n) && n >= 1 ? n : 90;
|
|
5711
|
+
})(),
|
|
5712
|
+
waitTimeoutSeconds: (() => {
|
|
5713
|
+
const raw = document.getElementById('config-epr-waitTimeout')?.value;
|
|
5714
|
+
const n = Number.parseInt(String(raw || '900'), 10);
|
|
5715
|
+
return Number.isFinite(n) && n >= 1 ? n : 900;
|
|
5716
|
+
})(),
|
|
5353
5717
|
mode: document.getElementById('config-epr-mode')?.value || 'paste',
|
|
5354
5718
|
midBatchAudits: !!document.getElementById('config-epr-midBatch')?.checked,
|
|
5355
5719
|
preflight: document.getElementById('config-epr-preflight')?.value || 'off',
|
|
@@ -5361,7 +5725,9 @@ function collectMissionConfigPayload() {
|
|
|
5361
5725
|
};
|
|
5362
5726
|
for (const mode of CONFIG_PERSONA_MODES) {
|
|
5363
5727
|
const el = document.getElementById(`config-persona-${mode.id}`);
|
|
5364
|
-
if (el
|
|
5728
|
+
if (!el) continue;
|
|
5729
|
+
// Empty = Inherit default: send null so merge deletes a stored override.
|
|
5730
|
+
payload.agentPersona.modes[mode.id] = el.value ? el.value : null;
|
|
5365
5731
|
}
|
|
5366
5732
|
return payload;
|
|
5367
5733
|
}
|
|
@@ -5452,7 +5818,7 @@ const HEALTH_CHECK_META = {
|
|
|
5452
5818
|
handoff: {
|
|
5453
5819
|
okDetail: 'HANDOFF.md is present and parseable (idle Plan is still healthy).',
|
|
5454
5820
|
failDetail: 'HANDOFF.md is missing or not parseable.',
|
|
5455
|
-
autofix: { text: '/
|
|
5821
|
+
autofix: { text: '/handoff', subject: '/handoff', destination: 'chatInput', label: 'Autofix' },
|
|
5456
5822
|
},
|
|
5457
5823
|
agents: {
|
|
5458
5824
|
okDetail: 'Agents are optional in L0; empty .cursor/agents/ is healthy. Cataloged agents (if any) are listed in Agents.',
|
|
@@ -5468,12 +5834,13 @@ const HEALTH_CHECK_META = {
|
|
|
5468
5834
|
memory: {
|
|
5469
5835
|
okDetail: 'Memory has recorded errors and/or decisions.',
|
|
5470
5836
|
failDetail: 'Memory index has no errors or decisions yet (optional until first WRITE).',
|
|
5471
|
-
autofix: { text: '/
|
|
5837
|
+
autofix: { text: '.cursor/memory/', subject: 'path .cursor/memory/', destination: 'filePicker', label: 'Copy path' },
|
|
5472
5838
|
},
|
|
5473
5839
|
git: {
|
|
5474
5840
|
okDetail: 'Git branch signal is available.',
|
|
5475
5841
|
failDetail: 'Git repository signal missing (no branch).',
|
|
5476
|
-
|
|
5842
|
+
// Covers not-a-repo and git-init-but-zero-commits (rev-parse HEAD fails until first commit).
|
|
5843
|
+
autofix: { text: 'git init && git commit --allow-empty -m "init"', subject: 'git init && git commit --allow-empty -m "init"', destination: 'terminal', label: 'Autofix' },
|
|
5477
5844
|
},
|
|
5478
5845
|
config: {
|
|
5479
5846
|
okDetail: 'Workspace config is loaded.',
|
|
@@ -5491,23 +5858,28 @@ const HEALTH_VITAL_GROUPS = [
|
|
|
5491
5858
|
];
|
|
5492
5859
|
|
|
5493
5860
|
function healthCheckSeverity(check, aggregateStatus) {
|
|
5861
|
+
// Per-check levels are ok | warning | degraded only. Aggregate snapshot/serve
|
|
5862
|
+
// `error` is empty-checks (presence + empty state); producers never emit
|
|
5863
|
+
// status:error with checks present, so do not invent a per-check error sev.
|
|
5494
5864
|
if (check && check.ok) return 'ok';
|
|
5495
5865
|
if (aggregateStatus === 'degraded') return 'degraded';
|
|
5496
|
-
if (aggregateStatus === 'error') return 'error';
|
|
5497
5866
|
return 'warning';
|
|
5498
5867
|
}
|
|
5499
5868
|
|
|
5500
|
-
/*
|
|
5501
|
-
|
|
5869
|
+
/* Severity chrome: per-check uses ok | warning | degraded; aggregate `error`
|
|
5870
|
+
remains for snapshot/serve presence only (empty checks). Tone drives the dot
|
|
5871
|
+
class; label drives text/aria. Per-check [data-sev] CSS matches the severity
|
|
5872
|
+
key written on the element, not a chrome field. Aggregate error has no
|
|
5873
|
+
per-check CSS (producers never emit error+checks). */
|
|
5502
5874
|
const HEALTH_SEVERITY_CHROME = {
|
|
5503
|
-
ok: { tone: 'green', label: 'ok'
|
|
5504
|
-
warning: { tone: 'yellow', label: 'warn'
|
|
5505
|
-
degraded: { tone: 'orange', label: 'degraded'
|
|
5506
|
-
error: { tone: 'red', label: 'error'
|
|
5875
|
+
ok: { tone: 'green', label: 'ok' },
|
|
5876
|
+
warning: { tone: 'yellow', label: 'warn' },
|
|
5877
|
+
degraded: { tone: 'orange', label: 'degraded' },
|
|
5878
|
+
error: { tone: 'red', label: 'error' },
|
|
5507
5879
|
};
|
|
5508
5880
|
|
|
5509
5881
|
function healthSeverityChrome(sev) {
|
|
5510
|
-
return HEALTH_SEVERITY_CHROME[sev] || { tone: 'gray', label: sev || 'unknown'
|
|
5882
|
+
return HEALTH_SEVERITY_CHROME[sev] || { tone: 'gray', label: sev || 'unknown' };
|
|
5511
5883
|
}
|
|
5512
5884
|
|
|
5513
5885
|
function healthSeverityLabel(sev) {
|
|
@@ -5526,13 +5898,6 @@ function toggleHealthCheck(checkId) {
|
|
|
5526
5898
|
if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
5527
5899
|
}
|
|
5528
5900
|
|
|
5529
|
-
function healthCheckKeydown(event, checkId) {
|
|
5530
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
5531
|
-
event.preventDefault();
|
|
5532
|
-
toggleHealthCheck(checkId);
|
|
5533
|
-
}
|
|
5534
|
-
}
|
|
5535
|
-
|
|
5536
5901
|
// ===== Memory tab: interactive error rows (expand + copy fix prompt) =====
|
|
5537
5902
|
function toggleMemoryError(idx) {
|
|
5538
5903
|
const card = document.getElementById('memory-error-' + idx);
|
|
@@ -5584,19 +5949,6 @@ function renderMemoryErrorRow(entry, idx) {
|
|
|
5584
5949
|
`;
|
|
5585
5950
|
}
|
|
5586
5951
|
|
|
5587
|
-
/** @deprecated Prefer toggleHealthCheck; kept for any leftover callers. */
|
|
5588
|
-
function showHealthInfo(el, ok) {
|
|
5589
|
-
const parent = el.parentElement;
|
|
5590
|
-
const existing = parent.querySelector('.health-message');
|
|
5591
|
-
if (existing) { existing.remove(); return; }
|
|
5592
|
-
const msg = document.createElement('div');
|
|
5593
|
-
msg.className = 'health-message';
|
|
5594
|
-
msg.textContent = ok
|
|
5595
|
-
? 'All good: no action needed.'
|
|
5596
|
-
: 'Check may indicate an issue. Review the relevant logs or configuration.';
|
|
5597
|
-
parent.appendChild(msg);
|
|
5598
|
-
}
|
|
5599
|
-
|
|
5600
5952
|
// ===== Get connection uptime (Phase 2) =====
|
|
5601
5953
|
function getConnectionUptime() {
|
|
5602
5954
|
if (sseMode !== 'live') return '';
|
|
@@ -7438,8 +7790,9 @@ function renderUnsafe() {
|
|
|
7438
7790
|
<div class="live-activity-header">
|
|
7439
7791
|
<span class="card-title">${spaceIconSvg('monitor')}Crew Monitor</span>
|
|
7440
7792
|
<span class="section-subtitle" style="margin-left:auto">${feedEvents.length} shown</span>
|
|
7793
|
+
<button type="button" class="git-action-btn monitor-density-btn" data-monitor-density-toggle data-focus-key="monitor-density-toggle" aria-pressed="${currentMonitorDensity() === 'comfortable' ? 'true' : 'false'}" title="Toggle Crew Monitor row density (compact single-line rows vs comfortable two-line rows)" onclick="toggleMonitorDensity()">${currentMonitorDensity() === 'comfortable' ? 'Comfortable' : 'Compact'}</button>
|
|
7441
7794
|
</div>
|
|
7442
|
-
<div class="live-activity-feed">
|
|
7795
|
+
<div class="live-activity-feed monitor-cq">
|
|
7443
7796
|
${feedEvents.length === 0
|
|
7444
7797
|
? renderEmptyStateCta({
|
|
7445
7798
|
headline: 'Listening',
|
|
@@ -7453,42 +7806,56 @@ function renderUnsafe() {
|
|
|
7453
7806
|
const info = semanticEventInfo(ev.kind, ev.refs?.commitType);
|
|
7454
7807
|
const gloss = info.gloss || info.tag || ev.kind || 'event';
|
|
7455
7808
|
const staggerIdx = Math.min(idx, 5);
|
|
7456
|
-
const
|
|
7457
|
-
const
|
|
7809
|
+
const row = parseCrewRow(ev);
|
|
7810
|
+
const role = crewActorRole(ev, row.actor);
|
|
7811
|
+
const rawActor = row.actor || crewEventActor(ev);
|
|
7458
7812
|
const actionAttrs = activityTargetAttributes(ev, 'monitor-activity', { kindGloss: gloss });
|
|
7459
7813
|
const rowA11y = actionAttrs
|
|
7460
7814
|
? actionAttrs
|
|
7461
|
-
: ` aria-label="${escapeAttr(`${gloss}: ${ev.label ||
|
|
7462
|
-
//
|
|
7463
|
-
//
|
|
7464
|
-
//
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
const
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7815
|
+
: ` aria-label="${escapeAttr(`${gloss}: ${ev.label || role}`)}"`;
|
|
7816
|
+
// Row order (design v2, 2026-08-05): team badge, actor, verb,
|
|
7817
|
+
// primary, ref chips, time. The kind glyph is gone — its colour
|
|
7818
|
+
// moved onto the badge and its meaning onto the badge tooltip, so
|
|
7819
|
+
// nothing composes a glyph into the text and a flat single-segment
|
|
7820
|
+
// label cannot misplace one. `info.bg` tints the badge; that is the
|
|
7821
|
+
// only place a Crew row carries it.
|
|
7822
|
+
const badgeHtml = `<span class="monitor-row-badge" style="background:${info.bg};color:${info.color}" title="${escapeAttr(gloss)}" aria-hidden="true">${escapeHtml(crewActorInitials(role))}</span>`;
|
|
7823
|
+
// Actor tooltip keeps the wire token next to the mask, so an
|
|
7824
|
+
// operator reading `Tech Lead` can still see it came from `Eng`.
|
|
7825
|
+
const actorHtml = `<span class="monitor-row-actor" title="${escapeAttr(`${role} (${rawActor})`)}">${escapeHtml(role)}</span>`;
|
|
7826
|
+
const verbIcon = crewRowIconSvg(CREW_VERB_ICON[row.verb] || 'info', 'monitor-row-verb-icon');
|
|
7827
|
+
const verbHtml = row.verb
|
|
7828
|
+
? `<span class="monitor-row-verb v-${escapeAttr(row.verb)}" title="${escapeAttr(row.verb)}">${verbIcon}<em>${escapeHtml(row.verb)}</em></span>`
|
|
7829
|
+
: '';
|
|
7830
|
+
// Ref chips: fixed content, dropped whole by the @container
|
|
7831
|
+
// ladder. Never ellipsised — half a SHA is worse than no SHA.
|
|
7832
|
+
const refsHtml = [
|
|
7833
|
+
row.pr ? `<span class="monitor-row-ref ref-pr">${escapeHtml(row.pr)}</span>` : '',
|
|
7834
|
+
row.sha ? `<span class="monitor-row-ref ref-sha">${escapeHtml(row.sha)}</span>` : '',
|
|
7835
|
+
row.worker ? `<span class="monitor-row-ref ref-worker" title="worker id">${escapeHtml(row.worker)}</span>` : '',
|
|
7836
|
+
row.plan
|
|
7837
|
+
? `<span class="monitor-row-plan" title="${escapeAttr(row.plan)}">${crewRowIconSvg('flag', 'monitor-row-plan-flag')}<span class="monitor-row-plan-name">${escapeHtml(row.plan.replace(/\.plan\.md$/i, ''))}</span></span>`
|
|
7838
|
+
: '',
|
|
7839
|
+
].join('');
|
|
7840
|
+
// labelFull (+ sourcePath) must be reachable in every layout:
|
|
7841
|
+
// display `label` is capped at MAX_SEMANTIC_LABEL upstream, so
|
|
7842
|
+
// no layout is a substitute for it. It rides on .monitor-row-
|
|
7843
|
+
// primary rather than the row, because an actionable row's own
|
|
7844
|
+
// title is already spoken for by activityTargetAttributes
|
|
7845
|
+
// (the copy-path/copy-sha affordance) and a second title
|
|
7846
|
+
// attribute on the same element is silently dropped. Primary is
|
|
7847
|
+
// also the only field that ellipsises, so the full text sits on
|
|
7848
|
+
// exactly the element whose text is cut.
|
|
7849
|
+
const feedTitle = (ev.labelFull || ev.label || '') + (ev.sourcePath ? `\n${ev.sourcePath}` : '');
|
|
7850
|
+
const rowTitle = actionAttrs ? '' : ` title="${escapeAttr(feedTitle)}"`;
|
|
7486
7851
|
return `
|
|
7487
|
-
<div class="monitor-row stagger-fade stagger-${staggerIdx}"${rowA11y}>
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
<span class="
|
|
7852
|
+
<div class="monitor-row stagger-fade stagger-${staggerIdx}"${rowA11y}${rowTitle}>
|
|
7853
|
+
${badgeHtml}
|
|
7854
|
+
${actorHtml}
|
|
7855
|
+
${verbHtml}
|
|
7856
|
+
<span class="monitor-row-primary" title="${escapeAttr(feedTitle)}">${escapeHtml(row.primary)}</span>
|
|
7857
|
+
<span class="monitor-row-meta">${refsHtml}</span>
|
|
7858
|
+
<span class="monitor-row-time">${crewEventTime(ev, info)}</span>
|
|
7492
7859
|
</div>`;
|
|
7493
7860
|
}).join('')}
|
|
7494
7861
|
</div>
|
|
@@ -7698,7 +8065,7 @@ function renderUnsafe() {
|
|
|
7698
8065
|
: `${fixBtn}${autofixBtn}`;
|
|
7699
8066
|
return `
|
|
7700
8067
|
<div class="health-card" role="listitem" id="health-card-${escapeAttr(id)}" data-check-id="${escapeAttr(id)}" data-severity="${escapeAttr(sev)}">
|
|
7701
|
-
<button type="button" class="health-item" aria-expanded="false" aria-controls="health-detail-${escapeAttr(id)}" aria-label="${escapeAttr(c.label)}: ${escapeAttr(healthSeverityLabel(sev))}" onclick="toggleHealthCheck('${escapeJsString(id)}')"
|
|
8068
|
+
<button type="button" class="health-item" aria-expanded="false" aria-controls="health-detail-${escapeAttr(id)}" aria-label="${escapeAttr(c.label)}: ${escapeAttr(healthSeverityLabel(sev))}" onclick="toggleHealthCheck('${escapeJsString(id)}')">
|
|
7702
8069
|
<span class="dot ${healthDotClass(sev)}"></span>
|
|
7703
8070
|
<span class="health-item-label">${escapeHtml(c.label)}</span>
|
|
7704
8071
|
<span class="health-item-sev" data-sev="${escapeAttr(sev)}">${escapeHtml(healthSeverityLabel(sev))}</span>
|