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 337 additions and 82 deletions
|
|
@ -12,7 +12,10 @@
|
||||||
surface their counts as the only chrome they get. */
|
surface their counts as the only chrome they get. */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--agent-header-h: 4.6em;
|
/* Bumped to 6em (#394) so the agent icon can be a full-height
|
||||||
|
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%);
|
||||||
|
|
@ -70,31 +73,41 @@ 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: center;
|
/* align-items: stretch lets the icon take the full header height
|
||||||
gap: 1em;
|
(it sizes itself via aspect-ratio off the stretched height). The
|
||||||
padding: 0.55em 1em;
|
main column + pills column self-centre via inner layout. */
|
||||||
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-header-title {
|
/* Main column: title row on top, state strip below (#394). Centred
|
||||||
|
vertically against the full-height icon on the left. */
|
||||||
|
.agent-header-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.1em;
|
justify-content: center;
|
||||||
|
gap: 0.45em;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
.agent-header-title h2 {
|
.agent-header-row {
|
||||||
|
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;
|
||||||
|
|
@ -103,10 +116,21 @@ 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 {
|
||||||
|
|
@ -116,11 +140,126 @@ 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 {
|
||||||
width: 44px;
|
/* Full-height square identity anchor (#394 — mara's spec). The
|
||||||
height: 44px;
|
`align-items: stretch` on .agent-header stretches the icon's
|
||||||
border-radius: 6px;
|
`<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;
|
flex-shrink: 0;
|
||||||
box-shadow: 0 0 14px -2px rgba(203, 166, 247, 0.35);
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
.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. */
|
||||||
|
|
@ -292,12 +431,9 @@ pre.diff {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
max-height: 30em;
|
max-height: 30em;
|
||||||
}
|
}
|
||||||
#state-row {
|
/* `#state-row` layout is provided by `.agent-state-row` /
|
||||||
margin: 0.4em 0 0.2em;
|
`.agent-header-row` above (#394) — kept as a no-op selector
|
||||||
display: flex;
|
anchor in case any future rule needs to scope by id. */
|
||||||
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. */
|
||||||
|
|
|
||||||
|
|
@ -141,34 +141,139 @@ window.marked = marked;
|
||||||
// ─── state rendering ────────────────────────────────────────────────────
|
// ─── state rendering ────────────────────────────────────────────────────
|
||||||
function setHeader(label, dashboardPort) {
|
function setHeader(label, dashboardPort) {
|
||||||
const title = $('title');
|
const title = $('title');
|
||||||
title.textContent = `◆ ${label} ◆ `;
|
// Title is now just the glowing identity glyph — DASHB04RD,
|
||||||
// ↑ DASHB04RD — back-link to the host dashboard. Opens in a new
|
// R3BU1LD, NEW SESSION all live in the overflow `⋯` menu now
|
||||||
// tab to keep the agent page anchored where the operator is.
|
// (#394). Glow + uppercase styling from h2 / .agent-header-title-row.
|
||||||
|
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;
|
||||||
title.append(
|
populateOverflowMenu(label, dashUrl);
|
||||||
el('a', {
|
}
|
||||||
href: dashUrl, target: '_blank', rel: 'noopener',
|
|
||||||
class: 'btn-dashlink', title: 'host dashboard',
|
// Overflow popover: dashboard back-link + rebuild + new-session.
|
||||||
}, '↑ DASHB04RD'),
|
// 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', {
|
rebuildBtn.addEventListener('click', () => {
|
||||||
href: '#', class: 'btn-rebuild', id: 'rebuild-btn',
|
if (!window.confirm(`rebuild ${label}? container will hot-reload.`)) return;
|
||||||
}, '↻ R3BU1LD');
|
closeOverflowMenu();
|
||||||
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();
|
||||||
});
|
});
|
||||||
title.append(btn);
|
menu.append(rebuildBtn);
|
||||||
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
|
||||||
|
|
@ -729,18 +834,9 @@ window.marked = marked;
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Wire the new-session button (always visible; arms a one-shot for
|
// (#394) — `↻ new session` button moved into the overflow `⋯`
|
||||||
// the next turn). Mildly destructive (drops --continue context) so
|
// menu and wired by `populateOverflowMenu()` above. Previously
|
||||||
// we confirm before posting.
|
// wired here as a static `#new-session-btn` in index.html.
|
||||||
(() => {
|
|
||||||
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
|
||||||
|
|
@ -788,7 +884,9 @@ window.marked = marked;
|
||||||
rel: 'noopener',
|
rel: 'noopener',
|
||||||
title: lnk.label || '',
|
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() + ' →');
|
a.append(((lnk.icon || '') + ' ' + (lnk.label || '')).trim() + ' →');
|
||||||
metaLinks.append(a);
|
metaLinks.append(a);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,45 +8,66 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="agent-shell">
|
<body class="agent-shell">
|
||||||
|
|
||||||
<!-- Fixed-overlay header. Frosted glass over the terminal —
|
<!-- Fixed-overlay header (#394 redesign): two-row layout in the
|
||||||
backdrop-filter blur shows the scrolled terminal text behind. -->
|
main column — row 1 carries the title + meta-nav, row 2 carries
|
||||||
|
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">
|
|
||||||
<h2 id="title">◆ … ◆</h2>
|
<div class="agent-header-main">
|
||||||
<nav class="meta agent-nav" id="meta-links"></nav>
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<div id="state-row" class="agent-state-row">
|
<!-- Right cluster: flyout triggers + overflow menu. Pills stay
|
||||||
<span id="alive-badge" class="status-badge status-loading" title="harness reachability">…</span>
|
hidden until their list is non-empty; the overflow `⋯` is
|
||||||
<span id="state-badge" class="state-badge state-loading">… booting</span>
|
always visible (rebuild + new-session live inside it per
|
||||||
<span id="model-chip" class="model-chip" hidden></span>
|
#394 — both rare, both destructive, both deserve one extra
|
||||||
<span id="ctx-badge" class="ctx-badge" hidden title="tokens used in the current context window"></span>
|
click). -->
|
||||||
<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>
|
<div class="agent-header-pills">
|
||||||
<span id="last-turn" class="last-turn" hidden></span>
|
<button type="button" id="inbox-pill" class="header-pill header-pill-inbox" hidden
|
||||||
<button type="button" id="cancel-btn" class="btn-cancel-turn" hidden>■ cancel turn</button>
|
title="open inbox flyout">
|
||||||
<button type="button" id="new-session-btn" class="btn-new-session"
|
<span class="header-pill-icon" aria-hidden="true">📬</span>
|
||||||
title="next turn runs without --continue, starting a fresh claude session">↻ new session</button>
|
<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>
|
||||||
|
<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-
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue