Consolidates three independently-written instances of the same thing:
.cred-warning (credentials.html, static markup), .tombstone-warn
(core.js, JS-built), .port-conflict (swarm.js, JS-built) -- the first
two differed only by an undeliberate 10% vs 8% tint, the strongest
argument this was drift rather than three genuinely different needs.
New autonomous custom element, frontend/packages/shared/src/hive-warn/,
matching the established one-component-one-dir shadow-DOM pattern
(hive-btn, hive-toast, hive-dialog). Purely presentational -- no
lifecycle beyond attaching its shadow root once, no
attributeChangedCallback needed since severity/pulse are read directly
by :host([...]) CSS selectors rather than mirrored onto an inner
element. Content passes through via a single default <slot>, so every
call site keeps writing its existing <strong>/<code> markup unchanged.
API: <hive-warn> (amber, default) / <hive-warn severity="red"> for an
active incident vs a standing caveat, plus an opt-in pulse boolean
modifier (only the port-collision banner wants it -- a banner that's
always present and always pulsing just trains you to stop seeing it).
Tint is a single canonical 8% for both severities now, replacing the
10%/8% split.
Registered once in dashboard's common.js (same side-effect-import
pattern as <hive-side-panel>) so every dashboard page picks it up
without a per-file import, since all three call sites (core.js,
swarm.js, credentials.js) already transitively import it.
Verified: npm run build clean for both dashboard and agent packages,
grepped source for leftover cred-warning/tombstone-warn/port-conflict
references (none), confirmed hive-warn/HiveWarn/hive-warn-pulse present
in every affected dist bundle, confirmed the still-live
questions-pulse keyframe (.questions) untouched.
`build_tombstone_views` folded `transient_snapshot`'s keys into its `live`
set, so an agent with in-flight transient work was treated as not-a-tombstone.
Since #3010 that set is derived from the running job graph, which made a
page about on-disk state a function of the scheduler.
Dropping the filter exposes what was always true underneath: nothing records
a destroy. Every definition-side artifact — state subvolume, proposed +
applied repos, `deployed/0`, meta registration, topology entry — is written by
`Provision` before the container exists and survives `lifecycle::destroy`. So
a mid-spawn agent is byte-identical on disk to a tombstone.
Per mara on #3020: remove the filter, warn on the page, keep the issue open
for the swarm-controller / snapshot-storage rework where the problem shape
changes anyway.
- dashboard/tombstones.rs: drop the param + the chain; document the real
semantics
- core.js: amber caveat banner above the rows; row badge `destroyed` ->
`offline`, which is what an absent container actually proves
- dashboard.css: `.tombstone-warn`, modelled on `.port-conflict` but amber and
without the pulse — a permanent banner that pulses trains you to ignore it
- docs/web-ui/dashboard.md: the pane was described as "destroyed-but-state-kept
agents", now the exact wrong claim
Per mara's explicit steer on hyperhive#2812 ("also replace the build
queue tab with this component" + "graph fetching should live in the
component, not build.js" + "dont replicate the grouping by dag"):
R3BU1LD QU3U3 is now a mounted <hive-jobq-graph endpoint="/api/jobq/graph">
element. builds.js no longer renders the queue itself, does its own
fetch, or hand-rolls a per-root tree/roll-up/cancel-button — all of
buildNodeTree/topoSort/entryFingerprint/renderQueueEntry/
firstFailedNode/rebuildQueueRowCache/QUEUE_STATE_GLYPH/rollupState is
gone.
builds.js's remaining job is listening for the component's
hive-jobq-graph-update event (added to the component in the prior
commit) to keep a flat jobqNodes array in sync, and using that for the
two things the generic view doesn't render: the count-pill and the
live-log panel. On the rebuild_queue_changed SSE tick, calls the
mounted element's .refresh() instead of doing its own fetch — that
event still carries its own queue payload on the wire (tabs.js/SW4RM
still reads it for the badges, untouched), this page just ignores it
now.
Also removed, now genuinely dead: the two elapsed/finished-time
tickers (nothing produces the .rqe-when spans they targeted anymore),
stateSlug and isoToSecs (no callers left), fmtElapsed import (no
callers left).
New @hive/shared/jobq-graph.js export entry in packages/shared's
package.json, alongside the existing hive-tab-strip.js/hive-menu.js/
etc. pattern.
docs/web-ui/dashboard.md's R3BU1LD QU3U3 section rewritten to match:
mounted-component shape, no source/reason/cancel-button/deep-link on
rows (generic component has none), settled entries show their full
step tree (Done nodes aren't filtered off this wire, unlike the old
DagView projection).
Verified against real production data again (this hive's own live
/api/jobq/graph, now settled — no in-flight build at test time) plus
a synthetic running-build case to exercise findLiveBuild's happy path:
correct live-node detection (build_log_id gate), correct in-flight
root count. Confirmed the built dist bundle actually registers
customElements.define("hive-jobq-graph", ...) — the new shared
package export resolves correctly through esbuild.
Branch reused per mara's explicit "dont rework #3000 - continue
working on _this_ pr [#2996], it already has the component that
replaces 90% of build.js" — this ships as part of PR #2996, not a
separate PR.
Fetching lives in the component (mara's steer on hyperhive#2812) — a
host that needs the raw node list for something the generic tree
doesn't show (a count badge, a live-log panel keyed on a specific
node) now listens for this bubbling/composed CustomEvent instead of
doing its own parallel fetch. Fires from both the self-fetch path
(refresh()) and a host-pushed render(nodes) call, so a listener sees
every update regardless of source.
Verified via jsdom: both paths dispatch with the correct nodes in
detail.
Shadow-DOM custom element (attachShadowCss, own <style>, matches the
<hive-dialog>/<hive-toast> pattern) that renders any hive_jobq graph
from the wire shape hive-jobq-wire serves: a tree from parent/child
structure, a state glyph per node, payload.label verbatim, and
payload.data as a generic key/value list. Never branches on what a
label or data key means, per the wire type's own opaque-payload
contract.
Fetch endpoint is a configurable attribute
(<hive-jobq-graph endpoint="/api/jobq/graph">) rather than
hardcoded, and a public render(nodes) method lets a host push
pre-fetched data (e.g. from its own SSE stream) instead. No transport
of its own beyond the initial self-fetch — refresh() is public so the
host decides its own refresh cadence.
Verified against real production data (61-node live rebuild-queue
graph, fetched from this hive's own /api/jobq/graph) via a jsdom
render: correct tree shape, correct state glyphs, correct data-list
presence count, both the self-fetching and render()-pushed paths,
and the empty-graph path.
Not wired into any page yet — the builds.js migration (hyperhive#2812)
follows once the open payload-gap question there is settled.
Adds services.hyperhive.forge.publicUrl (defaults to the gateway vhost
URL when behindGateway=true, null otherwise). HIVE_FORGE_PUBLIC_URL is
now sourced from it instead of hardcoding https://${forge.domain}
whenever behindGateway is on.
The 4 frontend call sites that built a forge link from
state.forge_public_url now hide the link when that's absent, rather
than guessing http://<browser-hostname>:3000 — a guess that's only
correct by accident once the operator isn't on plain localhost. Fixes
the dashboard H0M3 tile, per-agent-row forge links + agent menu, the
approval-queue PR link, and the per-agent page's own meta-nav forge
link (found during this pass, same defect, not in the original
3-site inventory).
Docs + doc-comments updated to match.
Part 1 of the dashboard-event-stream-split epic: the server-side
kinds= allow-list already exists and flow.js already uses it
(hive-c0re/src/dashboard/state_snapshot.rs). tabs.js, builds.js,
core.js, and logs.js were the remaining 4 subscribers still taking
every wire kind unfiltered — pure subscription discipline, no new
endpoint needed, per the investigation on the tracking issue.
Each kinds= list is read directly off that page's own existing
MUTATION_HANDLERS/SSE_HANDLERS dispatch table (tabs.js also needs
sent, checked separately for the operator inbox) — a kind not in a
page's table was already a silent no-op today, so this only removes
wire/parse/dispatch-lookup cost for kinds a page never acted on, zero
behavior change.
Note for reviewers: the SharedWorker (stream-worker.js) multiplexes
by exact URL string, so pages that used to share one unfiltered
upstream connection when open simultaneously (e.g. dashboard.html +
builds.html in two tabs) will now each hold their own filtered
connection instead, since their kinds= differ. Each connection is
still a single cheap SSE stream carrying only what that page acts on
— net win over the shared-but-bloated connection this replaces.
Closes#2965.
hive-c0re auto-generates an OpenAPI 3 spec via utoipa
(hive-c0re/src/dashboard/mod.rs's ApiDoc), served raw at
/api/openapi.json and browsable as a Swagger UI at /api/docs, but
docs/ never mentioned either — genuinely zero hits grepping the whole
docs/ tree. Documented both in docs/web-ui/dashboard.md's Dashboard
endpoints + H0M3 page sections.
Also added the H0M3 hub's API tile mara suggested ("maybe also add
home page app that opens swagger ui") -- a plain static link to
/api/docs, no gating needed since the endpoint always exists
(unlike Forge/Matrix, which are conditionally enabled).
Per mara: 'i wanted you to put this in .../settings.html' — the toggle
belongs with the other operator-local browser preferences, not buried
in each agent's own overflow menu.
Extracted the get/set + localStorage key into @hive/shared/prefs.js so
settings.html (writer) and every per-agent app.js (reader, via
HiveTerminal.create's expandDetails option) agree on the exact same key
without two independently-typed copies that could drift. Removed the
now-unused overflow-menu toggle + its agent.css rules from the agent
page. Docs moved from docs/web-ui/agent.md's overflow-button section to
docs/web-ui/dashboard.md's S3TT1NGS section, next to the existing
browser-notifications preference.
argus caught the settings block appending before the effort picker's
conditional despite the PR description and docs both saying it lands
after — DOM order is visual order here (no CSS order: override), so
actual layout was model -> settings -> effort. Moved the block after
the effort picker's if-block; layout now matches what both already
claimed.
Adds a browser-local (localStorage only, no backend field) toggle in
the per-agent overflow menu's new settings section: whether otherwise-
collapsed <details> rows in the live terminal (long tool-results,
Write/Edit diffs, ...) default open. Message-bearing rows that already
default open (send/ask/answer/recv) are unaffected either way.
The shared terminal factory (frontend/packages/shared/src/terminal/terminal.js)
gains an optional expandDetails option (boolean or zero-arg function),
read live on every details()/detailsDiff() call rather than captured
once, so flipping the toggle mid-session applies to the next rendered
row without a reload. Unused by the dashboard's own terminal pane, so
its default-closed behaviour is unchanged.
Closes#2961.
Every sub-page tabbar (logs.html, credentials.html, core.html,
builds.html) hand-wrote the same <nav class="hive-tabbar"><a
class="hive-tab">...</a></nav> boilerplate and then called
createTabStrip() on it after the fact. Add <hive-tab-strip>, a
markup-owning custom element (same reuse-boundary pattern as
<hive-menu>/<hive-side-panel>) that renders that markup from a
declarative tabs list, then wires the existing createTabStrip()
behaviour over what it just rendered — no behaviour duplication.
Convert all four sites to use it: each page's JS now calls
`.configure({ tabs, defaultId, onShow })` on the tabbar element instead
of `createTabStrip(el, opts)`, and configure() returns the identical
{ show, active } shape so nothing downstream changes. builds.js's
rebuild-queue count pill (builds-tab-count-rebuild) is expressed as a
tab's `badgeId` and renders nested in the same spot.
The dashboard's own tabbar and the two no-pane stats time-range
pickers are a different markup/behaviour shape and are intentionally
left alone.
docs/web-ui/agent.md still described a "loose-ends" and "tasks" flyout
that were superseded by the todos flyout when loose-ends-v2 landed —
GET /api/loose-ends and GET /api/bash-tasks are both gone server-side.
Replaced with an accurate description of the todos flyout (including
the mark-done bulk action from #2919), and noted that the ask->operator
inline-answer binding this doc also describes is currently
non-functional (its data source was the same removed endpoint) —
tracked separately as #2922, not fixed here.
buildLooseEndsList in app.js rendered the old loose-ends flyout and had
zero call sites left; removed it. buildAnswerForm stays — reconcileAskBinds
still calls it, even though that path is currently dead per #2922.
Fixes#2920
The per-agent web UI todos flyout (loose-ends v2) had no mark-done
affordance at all — dismissing a todo was only possible via the
cancel_loose_end MCP tool, one id at a time. Add a checkbox per row,
a select-all/select-none/mark-done bulk row, and a new
POST /api/todos/mark-done handler that loops the existing single-id
MarkTodoDone request over the in-agent socket (no new wire request
type needed — the todos list is small, so N same-host round-trips is
cheap).
Fixes#2917
Per mara's review on #2896: has_log: bool was fully redundant once
build_log_id: Option<i64> existed alongside it (has_log was always
just build_log_id.is_some()). Dropped has_log, threading the single
Option<i64> field through job_queue::mod.rs, the hivectl NodeView
test-helper literal, and the one remaining frontend consumer
(findLiveBuild's live-log-panel gate, which now checks
build_log_id != null instead of the separate bool).
Also fixed a now-stale doc comment on GET /api/build-log/{node_id}
that claimed the dashboard used on-demand node-id fetches "instead
of an inline build_log_id on the wire" -- no longer true after this
PR put one there for the BUILD L0GS deep-link.
cargo build/clippy/test clean across the three touched crates; nix
fmt clean; frontend build verified (0 has_log references, 3
build_log_id references in the built builds.js bundle).
Fixes hyperhive#2895. The rebuild-queue tree's per-node log icon (the
printer-glyph "open" affordance next to each node in the R3BU1LD
QU3U3 tab) linked directly to the raw-text download endpoint
(/api/build-log/<node_id>/raw, which sets Content-Disposition:
attachment server-side) -- surprising, since nothing about that icon
signals "this leaves the app", unlike the other two explicit
"download raw"/"raw" links elsewhere on the page.
Point it at the existing ?id=N#buildlogs deep-link into the BUILD
L0GS tab instead (builds.js's fetchBuild already auto-expands +
scrolls to the matching row there). That deep-link's id is the
build-log history row id -- a different id space than the queue
tree's NodeId, and wasn't exposed to the frontend before (only a
derived has_log bool was). Added NodeView.build_log_id: Option<i64>
to the wire type alongside the existing has_log (kept, since
findLiveBuild's separate live-log-panel gate still needs a plain
bool), threaded through job_queue::mod.rs, updated hivectl's NodeView
test-helper literal.
The raw download is still one click away once on that row's BUILD
L0GS detail (the two already-explicit raw-download links are
untouched). cargo build/clippy/test clean across the three touched
crates (hive-c0re, hive-host-sock, hivectl); nix fmt clean; frontend
build verified (grep for build_log_id in the built builds.js bundle).
Fixes hyperhive#2893: 'Element.attachShadow: Unable to re-attach to
existing ShadowDOM', crashing swarm.js's live-update render path.
A custom element's connectedCallback fires again on a same-document
*move* (insertBefore/append repositioning an already-connected node
runs the removal + insertion steps for its whole subtree), not just
on a fresh mount. swarm.js's row-fingerprint cache reuses + reorders
existing <li> subtrees on live updates -- reordering an unchanged,
cached row moves its already-initialised <hive-agent-menu>/<hive-menu>
without ever really detaching it from the document, so connectedCallback
re-runs full setup on an instance that's already set up. attachShadow()
throws unconditionally if the host already has a shadow root, and
HiveAgentMenu's unconditional child-menu creation would have appended a
second <hive-menu> on top of the first, doubling the dropdown, once the
shadow-attach crash itself was out of the way.
Both connectedCallbacks now bail early if already initialised
(shadowRoot present / _menu already built). Reproduced the crash and
duplicate-menu bug with an unguarded control copy of both files driven
via headless Chromium (simulating the exact row-reorder move), then
confirmed the guarded version throws nothing, keeps the same shadowRoot
object identity across the move, and doesn't duplicate the dropdown.
Same pure structural move as the previous commit, applied to the one
other remaining genuine component in shared/src (a self-contained
widget with its own behaviour + CSS, same class as hive-btn/hive-
dialog/hive-toast/hive-menu/side-panel/tabs) -- not the CSS-foundation
files (colors/theme/base/chrome.css) or the utility modules (forms.js,
dom.js, modal.js, shadow-css.js), which aren't components and don't
fit the one-dir-per-component convention.
External callers resolve terminal.js/terminal.css only through
@hive/shared's exports map, so again the two exports targets are the
only external-facing change. index.js's own internal re-export uses a
relative path within the package, so that needed updating too. Zero
call-site changes outside @hive/shared. Verified the built dashboard
(flow.js/common.css) and agent (app.js/agent.css) bundles still
resolve both files.
Pure structural move, no API or behaviour change: tabs.js/tabs.css
move into shared/src/tabs/, matching the one-dir-per-component
layout the other shared components already use (hive-btn,
hive-dialog, hive-toast, hive-menu, side-panel).
Both files are consumed exclusively through @hive/shared's
package.json exports map (./tabs.js, ./tabs.css), never by a raw
relative path, so updating the two export targets is the only
change needed -- none of the 9 call sites (dashboard tabbar, logs,
core, builds, credentials, stats x2, agent stats) touch anything.
Verified the built dashboard/agent bundles still resolve both
files correctly.
Per mara's review: '2 and maybe 1, but 3 also sounds reasonable on first
glance' (against 3 options I posted). Doing 2 and 1, leaving open()/
openNamed() as-is (option 3, tentative only).
Both dashboard/common.js and agent/app.js now export/use the
<hive-side-panel> element instance directly (sidePanel) instead of a
thin Panel = { open, openNamed, refresh, close } object that existed
purely to keep the old call-site shape unchanged. All 6 real call sites
updated to call the element's own methods directly.
The .side-panel-body class each wrapper stamped onto its own instance,
purely so common.css/agent.css's pre-existing content-styling selectors
kept matching, is gone too -- those selectors now use the element's own
tag name as the root (hive-side-panel .md, hive-side-panel .agent-inbox),
which already uniquely identifies the light-DOM instance without a
compatibility class. Verified via headless Chromium/CDP that the
tag-name selectors resolve correctly with no class needed.
Drive-by: removed an unrelated dead Panel import in call.js.
Per mara's review on the side-panel PR: create the shared <hive-side-panel>
instance once at module-evaluation time instead of lazily on first call
via an ensurePanel() guard every wrapper method had to remember to call.
ES modules execute after the document is parsed (same timing as a defer
script), so document.body is already available when this code runs --
lazy init bought nothing here and left a footgun for any future method
added to either wrapper.
The dashboard's Panel singleton and the per-agent UI's own inline Panel
IIFE each had their own near-identical implementation of the right-side
slide-in drawer used for file previews, diffs, logs, and inbox/todo
lists. Both are now thin wrappers around a new <hive-side-panel>
shadow-DOM custom element in @hive/shared, following the same house
pattern as <hive-menu>: the element owns and builds all its structural
chrome itself (backdrop, drawer, resize handle, header, title, close
button) in connectedCallback, and only the caller's opaque content node
is projected in via a default <slot> so each package's own
content-type-specific CSS keeps reaching it.
Public API is the union of both originals: open(title, content),
openNamed(name, title, content), refresh(name, title, content),
close(), and currentOwner(). Drag-to-resize + localStorage width
persistence (ported verbatim from the dashboard's original
implementation, the only one of the two that had it) is now available
to both consumers by default — a deliberate behavior widening for the
agent UI, which didn't have resize before. Along the way, fixed a
latent bug in the ported CSS: the resize handle was setting a
--side-panel-w custom property that no width rule ever consumed, so
dragging never actually resized the drawer even though it looked wired
up; the new shared stylesheet's width rule reads it properly.
Each package's own global stylesheet keeps its content-specific rules
(common.css's .side-panel-body .md, agent.css's .side-panel-body
.agent-inbox) exactly where they were — those can never be reached from
the shared element's shadow tree, same architectural floor as
<hive-menu>'s item-row styling. Each wrapper applies a plain
'side-panel-body' compatibility class to its own <hive-side-panel>
instance so those existing selectors keep matching by ordinary
light-DOM descendant matching, with the shared element itself having no
knowledge of what that class name means.
Panel.bind() is gone from both packages' public API — the shared
element wires its own listeners in connectedCallback, so there's no
bind step left to call. tabs.js's one call site (the only bind() caller
in either package) was updated to drop it.
The two original chrome CSS blocks disagreed on several purely visual
details beyond the resize-handle rules (z-index, backdrop color, drawer
border/box-shadow, title typography) — the dashboard's values (the more
feature-complete of the two) were kept as canonical, which is a small
visible style change for the agent UI's panel chrome (thinner border,
no box-shadow, no bold purple title). Flagged for visibility since nothing
in the original two implementations called this out explicitly.
Verified with a real headless-Chromium/CDP harness (bundled the actual
component + built page CSS, served statically, drove via raw CDP) for
both usage shapes: open/close, backdrop-click dismiss, Escape dismiss,
refresh() owner-matching (no-op on wrong owner, applies on matching
owner), and drag-to-resize (drawer width updates live during drag and
persists to localStorage on release).
Per mara's review on #2881: the dropdown box chrome (background/border/
radius/shadow/min-width/white-space) and the trigger button's base
icon-button treatment are both reachable from hive-menu.css now --
the box chrome lives on hive-menu's own shadow-owned .menu-dropdown
wrapper (no slotting constraint at all), and the trigger button is
styled via ::slotted([slot='trigger']) since it's the top-level slotted
node for that slot. Item-row styling stays in the caller's stylesheet
-- ::slotted() only reaches directly-slotted elements, not their
descendants, so individual dropdown items are architecturally
unreachable from hive-menu's shadow tree. Verified interactively via
headless Chromium/CDP: trigger opacity/hover/border-radius and the
dropdown wrapper's background/border all resolve correctly, hover and
click-to-open still work.
<hive-agent-menu> bundled two concerns: the agent-specific trigger/item
list, and generic "click a trigger, get a positioned dropdown" mechanics
(shadow attach, open/close, singleton close-on-open coordination,
outside-click/Escape handling). Pulled the latter out into a new
@hive/shared/hive-menu.js (<hive-menu>), following the established
per-component-directory + ._opts-before-append shadow-DOM pattern
(<hive-dialog>). <hive-agent-menu> now just builds the "⋮" trigger and
the action list and hands them to an internal <hive-menu> instance.
<hive-menu> takes ownership of every <hive-menu> instance in the app for
singleton coordination (closeAllMenus, renamed from closeAllAgentMenus)
— a deliberate widening from the old per-agent-menu-only tracking, since
the mechanism was never agent-specific to begin with.
The one subtlety worth spelling out: <hive-menu> projects the caller's
opaque trigger/content nodes via named <slot>s rather than moving them
into its own shadow root. That's load-bearing, not cosmetic — if it
re-parented them into its own shadow tree instead, <hive-agent-menu>'s
own classes (.agent-menu-btn, .agent-menu-item, ...) would stop applying,
since a <style> only styles elements within the same shadow tree/document
it's part of, and only slotting (not re-parenting) keeps the caller's
nodes in the caller's own tree for styling purposes. That in turn made
<hive-agent-menu>'s own shadow root redundant once it wasn't the thing
positioning or owning open/close state anymore, so it's dropped in favor
of a plain light-DOM element styled by dashboard.css (already the one
page it renders on) — hive-agent-menu.css is gone, its rules folded into
dashboard.css's per-agent-menu section, minus the positioning rules that
moved into hive-menu.css as the new generic `.menu-dropdown` wrapper.
Verified with a standalone esbuild bundle + a cached nix chromium driven
over raw CDP (no puppeteer/playwright/python3 available): hover-reveal
opacity, dropdown open/close/positioning, outside-click/Escape dismissal,
and cross-instance singleton coordination all behave identically to
before the split.
Moves buildAgentMenu's DOM-building body, the menuItem/menuSep/menuLink
helpers, agentMenuPost, and the open-dropdown coordination logic out of
swarm.js and into a new <hive-agent-menu> autonomous custom element
(dashboard/src/agent-menu/), following the same shadow-DOM + one-dir-per-
component shape as hive-dialog. swarm.js's buildAgentMenu is now a thin
wrapper that constructs the element and sets ._opts before appending it,
same convention hive-dialog uses since a custom element created via
document.createElement can't take constructor args.
The module-level "one dropdown open at a time" singleton (previously a
single mutable variable in swarm.js) becomes a tracked Set of open
instances inside the component module; each instance closes itself via
its own close() method rather than another instance reaching into its
shadow internals. The document-level outside-click and Escape listeners
move into the component module too, keyed off e.composedPath() instead of
e.target.closest() -- shadow-DOM event retargeting means a plain
e.target check no longer reliably reaches into a specific instance's
shadow tree. closeAllAgentMenus() is exported for swarm.js's
buildAgentTree, which still needs to close any open menu before it
replaces the container tree DOM.
The hover-reveal opacity rule crosses the shadow boundary via a
--menu-btn-opacity custom property (custom properties inherit through
shadow boundaries): dashboard.css sets it on hover of the light-DOM
hive-agent-menu element, and the component sets it directly from JS while
its own dropdown is open, since that's component-internal state a CSS
selector out in the light DOM can't see. The host element itself takes on
the structural role (flex:none, position:relative, ...) the old
light-DOM .agent-menu wrapper div played, since its shadow tree's
absolute-positioned dropdown needs a positioned ancestor to anchor off of.
Verified end to end with a standalone esbuild-bundled test harness run
under headless chromium: row layout/flex sizing, hover-reveal opacity,
and dropdown positioning all render correctly, and a scripted interaction
pass (singleton exclusivity, outside-click close, Escape close, toggle
behavior, menu-item click close, and the exported closeAllAgentMenus())
all pass.
Pulls the attachShadow-plus-plain-<style>-tag boilerplate that hive-dialog
and hive-toast already build per component into one shared helper, so the
next shadow-DOM custom element (the agent context menu, next commit) can
reuse it instead of re-deriving the same few lines. A plain <style>
element rather than a constructed CSSStyleSheet with adoptedStyleSheets --
each component instance builds its own fresh stylesheet per connect with
no sharing across instances, so adoptedStyleSheets buys nothing here over
the plain, universally-understood <style> tag.
#2875 (merged) fixed this on main's flat modal.js before #2793's
component-dir split landed. Porting the same one-line fix here now
instead of leaving it as a rebase landmine for whichever PR merges
second.
mara: 'i dont like js css attacher. is there a cleaner way?' — yes: each
component instance was already building its own fresh CSSStyleSheet()
per connect, no sharing across instances, so adoptedStyleSheets bought
nothing here over a plain <style> tag. Same raw-text CSS import, just a
simpler attach step.
Splits the shadow-DOM custom elements out of the flat shared/src layout
into per-component directories:
hive-btn/hive-btn.{js,css}
hive-dialog/hive-dialog.{js,css}
hive-toast/hive-toast.{js,css}
hive-dialog and hive-toast were previously defined inline inside
modal.js alongside the openDialog/themedConfirm/themedPrompt/themedToast
orchestration helpers; modal.js is now a slim entry point that imports
the two component modules for their customElements.define side effect
and keeps only the orchestration functions, which aren't components
themselves. hive-dialog.js now imports hive-btn.js directly (it's the
actual consumer that creates <hive-btn> elements), instead of modal.js
importing it on hive-dialog's behalf.
Pulled the identical shadow-root-plus-adopted-stylesheet boilerplate
(previously duplicated between modal.js's local attachShadow() and
hive-btn.js's inline version) into a shared shadow-css.js helper,
attachShadowCss(host, cssText, shadowInit), used by all three
components. Behaviorally identical — same attachShadow() options per
component, just deduplicated.
No external import paths changed: every consumer only ever imported
the package-level @hive/shared/modal.js entry point, never the
component internals directly, so this is fully internal to the shared
package. Verified with a full frontend build (dashboard + agent
bundles).
hive-dialog's dismiss-on-backdrop-click handler checked e.target === this
(the host). Shadow DOM event retargeting sets e.target to the host for
ANY click that originated inside the shadow tree once it reaches a
listener attached on the host itself, not just clicks that actually hit
the host's own rendering — so the check was true for every click inside
.box that no other element's listener consumed first (title, message, a
bare checkbox row with no button to intercept it), immediately closing
the whole dialog. Reported by mara: clicking a checkbox in a
confirmation dialog (e.g. the restart dialog) dismissed the dialog
instead of toggling the box.
Switched to e.composedPath()[0] === this, the true original target
unaffected by retargeting — true only for a genuine backdrop click.
current_flake_rev canonicalizes to /nix/store/<hash>-<pname>; the
hash right after /nix/store/ is what varies between builds, the
trailing -<pname> is constant. slice(-12) was taking the tail, so
two different builds would very likely render the same truncated
string. slice the hash prefix out instead, with a plain head-slice
fallback for a non-store-path rev (e.g. a bare local dir in dev).
damocles caught this in review on PR #2869.
Adds hyperhive_rev to the dashboard's /api/state StateSnapshot,
resolved via the same current_flake_rev helper get_agent_meta's
per-agent hyperhive_rev already uses. home.js renders it next to the
existing hive-identity line, truncated to the last 12 chars with the
full value in title=, hidden when the flake ref isn't a local path pin.
Requested by annika (infra.run) via dmatrix, hyperhive#2854.
Per review: don't use is=, it reads as a hack (and it is one — is=-upgraded
built-ins can never host a shadow root, which is what caused the crash this PR
fixes in the first place). <hive-btn> is now a normal autonomous custom element
wrapping a real <button> inside its own shadow root, so it gets its shadow
encapsulation back (matching hive-dialog/hive-toast) instead of the document-
level stylesheet workaround from the previous commit.
delegatesFocus: true on the shadow root means .focus() on the host (what
modal.js calls for autofocus) reaches the inner button directly. The inner
button's native click is a composed event, so host-level click listeners
(what modal.js/themedPrompt already use) keep working unchanged.
modal.js: el('button', { is: 'hive-btn', ... }) -> el('hive-btn', { ... }) at
the one call site. dom.js: removed the is= special case from el() entirely —
it existed only to support this one now-gone usage. Build clean.
Element.attachShadow() throws NotSupportedError unconditionally for a
customized built-in (<button is="hive-btn">): the spec only allows
autonomous custom elements or a fixed list of native tags to host a
shadow root, and explicitly excludes any is=-upgraded built-in
regardless of which tag it upgrades. button isn't on that list either
way. This made every themed dialog (any confirm/prompt, since openDialog
always renders at least one button) throw and fail to render in a real
browser, though it passed CI since nothing there exercises actual
browser DOM.
hive-dialog and hive-toast are unaffected — both are genuine autonomous
custom elements (extends HTMLElement, no is= upgrade), which are valid
shadow hosts.
Fix: hive-btn no longer calls attachShadow. Styles adopt onto document
once (module-level guard) instead of per-instance shadow root, scoped
via the [is="hive-btn"] attribute selector instead of :host — same
light-DOM approach the rest of the app's .btn consumers already use.
Native button behaviour is untouched, only the styling mechanism
changed. Build clean.
mara: 'wait the common styles is literally just the button stuff? pls
make a button component now as part of this pr and replace the usage
in the modal. replacing all usages and finally removing the btn styles
from common css is a follow up then.'
<hive-btn> (hive-btn.js) is a customized built-in <button is="hive-btn">
with its own shadow root -- extending HTMLButtonElement keeps every
native button behaviour (click/keyboard activation, :disabled, form
participation) instead of re-implementing it on a generic wrapper.
Shadow root holds only an adopted stylesheet + a <slot>, so the
button's light-DOM content (label, or asyncBtn's swapped-in spinner
span) renders through unchanged -- slotted content stays styled by the
light-DOM cascade, so the global .spinner class still applies.
Variants (cancel/confirm/danger) are a 'variant' attribute, not a CSS
class, since they're a semantic prop of the component.
Customized built-ins aren't supported in Safari/WebKit -- fine here,
the project targets recent Firefox only (same reasoning as the
original custom-elements pilot).
Wired into modal.js: HiveDialog's buttons now render as
<button is="hive-btn" variant="...">, replacing the old
component-common.css .btn copy -- deleted that file + component-
styles.js entirely (their sole purpose was giving dialog buttons a
.btn look, which hive-btn now owns properly). dom.js's el() gained
support so it can create customized built-ins the same way it
creates everything else. hive-dialog.css dropped the now-dead
.cancel/.confirm/.confirm.danger rules.
Per mara's scoping: NOT touching the other .btn consumers across the
app (dashboard/agent submit buttons, form() helper, etc.) or removing
.btn from dashboard/common.css / agent/agent.css in this PR -- that
migration + cleanup is an explicit follow-up.
Verified with a full frontend build (grepped bundled JS for hive-btn/
variant to confirm it inlines); nix fmt clean.
mara: 'i dont like sharing css via js, thats not how it should be done.'
Replaced the DIALOG_CSS/TOAST_CSS template-string constants in modal.js
and the inline CSS text in component-styles.js with three real .css
files (hive-dialog.css, hive-toast.css, component-common.css),
imported as raw text via esbuild's 'text' loader and turned into
CSSStyleSheet objects at runtime (same replaceSync() call as before --
only where the CSS text comes from changed). Both packages' build.mjs
gained a '.css': 'text' loader entry on their JS-bundling step; this
doesn't collide with the separate page-stylesheet bundling ('css'
loader), which is a different esbuild invocation over different entry
points.
No behavior change -- same adoptedStyleSheets wiring, same rules,
same output. Verified with a full frontend build (grepped the bundled
JS to confirm the CSS text inlines correctly); nix fmt clean.
Follow-up to the light-DOM custom-elements pilot (mara: 'that one landed
and works. i dont really like the css still being shared - id like that
to be split by component, with common stuff via @include').
<hive-dialog> and <hive-toast> now attach a shadow root and adopt a
component-scoped CSSStyleSheet built from a template-string constant in
modal.js (DIALOG_CSS / TOAST_CSS), plus a new shared/src/component-
styles.js sheet (currently just .btn) adopted alongside it via
adoptedStyleSheets -- the native equivalent of a Sass @include, no
preprocessor added. Theme vars keep resolving through the shadow
boundary since CSS custom properties inherit across it; only plain
class rules needed the explicit move.
Deleted the global shared/src/modal.css entirely and dropped its
@import from both dashboard/common.css and agent/agent.css -- nothing
outside modal.js renders the old .tc-* classes any more. The <hive-
dialog> element is now the backdrop itself (:host carries the fixed-
position/centering rules that used to be .tc-backdrop on a light-DOM
div); box/title/message/content/actions all render inside its shadow
root. <hive-toast> similarly styles :host directly instead of a light-
DOM div, with the message text placed straight into the shadow root
(no <slot> needed since there's no external light-DOM content to
project).
Public API (openDialog/themedConfirm/themedPrompt/themedToast)
unchanged -- no call-site changes needed anywhere in dashboard/agent.
Verified with a full frontend build; nix fmt clean.
`hive_host_sock::jobs::State` was a hand-maintained copy of
`hive_jobq::State` — five variants spelled the same in both, kept in sync
by whoever remembered. Adding `Skipped` last week meant adding it twice.
The wire crate now re-exports the scheduler's enum and `to_wire_state` is
gone.
Two states that were hidden now reach clients. `to_wire_state` renamed
`Pending` to `Queued` and folded `Finishing` into `Running`, so the
dashboard could not distinguish a node waiting on its dependencies from
one whose own work is done while its sub-nodes still run. Both are now
visible, and consumers say which they mean.
Every consumer had to move with it, and only the Rust ones said so: the
exhaustive matches in `hivectl` and `DagView::rollup_state` failed to
compile, while the dashboard's fourteen string comparisons would have
gone quietly wrong — a `finishing` node no longer counting as running,
a `pending` node no longer as queued.
The frontend also builds CSS class names out of the state string
(`rqe-` + state, `rqe-node-` + state) and keys its glyph map on it, all
lowercase. Those go through a `stateSlug` helper now; comparisons use the
wire spelling, presentation lowercases. Without that split every queue
entry and node chip would have silently lost its styling.
Dropping the `State as JobState` alias in hive-c0re falls out of this:
the alias only existed to tell two `State` types apart, and there is one
now.
Closes hyperhive#2788.
State::Skipped now rides the wire (per hive-host-sock's dag_view, no
longer filtered) — this was the last missing piece: the frontend had
no glyph for it, so a skipped node's per-node chip fell through to
QUEUE_STATE_GLYPH's '?' fallback.
Added a 'skipped' entry (·, same quiet glyph hivectl already uses for
the same state — no contract between them, just consistent taste) and
a .rqe-node-skipped CSS rule (dimmed only, no strikethrough —
deliberately distinct from .rqe-node-cancelled: a skipped node wasn't
dropped mid-flight, it was never going to run, so it should read as
expected/quiet rather than alarming). rollupState's defensive arm
(every(n => skipped || done) => done) already landed in #2799 and
needed no changes here.
buildNodeTree has no state-based filtering, so skipped nodes render
in the tree exactly like any other node kind — no other changes
needed. Verified with a full frontend build; nix fmt clean.
A node ruled out by its own dependency edges settles `Skipped` host-side,
but the wire folded it into `Cancelled` and `dag_view` filtered it out
entirely, so a client never saw which branch a run didn't take. Post-#2785
that is not a rare shape: every approval DAG has two not-taken tails and
every rebuild has one, on the happy path as much as on failure.
`State` gains `Skipped`, and it counts as terminal — the wait loops in
hivectl's progress display and the daemon's dag-settled check decide
"finished" with `all(is_terminal)`, so omitting it would hang them on
essentially every DAG.
`dag_view` now emits skipped nodes but no longer lets them keep a DAG
alive. Serialization and completion were the same expression: a DAG left
the snapshot because its nodes had all been filtered away. Keeping skipped
nodes on the wire under that rule would pin every finished deploy in the
queue view forever, so the completion test is now its own flag.
`rollupState` in the dashboard gains the matching arm. It has no `done`
case — `done` is inferred by falling off the wire — so its trailing
`return 'queued'` catches anything it doesn't recognise, and a green
deploy would have read as permanently queued the moment the backend
started emitting the new state. The Rust and JS roll-ups have silently
disagreed before; they are edited together here and say so.
Pilot for the components-split proposal (mara wants a look at using
custom elements now that we're recent-Firefox-only). Picked the
themed dialog system as the first candidate: most self-contained of
our existing de-facto reusable components (transient, imperative call
sites, no external render-tree coupling), and shared between the
dashboard and per-agent UI already.
<hive-dialog> replaces the manually-built tc-backdrop/tc-box tree in
openDialog — connectedCallback renders, the keydown listener and
click-outside-to-dismiss are owned by the element instead of a
closure, and the outcome is reported via a hive-dialog-close
CustomEvent rather than a hand-rolled resolve callback threaded
through the DOM tree.
<hive-toast> replaces the toast div themedToast built inline —
connectedCallback starts the auto-dismiss timer,
disconnectedCallback clears it (previously a closure-captured
setTimeout handle with no explicit cleanup on early removal).
Both are light DOM (no shadow root) — styling stays exactly where it
already lived, in modal.css's .tc-* classes, imported globally by
both packages' base stylesheets. This was the deliberate call for a
first pilot: shadow DOM would need every shared stylesheet
re-imported per instance (CSS custom properties pierce shadow
boundaries for theming, but plain class rules like .btn don't), which
is real migration cost. Light DOM validates the pattern (lifecycle
encapsulation, less manual event bookkeeping) without paying that
cost; shadow DOM is a drop-in upgrade to these same two classes if a
later pilot wants real style encapsulation.
Public API unchanged (openDialog/themedConfirm/themedPrompt/
themedToast) — every existing call site across dashboard + agent
keeps working with no changes. Verified with a full frontend build.
The dashboard has a themed modal/dialog system (modal.js: themedToast/
themedConfirm/themedPrompt) and a data-async form submit interceptor
(bindAsyncForms) that every dashboard action routes through. The
per-agent UI never adopted either — it had its own more primitive
data-async handler using native window.confirm()/alert() (8 call
sites) and a duplicated el() DOM helper.
- Moved el() out of dashboard/common.js into shared/src/dom.js.
- Moved modal.js + modal.css from dashboard/src/ to shared/src/,
updating its internal el import.
- Moved bindAsyncForms from dashboard/common.js into shared/forms.js,
alongside the asyncBtn primitive it's built on.
- Updated every dashboard file's imports to the new shared locations
(no re-export shims).
- agent.css now @imports shared/modal.css so the dialogs render
themed there too.
- agent/app.js: dropped its local el()/data-async duplicate, wired
bindAsyncForms(), and replaced all 8 window.confirm() sites with
themedConfirm (async, wrapped in a fire-and-forget IIFE where the
call site needs a synchronous boolean return, e.g. the slash-command
dispatcher).
Closes hyperhive#2791. Verified with a full frontend build
(npm run build) — both dashboard and agent bundles compile clean and
agent.css picks up the .tc-* dialog styles it previously lacked.
asyncBtn() (shared/src/forms.js) is used by both the dashboard and the
per-agent UI, but its .spinner class + @keyframes spin animation only
lived in dashboard.css. The agent UI's loading spinner rendered as a
static unstyled glyph instead of the animated amber spinner the
dashboard gets. Moved the rule to shared/base.css, which both
common.css (dashboard) and agent.css already @import.
Closes#2720 (partial — kept state + infra card layout).
core.html doesn't load dashboard.css so the .container-row styles from
the operator SPA weren't available. Add equivalent card rules directly in
core.css: .containers flex column, .container-row with bg-elev background
+ border + border-radius, .tombstone dashed variant, .head flex row with
badge + meta, .actions flex row for the action buttons. Matches the visual
weight of agent cards on the main dashboard.
Closes#2720 (partial — schedules + call history fixes).
schedules:
- Agent-name column headers: switch from -45° CSS transform (which clipped
names mid-glyph) to writing-mode:vertical-rl + rotate(180deg). Names now
read bottom-to-top without truncation in their 28px column.
- Shrink 'next' column from 8em → 5.5em and 'every' from 7em → 5em;
these only hold short duration strings so the wider widths wasted space.
call history:
- Approval history <li> items now get a lightweight card treatment
(bg-elev background, 1px border, 3px left accent) matching the visual
weight of the pending .approval-card items above them.
- Left border colour reflects outcome via :has(.glyph-*): green for
approved, red for denied, amber for failed.
Closes#2720 (partial — stats tab placement + contrast).
The time-window nav was in <main> below the page title, so it controlled
hash routing but wasn't visually part of the page chrome. Move it into
<header> (same row as ← home / ST4TS), fill the remaining header space,
and right-align the buttons — consistent with the /logs.html tabbar pattern.
Also set explicit color/border on inactive buttons so they remain readable
on lower-contrast operator colour schemes (var(--subtext1) fallback to
var(--muted)).