frontend: vibec0re terminal overhaul (#360)

Per operator spec at #360#issuecomment-3333:
- full-screen terminal
- frosted-glass header overlaid on top
- inbox + loose-ends → flyout
- no a/b flag, just ship it

## Layout

`frontend/packages/agent/src/index.html` restructured to a three-zone
fixed-overlay shape:

- `<header.agent-header>` — fixed top, frosted glass via
  `backdrop-filter: blur(12px) saturate(140%)`. Holds icon + title +
  nav links + state-row (badges/buttons) + two new pill buttons that
  surface inbox / loose-ends counts (and open the side panel on click).
- `<main.agent-main>` — fills the viewport. Terminal positioned absolute
  inset:0 with padding-top/-bottom + scroll-padding equal to the
  floating header/composer heights so first/last rows stay reachable
  and `↓ N new` pill anchors land in the visible scroll zone.
- `<footer.agent-composer>` — fixed bottom, mirror-frosted. Owns
  `#term-input`; dropped the in-frame dashed separator (border-top
  + box-shadow on the bar already separate it from the terminal).
- `<div.side-panel>` — singleton drawer (copy of the dashboard
  pattern, candidate for extraction into @hive/shared). Inbox +
  loose-ends details render here instead of expanding inline.

Dropped from the page: the pre-banner ASCII shimmer (`<pre.banner>`)
and the in-page `<details>` collapsibles for inbox + loose-ends. The
banner JS path (`setBannerActive`) is now a no-op (early-returns on
missing element); kept as dead code rather than ripped out to keep
the diff focused.

## JS

`frontend/packages/agent/src/app.js`:

- New `Panel` singleton with `open(name, title, content)` +
  `close()` + `refresh(name, title, content)` (no-op if a different
  view owns the panel — lets live updates re-render an open view
  without grabbing focus from a closed one). Mirror of the
  dashboard's Panel module; the duplication is intentional for now.
- `renderInbox` + `renderLooseEnds` refactored: update the header
  pill counts, hide/show the pills, and `Panel.refresh` if the
  matching view is open. The list-building DOM logic moved into
  `buildInboxList` + `buildLooseEndsList` so the pill click handler
  can call them on the latest snapshot kept in `lastInbox` /
  `lastLooseEnds` module state.
- Pill click handlers `Panel.open(...)` with the freshly built list.
- Auto-expand behavior on first appearance dropped (the pill +
  count badge is the discoverable signal; auto-popping the flyout
  would interrupt whatever the operator is doing).
- `setHeader` no longer touches `#banner` (element removed); title +
  dashboard back-link + rebuild button still get appended to `#title`.

## CSS

`frontend/packages/agent/src/agent.css` major additions, scoped
`body.agent-shell` so the sibling `stats.html` (which doesn't apply
the shell class) keeps its normal-document scroll + `.banner` ASCII
header via a `body:not(.agent-shell)` block.

New CSS custom properties on :root: `--agent-header-h`,
`--agent-composer-h`, `--agent-frost-bg`, `--agent-frost-blur`. The
terminal's padding + scroll-padding derive from these so a single
height tweak ripples consistently.

Added `.header-pill` (inbox/loose-ends triggers) +
`.agent-status-overlay` (centred login card when status != online).
Side-panel rules copied from `dashboard.css` with one delta: width
caps at 640px (vs dashboard's 760px) since per-agent inbox / loose-
ends rows are narrower than approval diffs / file previews.

## Validation

- `npm run build` — succeeds both workspaces.
  - agent: `dist/static/{app.js (115kb), stats.js (435kb), agent.css (21kb)}`
  - dashboard unchanged (no shared sources touched).
- Browser smoke test isn't possible from inside iris's container
  (no JS engine) — op-side check on next deploy.

Closes #360.
This commit is contained in:
iris 2026-05-24 03:49:26 +02:00
parent 4d7c767eb0
commit e931c08739
3 changed files with 496 additions and 79 deletions

View file

@ -3,10 +3,31 @@
@import "@hive/shared/base.css";
@import "@hive/shared/terminal.css";
body {
/* 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) {
max-width: 110em;
margin: 1.5em auto;
padding: 0 1.5em;
height: auto;
}
.banner {
text-align: center;
@ -26,30 +47,177 @@ body {
color: transparent;
filter: drop-shadow(0 0 6px rgba(203, 166, 247, 0.45));
}
.banner.active {
animation: banner-shimmer 1.8s linear infinite;
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);
}
@keyframes banner-shimmer {
from { background-position: 200% 0; }
to { background-position: -100% 0; }
.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;
}
.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: 40px;
height: 40px;
width: 44px;
height: 44px;
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); }
@ -347,14 +515,54 @@ 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; }
}
/* `.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; }
/* 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. */
.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;
}
@ -387,3 +595,97 @@ 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 <details>-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;
}