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 @@