diff --git a/frontend/packages/agent/src/agent.css b/frontend/packages/agent/src/agent.css index 6222bcb2..68103202 100644 --- a/frontend/packages/agent/src/agent.css +++ b/frontend/packages/agent/src/agent.css @@ -3,31 +3,10 @@ @import "@hive/shared/base.css"; @import "@hive/shared/terminal.css"; -/* ─── full-screen vibec0re overhaul (issue #360) ────────────────── - Layout shape: fixed-position frosted-glass header at top, fixed- - position composer at bottom, full-viewport terminal in between. - The terminal scrolls — its text passes BENEATH the floating - header/composer with backdrop-filter blur for the frosted look. - Inbox + loose-ends move into the side-panel flyout; header pills - surface their counts as the only chrome they get. */ - -:root { - --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%); -} - -html, body { height: 100%; margin: 0; } - -/* Legacy in-page layout retained for the sibling stats page - (`stats.html`) which doesn't apply `body.agent-shell` and stays - on a normal-document scroll. */ -body:not(.agent-shell) { +body { max-width: 110em; margin: 1.5em auto; padding: 0 1.5em; - height: auto; } .banner { text-align: center; @@ -47,177 +26,30 @@ body:not(.agent-shell) { color: transparent; filter: drop-shadow(0 0 6px rgba(203, 166, 247, 0.45)); } - -body.agent-shell { - background: var(--bg); - color: var(--fg); - /* Body itself doesn't scroll; the terminal does inside .agent-main. */ - overflow: hidden; - /* Subtle radial accent to give the otherwise-flat full-screen - surface some depth and reinforce the vibec0re mood. */ - background: - radial-gradient(ellipse 80% 60% at 50% 0%, - rgba(203, 166, 247, 0.06) 0%, - transparent 60%), - var(--bg); +.banner.active { + animation: banner-shimmer 1.8s linear infinite; } - -.agent-header { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 30; - min-height: var(--agent-header-h); - display: flex; - 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; +@keyframes banner-shimmer { + from { background-position: 200% 0; } + to { background-position: -100% 0; } } - -.agent-header-title { - display: flex; - flex-direction: column; - gap: 0.1em; - min-width: 0; - flex: 1 1 auto; -} -.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; - margin: 0; -} - -.agent-state-row { - margin: 0; - display: flex; - align-items: center; - gap: 0.5em; - flex-wrap: wrap; -} - h2, h3 { color: var(--purple); text-transform: uppercase; letter-spacing: 0.15em; text-shadow: 0 0 8px rgba(203, 166, 247, 0.4); } +.title-row { + display: flex; + align-items: center; + gap: 0.6rem; +} +.title-row h2 { margin: 0; } .agent-icon { - width: 44px; - height: 44px; + width: 40px; + height: 40px; border-radius: 6px; flex-shrink: 0; - box-shadow: 0 0 14px -2px rgba(203, 166, 247, 0.35); -} - -/* Header pill — inbox / loose-ends triggers. Compact, count-prominent. */ -.header-pill { - background: transparent; - border: 1px solid var(--purple-dim); - color: var(--fg); - font-family: inherit; - font-size: 0.85em; - letter-spacing: 0.04em; - border-radius: 999px; - padding: 0.25em 0.7em; - display: inline-flex; - align-items: center; - gap: 0.4em; - cursor: pointer; - transition: border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease; -} -.header-pill:hover { - border-color: var(--purple); - color: var(--purple); - box-shadow: 0 0 10px -2px var(--purple); -} -.header-pill-icon { font-size: 1.05em; line-height: 1; } -.header-pill-label { color: var(--muted); } -.header-pill-count { - background: var(--purple-dim); - color: var(--purple); - border-radius: 999px; - padding: 0 0.5em; - min-width: 1.6em; - text-align: center; - font-weight: bold; - font-variant-numeric: tabular-nums; -} -.header-pill-inbox .header-pill-count { - background: rgba(250, 179, 135, 0.18); - color: var(--amber); -} -.header-pill-loose .header-pill-count { - background: rgba(243, 139, 168, 0.18); - color: var(--red); -} - -.agent-main { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - overflow: hidden; -} - -/* Login flow overlay: only rendered when status != online. Sits - centred over the (likely-empty) terminal area; doesn't take chrome - space in the normal online flow. */ -.agent-status-overlay { - position: absolute; - top: calc(var(--agent-header-h) + 1.5em); - left: 50%; - transform: translateX(-50%); - max-width: 44em; - width: calc(100% - 3em); - z-index: 10; -} -.agent-status-overlay:empty { display: none; } -.agent-status-overlay > * { - background: var(--bg-elev); - border: 1px solid var(--purple-dim); - border-radius: 6px; - padding: 1em 1.2em; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); -} - -.agent-composer { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 30; - min-height: var(--agent-composer-h); - background: var(--agent-frost-bg); - -webkit-backdrop-filter: var(--agent-frost-blur); - backdrop-filter: var(--agent-frost-blur); - border-top: 1px solid var(--purple-dim); - box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.35); -} -.agent-composer .term-input { - /* The composer is its own chrome now — drop the in-terminal-wrap - padding the legacy layout assumed. */ - padding: 0.45em 1em; -} -.agent-composer .term-input .sendform-term { - /* No dashed top-border in the floating composer — the box-shadow - and frosted border already separate it from the terminal. */ - border-top: 0; - padding-top: 0; } .meta { color: var(--muted); font-size: 0.85em; } .status-online { color: var(--green); text-shadow: 0 0 6px rgba(166, 227, 161, 0.55); } @@ -515,54 +347,14 @@ pre.diff { 60% { box-shadow: 0 0 18px -4px currentColor, 0 0 4px 0 currentColor; } 100% { box-shadow: 0 0 0 0 currentColor, 0 0 0 0 currentColor; } } -/* Full-screen overrides for the shared terminal rules. The base - `.terminal-wrap` (in shared/src/terminal.css) ships a crust-on- - black frame for the in-page case; the agent page now owns the - whole viewport so the frame chrome would be redundant noise. - `.live.terminal` similarly drops the in-page max-height cap so - it can fill the main area top-to-bottom; the floating header + - composer overlay it via fixed positioning. */ -.agent-main .terminal-wrap { - position: absolute; - inset: 0; - border: 0; - background: transparent; - box-shadow: none; - border-radius: 0; - margin: 0; - padding: 0; -} -.agent-main .live.terminal { - position: absolute; - inset: 0; - height: auto; - max-height: none; - /* Scroll behind the floating header/composer, but keep the first - and last rows reachable with extra padding inside the scroll - area. scroll-padding-* keeps anchor-jumps (the `↓ N new` pill, - focus restore) clear of the floats too. */ - padding-top: calc(var(--agent-header-h) + 0.8em); - padding-bottom: calc(var(--agent-composer-h) + 0.8em); - scroll-padding-top: calc(var(--agent-header-h) + 0.8em); - scroll-padding-bottom: calc(var(--agent-composer-h) + 0.8em); - overflow: auto; -} -/* Tail pill (↓ N new): nudged up so it floats clear of the composer - rather than colliding with the frosted bar. */ -.agent-main .tail-pill { - bottom: calc(var(--agent-composer-h) + 0.6em); -} - -/* Composer chrome — used to live inside `.terminal-wrap`; now lives - inside the fixed `.agent-composer` defined further up. The base - rules below stay scoped to whichever ancestor owns it. */ +/* `.terminal-wrap`, `.live`, `.live.terminal`, row + pill + details + styling all live in hive-fr0nt::TERMINAL_CSS (prepended by serve_css). + What stays here is the composer chrome that sits inside the wrap. */ +.term-input { padding: 0.4em 1em 0.8em; } .term-input .sendform-term { display: flex; align-items: flex-start; gap: 0.5em; - /* The dashed in-frame separator is dropped — see the - .agent-composer .term-input override above for the floating-bar - variant. */ border-top: 1px dashed var(--purple-dim); padding-top: 0.5em; } @@ -595,97 +387,3 @@ pre.diff { .term-input.disabled .prompt { color: var(--muted); text-shadow: none; } .term-input.disabled textarea { color: var(--muted); } /* Row + pill + details styling moved to hive-fr0nt::TERMINAL_CSS. */ - -/* ─── side panel (singleton drawer) ──────────────────────────────── - Inbox + loose-ends details open here instead of expanding inline - (issue #360). Copy of the dashboard's side-panel pattern — - candidate for extraction into @hive/shared once both surfaces - stabilize. */ -.side-panel { - position: fixed; - inset: 0; - z-index: 50; - /* Closed: ignore pointer events so the agent page underneath stays - interactive; `.open` flips it back on. */ - pointer-events: none; -} -.side-panel-backdrop { - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.55); - opacity: 0; - transition: opacity 0.2s ease; -} -.side-panel-drawer { - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: min(640px, 92vw); - display: flex; - flex-direction: column; - background: var(--bg-elev); - border-left: 2px solid var(--purple); - box-shadow: -10px 0 30px rgba(0, 0, 0, 0.45); - transform: translateX(100%); - transition: transform 0.25s ease; -} -.side-panel.open { pointer-events: auto; } -.side-panel.open .side-panel-backdrop { opacity: 1; } -.side-panel.open .side-panel-drawer { transform: translateX(0); } -.side-panel-head { - flex: 0 0 auto; - display: flex; - align-items: center; - justify-content: space-between; - gap: 1em; - padding: 0.7em 1em; - border-bottom: 1px solid var(--border); -} -.side-panel-title { - color: var(--purple); - font-weight: bold; - letter-spacing: 0.05em; - word-break: break-all; -} -.side-panel-close { - flex: 0 0 auto; - background: var(--bg); - color: var(--fg); - border: 1px solid var(--border); - font-family: inherit; - font-size: 1em; - line-height: 1; - padding: 0.25em 0.55em; - cursor: pointer; -} -.side-panel-close:hover { border-color: var(--red); color: var(--red); } -.side-panel-body { - flex: 1 1 auto; - overflow: auto; - padding: 0.8em 1em; -} -/* Inbox / loose-ends lists rendered into the side-panel body. The - legacy
-collapsible variant of .agent-inbox is gone, so - here we strip the inbox-only chrome (background, border-left) and - let the panel body's own padding own the framing. */ -.side-panel-body .agent-inbox { - margin: 0; - font-size: inherit; - color: var(--fg); -} -.side-panel-body .agent-inbox ul { - background: transparent; - border-left: 0; - padding: 0; - max-height: none; - overflow: visible; -} - -/* Empty-state placeholders for the side panel (when count drops to 0 - between the click and the render — rare, but possible). */ -.side-panel-empty { - color: var(--muted); - font-style: italic; - padding: 1em 0; -} diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 605078ce..c2a56c55 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -77,69 +77,10 @@ window.marked = marked; } }); - // ─── side panel (singleton drawer for inbox + loose-ends flyouts) ────── - // Shared shape with the dashboard's panel. Candidate for extraction - // into @hive/shared in a follow-up — keeping the duplication for - // now to land #360 without simultaneously refactoring the dashboard. - const Panel = (() => { - const root = $('side-panel'); - const titleEl = $('side-panel-title'); - const bodyEl = $('side-panel-body'); - /** Owner key (e.g. 'inbox', 'loose-ends'). Refresh hooks check - * against this so a live event only re-renders the panel when - * the matching view is actually visible. null when closed. */ - let owner = null; - function open(name, title, content) { - owner = name; - titleEl.textContent = title; - bodyEl.replaceChildren(...(content ? [content] : [])); - root.classList.add('open'); - root.setAttribute('aria-hidden', 'false'); - } - function close() { - owner = null; - root.classList.remove('open'); - root.setAttribute('aria-hidden', 'true'); - } - function refresh(name, title, content) { - if (owner !== name) return; - titleEl.textContent = title; - bodyEl.replaceChildren(...(content ? [content] : [])); - } - function bind() { - $('side-panel-close').addEventListener('click', close); - $('side-panel-backdrop').addEventListener('click', close); - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && root.classList.contains('open')) close(); - }); - } - return { open, close, refresh, bind, currentOwner: () => owner }; - })(); - Panel.bind(); - - // Wire the header pills to open the side panel. Pre-built (vs - // re-building per-click) so the freshest snapshot already lives - // in `lastInbox` / `lastLooseEnds` when the pill is clicked — even - // if it fires during a turn the render is the same. - (function bindHeaderPills() { - const inboxPill = $('inbox-pill'); - if (inboxPill) { - inboxPill.addEventListener('click', () => { - Panel.open('inbox', 'inbox · ' + lastInbox.length, - buildInboxList(lastInbox)); - }); - } - const loosePill = $('loose-ends-pill'); - if (loosePill) { - loosePill.addEventListener('click', () => { - Panel.open('loose-ends', 'loose ends · ' + lastLooseEnds.length, - buildLooseEndsList(lastLooseEnds)); - }); - } - })(); - // ─── state rendering ──────────────────────────────────────────────────── function setHeader(label, dashboardPort) { + $('banner').textContent = + `░▒▓█▓▒░ ${label} ░▒▓█▓▒░ hyperhive ag3nt ░▒▓█▓▒░`; const title = $('title'); title.textContent = `◆ ${label} ◆ `; // ↑ DASHB04RD — back-link to the host dashboard. Opens in a new @@ -474,7 +415,8 @@ window.marked = marked; // Loose-ends section: same data the get_loose_ends MCP tool // returns. Best-effort fetch on cold load + after every turn_end // (a turn likely answered or asked something). Silent failure - // keeps the pill count at zero rather than surfacing a stale chrome. + // keeps the section hidden rather than surfacing an empty banner. + let lastLooseEndsCount = 0; async function refreshLooseEnds() { try { const resp = await fetch('/api/loose-ends'); @@ -489,22 +431,24 @@ window.marked = marked; renderLooseEnds([]); } } - /** Latest snapshot kept in module state so the pill click handler - * has fresh data to render into the panel without re-fetching. */ - let lastLooseEnds = []; - let lastInbox = []; - - function buildLooseEndsList(threads) { - // Returns the
the side panel renders. The structural shape - // mirrors the legacy
-collapsible block — same CSS rules - // apply via `.side-panel-body .agent-inbox`. - const wrap = el('div', { class: 'agent-inbox' }); + function renderLooseEnds(threads) { + const root = $('loose-ends-section'); + const list = $('loose-ends-list'); + const summary = $('loose-ends-summary'); + if (!root || !list || !summary) return; if (!threads.length) { - wrap.append(el('p', { class: 'side-panel-empty' }, - 'no loose ends — every question, approval and reminder is resolved.')); - return wrap; + root.hidden = true; + lastLooseEndsCount = 0; + return; } - const list = el('ul'); + root.hidden = false; + summary.textContent = 'loose ends · ' + threads.length; + list.innerHTML = ''; + // Auto-expand on first appearance of any open thread so the + // operator notices new loose ends; collapse only on operator + // click (sticky after that). + if (lastLooseEndsCount === 0) root.open = true; + lastLooseEndsCount = threads.length; const fmtAge = (s) => { if (s < 60) return s + 's'; if (s < 3600) return Math.floor(s / 60) + 'm'; @@ -548,21 +492,6 @@ window.marked = marked; } list.append(li); } - wrap.append(list); - return wrap; - } - - /** Pill-count + open-panel-refresh wiring for loose-ends. The legacy - * in-page `
` block is gone — operator clicks the header - * pill to surface the list in the side panel. */ - function renderLooseEnds(threads) { - lastLooseEnds = threads; - const pill = $('loose-ends-pill'); - const count = $('loose-ends-count'); - if (count) count.textContent = threads.length; - if (pill) pill.hidden = threads.length === 0; - Panel.refresh('loose-ends', 'loose ends · ' + threads.length, - buildLooseEndsList(threads)); } // Inline "answer as operator" form for a question loose-end. POSTs to @@ -601,14 +530,18 @@ window.marked = marked; return wrap; } - function buildInboxList(rows) { - const wrap = el('div', { class: 'agent-inbox' }); + function renderInbox(rows) { + const root = $('inbox-section'); + const list = $('inbox-list'); + const summary = $('inbox-summary'); + if (!root || !list || !summary) return; if (!rows.length) { - wrap.append(el('p', { class: 'side-panel-empty' }, - 'inbox empty.')); - return wrap; + root.hidden = true; + return; } - const list = el('ul'); + root.hidden = false; + summary.textContent = 'inbox · ' + rows.length; + list.innerHTML = ''; const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(5, 19); for (const m of rows) { const li = el('li', m.in_reply_to != null ? { class: 'inbox-reply' } : {}); @@ -623,18 +556,6 @@ window.marked = marked; ); list.append(li); } - wrap.append(list); - return wrap; - } - - /** Pill-count + open-panel-refresh wiring for inbox. */ - function renderInbox(rows) { - lastInbox = rows; - const pill = $('inbox-pill'); - const count = $('inbox-count'); - if (count) count.textContent = rows.length; - if (pill) pill.hidden = rows.length === 0; - Panel.refresh('inbox', 'inbox · ' + rows.length, buildInboxList(rows)); } // Harness reachability badge: derived from the same `s.status` the // status block reads. Each status maps to a glyph + label + colour diff --git a/frontend/packages/agent/src/index.html b/frontend/packages/agent/src/index.html index 5874e191..dcc0d2b9 100644 --- a/frontend/packages/agent/src/index.html +++ b/frontend/packages/agent/src/index.html @@ -6,79 +6,43 @@ - - - -
+ + +
-
-

◆ … ◆

- -
+

◆ … ◆

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

loading…

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