Compare commits

..

No commits in common. "92becdd95154cafed3d3c9bee73c7fc88c16654a" and "88bc07fbbe0f73295ac4feefd7b4b6d20137c1b5" have entirely different histories.

3 changed files with 131 additions and 347 deletions

View file

@ -12,10 +12,7 @@
surface their counts as the only chrome they get. */ surface their counts as the only chrome they get. */
:root { :root {
/* Bumped to 6em (#394) so the agent icon can be a full-height --agent-header-h: 4.6em;
square identity anchor without crowding the two-row main column
(title + nav-links on top, state strip below). */
--agent-header-h: 6em;
--agent-composer-h: 3.6em; --agent-composer-h: 3.6em;
--agent-frost-bg: rgba(30, 30, 46, 0.72); --agent-frost-bg: rgba(30, 30, 46, 0.72);
--agent-frost-blur: blur(12px) saturate(140%); --agent-frost-blur: blur(12px) saturate(140%);
@ -73,41 +70,31 @@ body.agent-shell {
z-index: 30; z-index: 30;
min-height: var(--agent-header-h); min-height: var(--agent-header-h);
display: flex; display: flex;
/* align-items: stretch lets the icon take the full header height align-items: center;
(it sizes itself via aspect-ratio off the stretched height). The gap: 1em;
main column + pills column self-centre via inner layout. */ padding: 0.55em 1em;
align-items: stretch;
gap: 0.9em;
padding: 0.5em 1em;
background: var(--agent-frost-bg); background: var(--agent-frost-bg);
-webkit-backdrop-filter: var(--agent-frost-blur); -webkit-backdrop-filter: var(--agent-frost-blur);
backdrop-filter: var(--agent-frost-blur); backdrop-filter: var(--agent-frost-blur);
border-bottom: 1px solid var(--purple-dim); border-bottom: 1px solid var(--purple-dim);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
flex-wrap: wrap;
} }
/* Main column: title row on top, state strip below (#394). Centred .agent-header-title {
vertically against the full-height icon on the left. */
.agent-header-main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; gap: 0.1em;
gap: 0.45em;
min-width: 0; min-width: 0;
flex: 1 1 auto; flex: 1 1 auto;
} }
.agent-header-row { .agent-header-title h2 {
display: flex;
align-items: center;
gap: 0.8em;
flex-wrap: wrap;
min-width: 0;
}
.agent-header-title-row h2 {
margin: 0; margin: 0;
line-height: 1; line-height: 1;
} }
.agent-nav { .agent-nav {
/* Slim nav row directly under the title keeps the operator's
fingers near stats/screen/forge without dominating the header. */
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.4em 0.8em; gap: 0.4em 0.8em;
@ -116,21 +103,10 @@ body.agent-shell {
.agent-state-row { .agent-state-row {
margin: 0; margin: 0;
gap: 0.5em;
}
/* Right cluster flyout pills stacked / inline with the overflow
trigger. Vertically centred against the full-height icon, no
wrap; pills can drop to a row of their own under crowding via
the flex-wrap of `.agent-header-pills` itself. */
.agent-header-pills {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5em; gap: 0.5em;
flex-shrink: 0;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end;
align-self: center;
} }
h2, h3 { h2, h3 {
@ -140,126 +116,11 @@ h2, h3 {
text-shadow: 0 0 8px rgba(203, 166, 247, 0.4); text-shadow: 0 0 8px rgba(203, 166, 247, 0.4);
} }
.agent-icon { .agent-icon {
/* Full-height square identity anchor (#394 mara's spec). The width: 44px;
`align-items: stretch` on .agent-header stretches the icon's height: 44px;
`<img>` box; `aspect-ratio: 1` keeps it square; `height: 100%`
makes it follow the header's actual height through resizes /
wrap. width:auto + aspect-ratio derives the width from height. */
height: 100%;
width: auto;
aspect-ratio: 1;
flex-shrink: 0;
border-radius: 8px;
box-shadow: 0 0 18px -2px rgba(203, 166, 247, 0.4);
object-fit: cover;
}
/* Meta-nav links (stats / screen / forge / dashboard / extras)
no underline (#394 mara's spec); hover lights with cyan glow +
subtle background tint. Reads as a row of soft tabs rather than
default-styled inline anchors. */
.agent-nav-link {
color: var(--cyan);
text-decoration: none;
font-size: 0.85em;
letter-spacing: 0.04em;
padding: 0.1em 0.35em;
border-radius: 3px;
text-shadow: 0 0 4px rgba(137, 220, 235, 0.4);
transition: color 0.15s ease, text-shadow 0.15s ease, background 0.15s ease;
}
.agent-nav-link:hover {
color: var(--fg);
background: rgba(137, 220, 235, 0.08);
text-shadow: 0 0 10px rgba(137, 220, 235, 0.85);
}
/* Overflow menu trigger `` round button on the right of the
pills row. Quiet by default, lights on hover / open (#394). */
.overflow-btn {
background: transparent;
border: 1px solid var(--purple-dim);
color: var(--muted);
border-radius: 999px;
width: 2em;
height: 1.8em;
font-size: 1em;
line-height: 1;
cursor: pointer;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
transition: color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.overflow-btn:hover,
.overflow-btn[aria-expanded="true"] {
color: var(--purple);
border-color: var(--purple);
box-shadow: 0 0 10px -2px var(--purple);
}
/* Overflow popover rebuild + new-session (and the dashboard
back-link, prepended in app.js setHeader). Positioned in JS so
the menu's top-right corner anchors under the trigger button. */
.overflow-menu {
position: fixed;
background: var(--agent-frost-bg);
-webkit-backdrop-filter: var(--agent-frost-blur);
backdrop-filter: var(--agent-frost-blur);
border: 1px solid var(--purple-dim);
border-radius: 6px; border-radius: 6px;
padding: 0.35em;
display: flex;
flex-direction: column;
gap: 0.15em;
z-index: 40;
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45);
min-width: 14em;
}
.overflow-item {
background: transparent;
border: 1px solid transparent;
color: var(--fg);
font-family: inherit;
font-size: 0.9em;
text-align: left;
padding: 0.4em 0.7em;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.6em;
letter-spacing: 0.06em;
text-decoration: none;
text-shadow: 0 0 4px currentColor;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.overflow-item:hover {
background: rgba(203, 166, 247, 0.08);
border-color: var(--purple-dim);
}
.overflow-item-icon {
font-size: 1.05em;
width: 1.4em;
text-align: center;
flex-shrink: 0; flex-shrink: 0;
} box-shadow: 0 0 14px -2px rgba(203, 166, 247, 0.35);
.overflow-item-rebuild { color: var(--amber); }
.overflow-item-new-session { color: var(--amber); }
.overflow-item-rebuild:hover,
.overflow-item-new-session:hover {
background: rgba(250, 179, 135, 0.1);
border-color: var(--amber);
}
.overflow-item-dashboard { color: var(--cyan); }
.overflow-item-dashboard:hover {
background: rgba(137, 220, 235, 0.1);
border-color: var(--cyan);
}
.overflow-item:disabled {
opacity: 0.4;
cursor: progress;
} }
/* Header pill — inbox / loose-ends triggers. Compact, count-prominent. */ /* Header pill — inbox / loose-ends triggers. Compact, count-prominent. */
@ -388,14 +249,20 @@ a:hover { color: var(--fg); text-shadow: 0 0 12px rgba(137, 220, 235, 0.9); }
} }
.btn-login { color: var(--amber); border-color: var(--amber); } .btn-login { color: var(--amber); border-color: var(--amber); }
.btn-cancel { color: var(--red); border-color: var(--red); font-size: 0.85em; padding: 0.15em 0.6em; } .btn-cancel { color: var(--red); border-color: var(--red); font-size: 0.85em; padding: 0.15em 0.6em; }
/* `.btn-rebuild` was the per-agent header chip moved into the .btn-rebuild {
overflow menu in #394 (`.overflow-item-rebuild` covers it now). color: var(--amber);
The dashboard has its own `.btn-rebuild` rule for the per-row border: 1px solid var(--amber);
R3BU1LD form on the SW4RM tab; this one was specific to the padding: 0.15em 0.6em;
per-agent header. font-size: 0.55em;
`.btn-send` was a green send-button variant orphaned since font-family: inherit;
the dashboard's compose form was retired; no live consumer left text-decoration: none;
in either the agent or dashboard tree. */ letter-spacing: 0.1em;
margin-left: 0.6em;
vertical-align: middle;
cursor: pointer;
}
.btn-rebuild:hover { background: rgba(250, 179, 135, 0.1); }
.btn-send { color: var(--green); border-color: var(--green); }
.sendform { display: flex; gap: 0.6em; margin-top: 0.5em; } .sendform { display: flex; gap: 0.6em; margin-top: 0.5em; }
.sendform input { .sendform input {
font-family: inherit; font-size: 1em; font-family: inherit; font-size: 1em;
@ -425,6 +292,12 @@ pre.diff {
word-break: break-all; word-break: break-all;
max-height: 30em; max-height: 30em;
} }
#state-row {
margin: 0.4em 0 0.2em;
display: flex;
align-items: center;
gap: 0.6em;
}
/* Per-agent inbox section collapsible, dim, lives between the /* Per-agent inbox section collapsible, dim, lives between the
state row and the terminal so the operator can peek at what state row and the terminal so the operator can peek at what
landed without scrolling through the live tail. */ landed without scrolling through the live tail. */
@ -563,8 +436,21 @@ pre.diff {
text-shadow: 0 0 6px rgba(243, 139, 168, 0.55); } text-shadow: 0 0 6px rgba(243, 139, 168, 0.55); }
.status-badge.status-needs-login { color: var(--amber); border-color: var(--amber); } .status-badge.status-needs-login { color: var(--amber); border-color: var(--amber); }
.status-badge.status-offline { color: var(--muted); border-color: var(--muted); } .status-badge.status-offline { color: var(--muted); border-color: var(--muted); }
/* Orphaned in #394 `.btn-dashlink` chip beside the title moved .btn-dashlink {
into the overflow menu (`.overflow-item-dashboard` covers it). */ color: var(--cyan);
border: 1px solid var(--cyan);
padding: 0.15em 0.6em;
font-size: 0.55em;
font-family: inherit;
text-decoration: none;
letter-spacing: 0.1em;
margin-left: 0.6em;
vertical-align: middle;
}
.btn-dashlink:hover {
background: rgba(137, 220, 235, 0.1);
box-shadow: 0 0 10px -2px currentColor;
}
.btn-cancel-turn { .btn-cancel-turn {
font-family: inherit; font-family: inherit;
font-size: 0.8em; font-size: 0.8em;
@ -582,10 +468,27 @@ pre.diff {
background: rgba(243, 139, 168, 0.1); background: rgba(243, 139, 168, 0.1);
box-shadow: 0 0 10px -2px currentColor; box-shadow: 0 0 10px -2px currentColor;
} }
/* Orphaned in #394 `.btn-new-session` round-pill moved into the .btn-new-session {
overflow menu (`.overflow-item-new-session` covers it; the font-family: inherit;
`:disabled` opacity treatment lives on the shared font-size: 0.8em;
`.overflow-item:disabled` rule). */ letter-spacing: 0.08em;
background: transparent;
color: var(--amber);
border: 1px solid var(--amber);
border-radius: 999px;
padding: 0.2em 0.8em;
cursor: pointer;
text-shadow: 0 0 4px currentColor;
transition: box-shadow 0.15s ease, background 0.15s ease;
}
.btn-new-session:hover {
background: rgba(250, 179, 135, 0.1);
box-shadow: 0 0 10px -2px currentColor;
}
.btn-new-session:disabled {
opacity: 0.4;
cursor: progress;
}
.state-badge { .state-badge {
display: inline-block; display: inline-block;
padding: 0.25em 0.8em; padding: 0.25em 0.8em;

View file

@ -141,139 +141,34 @@ window.marked = marked;
// ─── state rendering ──────────────────────────────────────────────────── // ─── state rendering ────────────────────────────────────────────────────
function setHeader(label, dashboardPort) { function setHeader(label, dashboardPort) {
const title = $('title'); const title = $('title');
// Title is now just the glowing identity glyph — DASHB04RD, title.textContent = `${label}`;
// R3BU1LD, NEW SESSION all live in the overflow `⋯` menu now // ↑ DASHB04RD — back-link to the host dashboard. Opens in a new
// (#394). Glow + uppercase styling from h2 / .agent-header-title-row. // tab to keep the agent page anchored where the operator is.
title.textContent = `${label}`;
document.title = `${label} // hyperhive`;
const dashUrl = `${location.protocol}//${location.hostname}:${dashboardPort}/`; const dashUrl = `${location.protocol}//${location.hostname}:${dashboardPort}/`;
dashboardBase = dashUrl; dashboardBase = dashUrl;
populateOverflowMenu(label, dashUrl); title.append(
} el('a', {
href: dashUrl, target: '_blank', rel: 'noopener',
// Overflow popover: dashboard back-link + rebuild + new-session. class: 'btn-dashlink', title: 'host dashboard',
// Per #394 mara's spec — rebuild + new-session both moved off the }, '↑ DASHB04RD'),
// header strip into the `⋯` menu (rare actions, both destructive ' ',
// enough to warrant one extra click; the operator rebuilds from
// the host dashboard normally). Dashboard link also slotted in so
// every "leave this page" action lives in one menu.
let overflowMenuPopulated = false;
function populateOverflowMenu(label, dashUrl) {
const menu = $('overflow-menu');
if (!menu) return;
menu.replaceChildren();
// ↑ dashboard — host dashboard back-link (was `.btn-dashlink`
// beside the title pre-#394).
menu.append(el('a', {
class: 'overflow-item overflow-item-dashboard',
href: dashUrl,
target: '_blank',
rel: 'noopener',
role: 'menuitem',
},
el('span', { class: 'overflow-item-icon', 'aria-hidden': 'true' }, '↑'),
'dashboard',
));
// ↻ rebuild — POST `{dash}/rebuild/{label}` after a confirm.
// Same shape as the old `.btn-rebuild` handler.
const rebuildBtn = el('button', {
type: 'button',
class: 'overflow-item overflow-item-rebuild',
role: 'menuitem',
id: 'rebuild-btn',
},
el('span', { class: 'overflow-item-icon', 'aria-hidden': 'true' }, '↻'),
'rebuild container',
); );
rebuildBtn.addEventListener('click', () => { const btn = el('a', {
if (!window.confirm(`rebuild ${label}? container will hot-reload.`)) return; href: '#', class: 'btn-rebuild', id: 'rebuild-btn',
closeOverflowMenu(); }, '↻ R3BU1LD');
btn.addEventListener('click', (e) => {
e.preventDefault();
if (!confirm(`rebuild ${label}? container will hot-reload.`)) return;
const f = document.createElement('form'); const f = document.createElement('form');
f.method = 'POST'; f.method = 'POST';
f.action = `${dashUrl}rebuild/${label}`; f.action = `${dashUrl}rebuild/${label}`;
document.body.appendChild(f); document.body.appendChild(f);
f.submit(); f.submit();
}); });
menu.append(rebuildBtn); title.append(btn);
document.title = `${label} // hyperhive`;
// ↻ new session — arms a one-shot for the next turn. Mildly
// destructive (drops --continue context) so we confirm.
const newSessBtn = el('button', {
type: 'button',
class: 'overflow-item overflow-item-new-session',
role: 'menuitem',
id: 'new-session-btn',
title: 'next turn runs without --continue, starting a fresh claude session',
},
el('span', { class: 'overflow-item-icon', 'aria-hidden': 'true' }, '↻'),
'new claude session',
);
newSessBtn.addEventListener('click', () => {
if (!window.confirm('arm a fresh claude session for the next turn? all prior --continue context will be dropped.')) return;
newSessBtn.disabled = true;
closeOverflowMenu();
postNewSession().finally(() => { newSessBtn.disabled = false; });
});
menu.append(newSessBtn);
overflowMenuPopulated = true;
} }
function openOverflowMenu() {
const btn = $('overflow-btn');
const menu = $('overflow-menu');
if (!btn || !menu || !overflowMenuPopulated) return;
// Position the menu so its top-right corner anchors just below
// the trigger button's bottom-right edge. Using fixed positioning
// + getBoundingClientRect so we don't get trapped in any of the
// header's stacking contexts.
const r = btn.getBoundingClientRect();
menu.style.top = `${Math.round(r.bottom + 6)}px`;
// Render off-screen first to measure, then anchor the right edge.
menu.hidden = false;
const mr = menu.getBoundingClientRect();
menu.style.left = `${Math.round(r.right - mr.width)}px`;
btn.setAttribute('aria-expanded', 'true');
}
function closeOverflowMenu() {
const btn = $('overflow-btn');
const menu = $('overflow-menu');
if (!btn || !menu) return;
menu.hidden = true;
btn.setAttribute('aria-expanded', 'false');
}
function toggleOverflowMenu() {
const menu = $('overflow-menu');
if (!menu) return;
if (menu.hidden) openOverflowMenu();
else closeOverflowMenu();
}
// Wire once on boot. The trigger itself + click-outside + Escape
// dismissal pattern matches the side-panel flyout (Panel).
(function bindOverflowMenu() {
const btn = $('overflow-btn');
if (!btn) return;
btn.addEventListener('click', (e) => {
e.stopPropagation();
toggleOverflowMenu();
});
document.addEventListener('click', (e) => {
const menu = $('overflow-menu');
if (!menu || menu.hidden) return;
if (menu.contains(e.target) || btn.contains(e.target)) return;
closeOverflowMenu();
});
document.addEventListener('keydown', (e) => {
const menu = $('overflow-menu');
if (e.key === 'Escape' && menu && !menu.hidden) {
closeOverflowMenu();
btn.focus();
}
});
})();
function renderOnline(_label, _root) { function renderOnline(_label, _root) {
// Online state is conveyed by the `#alive-badge` chip in the // Online state is conveyed by the `#alive-badge` chip in the
// state row — no longer a separate paragraph in the status // state row — no longer a separate paragraph in the status
@ -834,9 +729,18 @@ window.marked = marked;
}); });
})(); })();
// (#394) — `↻ new session` button moved into the overflow `⋯` // Wire the new-session button (always visible; arms a one-shot for
// menu and wired by `populateOverflowMenu()` above. Previously // the next turn). Mildly destructive (drops --continue context) so
// wired here as a static `#new-session-btn` in index.html. // we confirm before posting.
(() => {
const btn = $('new-session-btn');
if (!btn) return;
btn.addEventListener('click', () => {
if (!window.confirm('arm a fresh claude session for the next turn? all prior --continue context will be dropped.')) return;
btn.disabled = true;
postNewSession().finally(() => { btn.disabled = false; });
});
})();
// Track banner activity by reference-counting in-flight turns. A turn // Track banner activity by reference-counting in-flight turns. A turn
// can begin while the previous turn_end is still in the pipeline (rare // can begin while the previous turn_end is still in the pipeline (rare
@ -884,9 +788,7 @@ window.marked = marked;
rel: 'noopener', rel: 'noopener',
title: lnk.label || '', title: lnk.label || '',
}); });
// Layout gap comes from `.agent-nav { gap }` (#394) — drop if (i > 0) a.style.marginLeft = '1em';
// the legacy per-link inline `marginLeft`. The trailing `→`
// is the "leaves this page" affordance.
a.append(((lnk.icon || '') + ' ' + (lnk.label || '')).trim() + ' →'); a.append(((lnk.icon || '') + ' ' + (lnk.label || '')).trim() + ' →');
metaLinks.append(a); metaLinks.append(a);
}); });

View file

@ -8,66 +8,45 @@
</head> </head>
<body class="agent-shell"> <body class="agent-shell">
<!-- Fixed-overlay header (#394 redesign): two-row layout in the <!-- Fixed-overlay header. Frosted glass over the terminal —
main column — row 1 carries the title + meta-nav, row 2 carries backdrop-filter blur shows the scrolled terminal text behind. -->
the live state strip. The agent icon eats the full header
height on the left as the identity anchor; flyout pills + an
overflow menu trigger sit on the right. Frosted glass over the
terminal — backdrop-filter blur shows the scrolled terminal
text behind. -->
<header class="agent-header" id="agent-header"> <header class="agent-header" id="agent-header">
<img class="agent-icon" src="/icon" alt=""> <img class="agent-icon" src="/icon" alt="">
<div class="agent-header-title">
<div class="agent-header-main"> <h2 id="title">◆ … ◆</h2>
<div class="agent-header-row agent-header-title-row"> <nav class="meta agent-nav" id="meta-links"></nav>
<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>
<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="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> </div>
<!-- Right cluster: flyout triggers + overflow menu. Pills stay <div id="state-row" class="agent-state-row">
hidden until their list is non-empty; the overflow `⋯` is <span id="alive-badge" class="status-badge status-loading" title="harness reachability"></span>
always visible (rebuild + new-session live inside it per <span id="state-badge" class="state-badge state-loading">… booting</span>
#394 — both rare, both destructive, both deserve one extra <span id="model-chip" class="model-chip" hidden></span>
click). --> <span id="ctx-badge" class="ctx-badge" hidden title="tokens used in the current context window"></span>
<div class="agent-header-pills"> <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>
<button type="button" id="inbox-pill" class="header-pill header-pill-inbox" hidden <span id="last-turn" class="last-turn" hidden></span>
title="open inbox flyout"> <button type="button" id="cancel-btn" class="btn-cancel-turn" hidden>■ cancel turn</button>
<span class="header-pill-icon" aria-hidden="true">📬</span> <button type="button" id="new-session-btn" class="btn-new-session"
<span class="header-pill-label">inbox</span> title="next turn runs without --continue, starting a fresh claude session">↻ new session</button>
<span class="header-pill-count" id="inbox-count">0</span>
</button>
<button type="button" id="loose-ends-pill" class="header-pill header-pill-loose" hidden
title="open loose-ends flyout">
<span class="header-pill-icon" aria-hidden="true">🪢</span>
<span class="header-pill-label">loose ends</span>
<span class="header-pill-count" id="loose-ends-count">0</span>
</button>
<button type="button" id="overflow-btn" class="overflow-btn"
aria-haspopup="menu" aria-expanded="false"
title="more actions">⋯</button>
</div> </div>
<!-- Flyout triggers. The inbox + loose-ends lists live in the
side panel now; these pills surface the count and act as the
single click target. The pill stays hidden until there's at
least one item to show. -->
<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="loose-ends-pill" class="header-pill header-pill-loose" hidden
title="open loose-ends flyout">
<span class="header-pill-icon" aria-hidden="true">🪢</span>
<span class="header-pill-label">loose ends</span>
<span class="header-pill-count" id="loose-ends-count">0</span>
</button>
</header> </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 <!-- Main content area. The terminal fills it edge-to-edge and
scrolls behind the floating header + composer. The `#status` scrolls behind the floating header + composer. The `#status`
overlay renders only when login is required (transient first- overlay renders only when login is required (transient first-