agent: redesign terminal header — full-height icon, two-row main, overflow menu (#394)
Operator brief (#394): the header had thirteen distinct visual elements in one flex row with three different border-radius languages, four colour treatments, three label styles. Mara's direction: - agent icon bigger (full header height) as the identity anchor - title glow stays - nav links lose their default-anchor underline - overflow `⋯` absorbs `↻ R3BU1LD` + `↻ new session` (rare, destructive — both worth one extra click; rebuild is normally done from the dashboard) - accent stacking in the state strip stays — that's the vibe ## Layout shape Three flex columns in `.agent-header`: [icon · full height] [main column · 2 rows] [pills + ⋯] The main column carries row 1 (`◆ AGENT ◆` title + meta-nav) on top and row 2 (alive · state · model · ctx · cost · last-turn · cancel-turn) below. ## Changes ### `index.html` - Wrap title + nav in `.agent-header-row .agent-header-title-row`; wrap state-row siblings in `.agent-header-row .agent-state-row`; both go inside a new `.agent-header-main` column. - Right cluster `.agent-header-pills` contains the inbox + loose pills + a new `<button id="overflow-btn">⋯</button>` trigger. - Drop static `#new-session-btn` from `#state-row` — moved into the overflow menu, populated dynamically. - Add `<div id="overflow-menu" role="menu" hidden>` as a sibling of `<header>` (lives outside the header so its `position: fixed` popover isn't trapped by any header stacking context). ### `agent.css` - `--agent-header-h: 4.6em → 6em` so the icon can be square + full height without crowding the two-row main column. Terminal padding-top + status overlay top + tail-pill all derive from this variable, so they follow automatically. - `.agent-header { align-items: stretch }` lets the icon stretch to full height; `.agent-icon { height: 100%; aspect-ratio: 1 }` sizes it as a square off the stretched height. - `.agent-nav-link` rule added — `text-decoration: none`, cyan + soft glow, hover lights brighter (mara's spec). - `.overflow-btn` (round trigger) + `.overflow-menu` (frosted popover, fixed-position) + `.overflow-item` (rows with an icon column + label, hover ink matches per-action accent — cyan for dashboard, amber for rebuild/new-session). - Remove the old `#state-row` selector (layout now provided by `.agent-state-row` + `.agent-header-row`). ### `app.js` - `setHeader` no longer appends DASHB04RD / R3BU1LD chips into the title — title is just the identity glyph now. Both actions get rendered into the overflow menu by `populateOverflowMenu()`. - `populateOverflowMenu(label, dashUrl)` builds three rows: `↑ dashboard` (anchor), `↻ rebuild container` (button — same POST-form action as before), `↻ new claude session` (button — same `/api/new-session` call as the legacy header button). - Overflow toggle / outside-click / Escape dismissal — same pattern as the side-panel flyout (`Panel`). - Drop the static `new-session-btn` IIFE binder; the dynamically- rendered menu item owns its handler now. - Drop the per-nav-link inline `marginLeft` (layout gap comes from the new `.agent-nav { gap }` rule). ## Validation - `npm run build` clean. - Build deltas: agent.css 21.0kb → 23.6kb (overflow + nav rules + comments), app.js 117.4kb → 118.9kb (menu builder + toggle). - Browser smoke test isn't possible from inside iris's container. Worth eyeballing post-deploy: - Icon fills the full header height as a square - Title glow + uppercase styling preserved - Nav links render without underline; hover lights brighter - `⋯` opens a frosted popover with `↑ dashboard`, `↻ rebuild container`, `↻ new claude session` - Rebuild confirm + POST works the same as the legacy chip - New-session confirm + POST works the same as the legacy button - State strip still wraps when crowded (model/ctx/cost multi-line on narrow viewports) - Cancel-turn button still appears while thinking and clears on turn end - Terminal padding-top adjusts to the new 6em header height (no row hidden under the chrome)
This commit is contained in:
parent
88bc07fbbe
commit
69312b8553
3 changed files with 336 additions and 81 deletions
|
|
@ -141,34 +141,139 @@ window.marked = marked;
|
|||
// ─── state rendering ────────────────────────────────────────────────────
|
||||
function setHeader(label, dashboardPort) {
|
||||
const title = $('title');
|
||||
title.textContent = `◆ ${label} ◆ `;
|
||||
// ↑ DASHB04RD — back-link to the host dashboard. Opens in a new
|
||||
// tab to keep the agent page anchored where the operator is.
|
||||
// Title is now just the glowing identity glyph — DASHB04RD,
|
||||
// R3BU1LD, NEW SESSION all live in the overflow `⋯` menu now
|
||||
// (#394). Glow + uppercase styling from h2 / .agent-header-title-row.
|
||||
title.textContent = `◆ ${label} ◆`;
|
||||
document.title = `${label} // hyperhive`;
|
||||
const dashUrl = `${location.protocol}//${location.hostname}:${dashboardPort}/`;
|
||||
dashboardBase = dashUrl;
|
||||
title.append(
|
||||
el('a', {
|
||||
href: dashUrl, target: '_blank', rel: 'noopener',
|
||||
class: 'btn-dashlink', title: 'host dashboard',
|
||||
}, '↑ DASHB04RD'),
|
||||
' ',
|
||||
populateOverflowMenu(label, dashUrl);
|
||||
}
|
||||
|
||||
// Overflow popover: dashboard back-link + rebuild + new-session.
|
||||
// Per #394 mara's spec — rebuild + new-session both moved off the
|
||||
// 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',
|
||||
);
|
||||
const btn = el('a', {
|
||||
href: '#', class: 'btn-rebuild', id: 'rebuild-btn',
|
||||
}, '↻ R3BU1LD');
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!confirm(`rebuild ${label}? container will hot-reload.`)) return;
|
||||
rebuildBtn.addEventListener('click', () => {
|
||||
if (!window.confirm(`rebuild ${label}? container will hot-reload.`)) return;
|
||||
closeOverflowMenu();
|
||||
const f = document.createElement('form');
|
||||
f.method = 'POST';
|
||||
f.action = `${dashUrl}rebuild/${label}`;
|
||||
document.body.appendChild(f);
|
||||
f.submit();
|
||||
});
|
||||
title.append(btn);
|
||||
document.title = `${label} // hyperhive`;
|
||||
menu.append(rebuildBtn);
|
||||
|
||||
// ↻ 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) {
|
||||
// Online state is conveyed by the `#alive-badge` chip in the
|
||||
// state row — no longer a separate paragraph in the status
|
||||
|
|
@ -729,18 +834,9 @@ window.marked = marked;
|
|||
});
|
||||
})();
|
||||
|
||||
// Wire the new-session button (always visible; arms a one-shot for
|
||||
// the next turn). Mildly destructive (drops --continue context) so
|
||||
// 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; });
|
||||
});
|
||||
})();
|
||||
// (#394) — `↻ new session` button moved into the overflow `⋯`
|
||||
// menu and wired by `populateOverflowMenu()` above. Previously
|
||||
// wired here as a static `#new-session-btn` in index.html.
|
||||
|
||||
// Track banner activity by reference-counting in-flight turns. A turn
|
||||
// can begin while the previous turn_end is still in the pipeline (rare
|
||||
|
|
@ -788,7 +884,9 @@ window.marked = marked;
|
|||
rel: 'noopener',
|
||||
title: lnk.label || '',
|
||||
});
|
||||
if (i > 0) a.style.marginLeft = '1em';
|
||||
// Layout gap comes from `.agent-nav { gap }` (#394) — drop
|
||||
// the legacy per-link inline `marginLeft`. The trailing `→`
|
||||
// is the "leaves this page" affordance.
|
||||
a.append(((lnk.icon || '') + ' ' + (lnk.label || '')).trim() + ' →');
|
||||
metaLinks.append(a);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue