hyperhive/frontend/packages/agent/src/index.html
iris c5610b075a frontend: unify dashboard + agent side panel into shared hive-side-panel
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).
2026-08-01 00:58:53 +02:00

105 lines
5.1 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>hyperhive agent</title>
<link rel="icon" type="image/svg+xml" href="icon">
<link rel="stylesheet" href="static/colors.css">
<link rel="stylesheet" href="static/theme.css">
<link rel="stylesheet" href="static/agent.css">
</head>
<body class="agent-shell">
<!-- Fixed-overlay header — see docs/web-ui.md::Per-agent page
for the three-column layout (icon · main · pills). All asset
/ API hrefs are relative — see docs/web-ui.md::Per-agent
relative paths for the document-baseURI resolution model. -->
<header class="agent-header" id="agent-header">
<img class="agent-icon" src="icon" alt="">
<div class="agent-header-main">
<div class="agent-header-row agent-header-title-row">
<h2 id="title">◆ … ◆</h2>
<!-- Meta-nav: backend-supplied links (stats / screen / forge /
…) plus a client-injected `↑ dashboard` link prepended in
setHeader so the host dashboard stays one click away
without a separate button styled differently. -->
<nav class="meta agent-nav" id="meta-links"></nav>
</div>
<!-- Hive identity line: "swarm / hive" label. Hidden until JS
populates it from /api/state (hive_name + swarm_name). -->
<div class="agent-header-row agent-hive-row">
<span class="agent-hive-label" hidden></span>
</div>
<div id="state-row" class="agent-state-row agent-header-row">
<span id="alive-badge" class="status-badge status-loading" title="harness reachability"></span>
<span id="state-badge" class="state-badge state-loading">… booting</span>
<span id="model-chip" class="model-chip" hidden></span>
<span id="effort-chip" class="effort-chip" hidden></span>
<span id="ctx-badge" class="ctx-badge" hidden title="tokens used in the current context window"></span>
<span id="cost-badge" class="ctx-badge" hidden title="cumulative tokens billed across the last turn (sum across every inference; tool-heavy turns rebill the cached prompt per call)"></span>
<span id="last-turn" class="last-turn" hidden></span>
<button type="button" id="cancel-btn" class="btn-cancel-turn" hidden>■ cancel turn</button>
</div>
</div>
<!-- Right cluster: flyout triggers + overflow menu. Pills stay
hidden until their list is non-empty; the overflow `⋯` is
always visible. Rebuild + new-session + logout live inside
it — see docs/web-ui.md::Per-agent page (Overflow button)
for the rare-destructive-extra-click rationale. -->
<div class="agent-header-pills">
<button type="button" id="inbox-pill" class="header-pill header-pill-inbox" hidden
title="open inbox flyout">
<span class="header-pill-icon" aria-hidden="true">📬</span>
<span class="header-pill-label">inbox</span>
<span class="header-pill-count" id="inbox-count">0</span>
</button>
<button type="button" id="todos-pill" class="header-pill header-pill-todos" hidden
title="open todos flyout">
<span class="header-pill-icon" aria-hidden="true">📋</span>
<span class="header-pill-label">todos</span>
<span class="header-pill-count" id="todos-count">0</span>
</button>
<button type="button" id="overflow-btn" class="overflow-btn"
aria-haspopup="menu" aria-expanded="false"
title="more actions"></button>
</div>
</header>
<!-- Overflow popover. Sits outside the header so the header's
`overflow: hidden`-adjacent ancestors don't clip it; positioned
in JS relative to the overflow button (top-right anchor). -->
<div id="overflow-menu" class="overflow-menu" role="menu" hidden></div>
<!-- Main content area. The terminal fills it edge-to-edge and
scrolls behind the floating header + composer. The `#status`
overlay renders only when login is required (transient first-
time-setup state); otherwise the terminal owns the screen. -->
<main class="agent-main" id="agent-main">
<div id="status" class="agent-status-overlay"></div>
<div class="terminal-wrap">
<div id="live" class="live terminal"><div class="meta">connecting…</div></div>
</div>
</main>
<!-- Fixed-overlay composer. Same frosted-glass treatment as the
header for symmetric framing. Empty until the harness sets up
the textarea via `renderTermInput`. -->
<footer class="agent-composer" id="agent-composer">
<div id="term-input" class="term-input"></div>
</footer>
<!-- Slide-in side panel (inbox / todos flyouts) is a <hive-side-panel>
element (@hive/shared/side-panel.js) — the Panel singleton in
app.js creates + appends it to <body> lazily on first use, so
nothing needs to be pre-declared here. -->
<!-- Single bundled entry. esbuild folds @hive/shared/terminal.js and
the marked npm package into app.js. -->
<script type="module" src="static/app.js" defer></script>
</body>
</html>