//
— the agent page's identity strip: icon, glyphic title, the // "swarm / hive" label row, and a slot for the status row // (`` — ../status-chips/, kept separate so this component // has no opinion on what badges exist). Three columns: icon, main // (title + hive-label), pills — see the `pills` prop doc below for why // the third column is the one that grows. Reuses `agent.css`'s // existing `.agent-header*`/`.agent-icon` rules (loaded globally by // index.html) rather than a component-scoped stylesheet — no new // visual language needed for the chrome itself. // Measures its own rendered height via ResizeObserver, writes it to // `--agent-header-real-h` (agent.css's downstream consumers — // `.agent-status-overlay`, `.agent-main`'s scroll padding — prefer // this over the static `--agent-header-h` guess). A static guess was // the root cause of two "login card behind the header" reports on // this PR (mara, live): any header content that can wrap an extra // line breaks the guess at whatever width triggers it, and no width // can be promised safe. Deliberately a SEPARATE var, not an overwrite // of `--agent-header-h` in place: `.agent-header`'s own `min-height` // also reads that var — overwriting it in place would make the // header's own size react to its own just-measured height every // tick, a real ResizeObserver feedback loop (confirmed live on the // first pass of this fix). `--agent-header-h` stays the static floor; // `--agent-header-real-h` is output-only. import { useEffect, useRef } from "preact/hooks"; import type { ComponentChildren } from "preact"; // Icon 404s when the agent has no `hyperhive.icon` override (no // bundled server-side default any more — see // `hive_agent::web_ui::screen::serve_icon`). Ports app.js's // `bindHeaderIconFallback` exactly: fire-and-forget load, swap to // `/favicon.svg` on failure, `dataset.fallback` guards against a // 404-on-the-fallback-itself loop. function handleIconError(e: Event) { const img = e.currentTarget as HTMLImageElement; if (img.dataset.fallback) return; img.dataset.fallback = "1"; img.src = "/favicon.svg"; } export interface HeaderProps { label: string; hiveLabel?: string | null; /** The status/badge row (``) — lives in the * `.agent-header-pills` cluster alongside `pills` (see below), not * stacked as its own row under the title. */ children?: ComponentChildren; /** Flyout triggers (inbox/todos/links pills, the overflow menu * button) plus `children` (the status badges), all in * `.agent-header-pills`. `.agent-header-main` only ever holds the * title + hive-label rows, which don't wrap in practice and so stay * a stable height; only this pills-cluster column (already sized for * a variable pill count) grows to fit badges+pills together, keeping * the header a stable height even on widescreen. Meta-nav * (``) lives here too, as a fixed-size trigger, not an * inline link list in the title row (docs/web-ui/agent.md). */ pills?: ComponentChildren; } export function Header({ label, hiveLabel, children, pills }: HeaderProps) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el || typeof ResizeObserver === "undefined") return; // getBoundingClientRect() (not ResizeObserver's own contentRect) — // we want the full visual box padding+border included, the thing // content below the fixed header actually needs to clear, not the // content-box-only measurement ResizeObserver's entry defaults to. const ro = new ResizeObserver(() => { document.documentElement.style.setProperty( "--agent-header-real-h", `${el.getBoundingClientRect().height}px`, ); }); ro.observe(el); return () => ro.disconnect(); }, []); return (

◆ {label} ◆

{hiveLabel ? (
{hiveLabel}
) : null}
{children || pills ? (
{children} {pills}
) : null}
); }