diff --git a/frontend/packages/agent/src/agent.css b/frontend/packages/agent/src/agent.css index 4328ac1..72318bb 100644 --- a/frontend/packages/agent/src/agent.css +++ b/frontend/packages/agent/src/agent.css @@ -12,10 +12,7 @@ surface their counts as the only chrome they get. */ :root { - /* 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-header-h: 4.6em; --agent-composer-h: 3.6em; --agent-frost-bg: rgba(30, 30, 46, 0.72); --agent-frost-blur: blur(12px) saturate(140%); @@ -73,41 +70,31 @@ body.agent-shell { z-index: 30; min-height: var(--agent-header-h); display: flex; - /* align-items: stretch lets the icon take the full header height - (it sizes itself via aspect-ratio off the stretched height). The - main column + pills column self-centre via inner layout. */ - align-items: stretch; - gap: 0.9em; - padding: 0.5em 1em; + align-items: center; + gap: 1em; + padding: 0.55em 1em; background: var(--agent-frost-bg); -webkit-backdrop-filter: var(--agent-frost-blur); backdrop-filter: var(--agent-frost-blur); border-bottom: 1px solid var(--purple-dim); 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 - vertically against the full-height icon on the left. */ -.agent-header-main { +.agent-header-title { display: flex; flex-direction: column; - justify-content: center; - gap: 0.45em; + gap: 0.1em; min-width: 0; flex: 1 1 auto; } -.agent-header-row { - display: flex; - align-items: center; - gap: 0.8em; - flex-wrap: wrap; - min-width: 0; -} -.agent-header-title-row h2 { +.agent-header-title h2 { margin: 0; line-height: 1; } .agent-nav { + /* Slim nav row directly under the title — keeps the operator's + fingers near stats/screen/forge without dominating the header. */ display: flex; flex-wrap: wrap; gap: 0.4em 0.8em; @@ -116,21 +103,10 @@ body.agent-shell { .agent-state-row { 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; align-items: center; gap: 0.5em; - flex-shrink: 0; flex-wrap: wrap; - justify-content: flex-end; - align-self: center; } h2, h3 { @@ -140,126 +116,11 @@ h2, h3 { text-shadow: 0 0 8px rgba(203, 166, 247, 0.4); } .agent-icon { - /* Full-height square identity anchor (#394 — mara's spec). The - `align-items: stretch` on .agent-header stretches the icon's - `` 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); + width: 44px; + height: 44px; 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; + box-shadow: 0 0 14px -2px rgba(203, 166, 247, 0.35); } /* 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-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 - overflow menu in #394 (`.overflow-item-rebuild` covers it now). - The dashboard has its own `.btn-rebuild` rule for the per-row - R3BU1LD form on the SW4RM tab; this one was specific to the - per-agent header. - `.btn-send` was a green send-button variant — orphaned since - the dashboard's compose form was retired; no live consumer left - in either the agent or dashboard tree. */ +.btn-rebuild { + color: var(--amber); + border: 1px solid var(--amber); + 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; + 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 input { font-family: inherit; font-size: 1em; @@ -425,6 +292,12 @@ pre.diff { word-break: break-all; 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 state row and the terminal so the operator can peek at what landed without scrolling through the live tail. */ @@ -563,8 +436,21 @@ pre.diff { 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-offline { color: var(--muted); border-color: var(--muted); } -/* Orphaned in #394 — `.btn-dashlink` chip beside the title moved - into the overflow menu (`.overflow-item-dashboard` covers it). */ +.btn-dashlink { + 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 { font-family: inherit; font-size: 0.8em; @@ -582,10 +468,27 @@ pre.diff { background: rgba(243, 139, 168, 0.1); box-shadow: 0 0 10px -2px currentColor; } -/* Orphaned in #394 — `.btn-new-session` round-pill moved into the - overflow menu (`.overflow-item-new-session` covers it; the - `:disabled` opacity treatment lives on the shared - `.overflow-item:disabled` rule). */ +.btn-new-session { + font-family: inherit; + font-size: 0.8em; + 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 { display: inline-block; padding: 0.25em 0.8em; diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 890b0f9..d72d1b8 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -141,139 +141,34 @@ window.marked = marked; // ─── state rendering ──────────────────────────────────────────────────── function setHeader(label, dashboardPort) { const title = $('title'); - // 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`; + 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. const dashUrl = `${location.protocol}//${location.hostname}:${dashboardPort}/`; dashboardBase = dashUrl; - 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', + title.append( + el('a', { + href: dashUrl, target: '_blank', rel: 'noopener', + class: 'btn-dashlink', title: 'host dashboard', + }, '↑ DASHB04RD'), + ' ', ); - rebuildBtn.addEventListener('click', () => { - if (!window.confirm(`rebuild ${label}? container will hot-reload.`)) return; - closeOverflowMenu(); + 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; const f = document.createElement('form'); f.method = 'POST'; f.action = `${dashUrl}rebuild/${label}`; document.body.appendChild(f); f.submit(); }); - 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; + title.append(btn); + document.title = `${label} // hyperhive`; } - 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 @@ -834,9 +729,18 @@ window.marked = marked; }); })(); - // (#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. + // 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; }); + }); + })(); // Track banner activity by reference-counting in-flight turns. A turn // can begin while the previous turn_end is still in the pipeline (rare @@ -884,9 +788,7 @@ window.marked = marked; rel: 'noopener', title: lnk.label || '', }); - // Layout gap comes from `.agent-nav { gap }` (#394) — drop - // the legacy per-link inline `marginLeft`. The trailing `→` - // is the "leaves this page" affordance. + if (i > 0) a.style.marginLeft = '1em'; a.append(((lnk.icon || '') + ' ' + (lnk.label || '')).trim() + ' →'); metaLinks.append(a); }); diff --git a/frontend/packages/agent/src/index.html b/frontend/packages/agent/src/index.html index f39065c..5874e19 100644 --- a/frontend/packages/agent/src/index.html +++ b/frontend/packages/agent/src/index.html @@ -8,66 +8,45 @@ - +
- -
-
-

◆ … ◆

- - -
- -
- - … booting - - - - - -
+
+

◆ … ◆

+
- -
- - - +
+ + … booting + + + + + +
+ + + +
- - -