Phase 1 of the backend/frontend code split (#273). Additive — no existing code is touched; the legacy hive-c0re/assets, hive-ag3nt/ assets and hive-fr0nt/assets trees stay in place until the Rust cutover later in this branch. Layout: frontend/package.json npm workspaces root frontend/packages/shared/ @hive/shared src/{base,terminal}.css + terminal.js (ES module) src/index.js re-exports terminal.js frontend/packages/dashboard/ @hive/dashboard src/{index.html, app.js, dashboard.css} ported from hive-c0re/assets build.mjs esbuild config → dist/ frontend/packages/agent/ @hive/agent src/{index,stats,screen}.html + agent.css + {app,stats}.js ported from hive-ag3nt/assets build.mjs esbuild config → dist/ Changes vs the existing assets: - terminal.js is an ES module exporting { create, linkify } instead of assigning to window.HiveTerminal. The dashboard / agent app.js files re-expose them on window so the IIFE bodies keep working unchanged through Phase 1; the global aliases can be dropped in a follow-up once the IIFEs are unwrapped. - marked is imported from the marked@4.3.0 npm package (replacing the vendored hive-fr0nt/assets/marked.umd.js bundle). - chart.js is imported from chart.js@4.4.4 (replacing the jsDelivr CDN script tag on the per-agent stats page — page now works offline / on operator machines without internet egress). - dashboard.css and agent.css both gain @import lines at the top that pull base.css + terminal.css from @hive/shared, replacing the runtime string concatenation in serve_css. - index.html / stats.html collapse from three / two script tags to one type="module" tag pointing at the bundled output. package-lock.json is intentionally omitted from this commit — npm isn't available in the iris container yet (approval pending) and the lockfile will land in the next commit on this branch once the toolchain is in place. The PR will not be opened until it's there. Phase 2 (nix derivations), Phase 3 (container plumbing + the hyperhive.frontend.extraFiles option for per-agent layering), and Phase 4 (Rust cutover to tower_http::ServeDir, delete hive-fr0nt + legacy assets dirs) land as follow-up commits on this same branch. Refs #273.
228 lines
8.3 KiB
CSS
228 lines
8.3 KiB
CSS
/* Shared terminal pane: a scroll-sticky log of rows + a "↓ N new" pill.
|
|
Pages wrap their stream container in `.terminal-wrap` and give the log
|
|
itself the `.live` class; renderer JS appends `.row` (flat line) or
|
|
`details.row` (collapsible body) elements. Row-kind classes
|
|
(`.turn-start`, `.tool-use`, `.thinking`, etc.) carry the per-event
|
|
colour; pages that don't emit a given kind simply never produce that
|
|
class — the unused rule sits in the bundle harmlessly.
|
|
|
|
`.terminal-wrap` provides the crust-on-black phosphor chrome that makes
|
|
the agent page feel like a terminal. Pages can opt in by wrapping a
|
|
block in this class; or skip it and the rows still render with their
|
|
class colours, just without the frame.
|
|
|
|
No `.term-input` here — composers are a separate concern (see
|
|
hive-fr0nt::COMPOSER_CSS / COMPOSER_JS once introduced). */
|
|
|
|
.terminal-wrap {
|
|
position: relative;
|
|
background: rgba(17, 17, 27, 0.78);
|
|
-webkit-backdrop-filter: blur(8px) saturate(120%);
|
|
backdrop-filter: blur(8px) saturate(120%);
|
|
border: 1px solid var(--purple-dim);
|
|
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.7);
|
|
border-radius: 4px;
|
|
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace;
|
|
font-size: 0.92em;
|
|
color: var(--fg);
|
|
margin-top: 0.6em;
|
|
}
|
|
.live {
|
|
background: rgba(255, 255, 255, 0.02);
|
|
border: 1px solid var(--purple-dim);
|
|
padding: 0.4em 0.6em;
|
|
overflow-y: auto;
|
|
max-height: 32em;
|
|
font-family: inherit;
|
|
}
|
|
.live.terminal {
|
|
background: transparent;
|
|
border: 0;
|
|
box-shadow: none;
|
|
border-radius: 0;
|
|
padding: 0.8em 1em 0.4em;
|
|
overflow-y: auto;
|
|
height: min(72vh, 60em);
|
|
max-height: none;
|
|
font-family: inherit;
|
|
font-size: inherit;
|
|
color: inherit;
|
|
}
|
|
.live .row,
|
|
.live details.row {
|
|
animation: row-fade-in 220ms ease-out both;
|
|
}
|
|
.live .row.no-anim,
|
|
.live details.row.no-anim {
|
|
animation: none;
|
|
}
|
|
@keyframes row-fade-in {
|
|
from { opacity: 0; transform: translateY(4px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
/* Unified prefix column for every row kind. The glyph (`→ ← · ◆ ✓ ✗ ⌁ !`)
|
|
is the first character of the row's text content; `padding-left` reserves
|
|
the column and `text-indent: -1.4em` pulls the glyph back into it. Wrapped
|
|
continuation lines then start under the body, not under the glyph, so
|
|
wraps don't blur into the next row. `details.row` summaries reuse the
|
|
same metrics below. */
|
|
.live .row {
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
padding: 0.05em 0;
|
|
line-height: 1.45;
|
|
border-left: 2px solid transparent;
|
|
padding-left: 1.9em;
|
|
text-indent: -1.4em;
|
|
margin: 0.1em 0;
|
|
}
|
|
.live .row + .row { border-top: 0; }
|
|
/* Row-kind colours. Pages register renderers that emit these classes;
|
|
any class no page emits is just dead CSS, which is fine. Turn-framing
|
|
classes carry their signal entirely on the coloured border-left rule —
|
|
no bold, no top/bottom margins, no background tint. The chrome was
|
|
overweight for what's just a "this is a boundary" marker. */
|
|
.live .turn-start { color: var(--amber); border-left-color: var(--amber); }
|
|
/* turn-body is a child block under turn-start carrying the wake-prompt
|
|
body; reset text-indent so wrapped content stays under its own column
|
|
instead of pulling back into the parent's prefix. */
|
|
.live .turn-body { color: var(--fg); text-indent: 0; margin-top: 0.15em; }
|
|
/* Any child block (markdown body, nested details) resets the parent
|
|
row's hanging indent so the content lays out from column 0 of the
|
|
body area. */
|
|
.live .row .md, .live .row > details { text-indent: 0; }
|
|
.live .turn-end-ok { color: var(--green); border-left-color: var(--green); }
|
|
.live .turn-end-fail { color: var(--red); border-left-color: var(--red); }
|
|
.live .text { color: var(--fg); }
|
|
.live .thinking { color: var(--muted); font-style: italic; }
|
|
.live .tool-use { color: var(--cyan); }
|
|
.live .tool-result { color: var(--muted); }
|
|
.live .result { color: var(--green); }
|
|
.live .note { color: var(--muted); }
|
|
/* Distinguish stderr lines (orange) and operator-initiated notes
|
|
(mauve, lightly emphasised) from ambient harness chatter so the
|
|
eye picks out anomalies + operator actions in the scrollback. */
|
|
.live .note.stderr { color: var(--amber); }
|
|
.live .note.op { color: var(--purple); font-style: italic; }
|
|
/* The .sys catch-all fires when renderStream landed an event shape it
|
|
couldn't classify. Make it visually loud so silently-dropped event
|
|
types surface for follow-up. */
|
|
.live .sys { color: var(--amber); }
|
|
.live .unread-badge {
|
|
color: var(--amber);
|
|
font-weight: normal;
|
|
margin-left: 0.6em;
|
|
font-size: 0.85em;
|
|
text-shadow: 0 0 6px rgba(250, 179, 135, 0.55);
|
|
animation: badge-pulse 1.4s ease-in-out infinite;
|
|
}
|
|
@keyframes badge-pulse {
|
|
0%, 100% { opacity: 1; text-shadow: 0 0 6px rgba(250, 179, 135, 0.55); }
|
|
50% { opacity: 0.7; text-shadow: 0 0 14px rgba(250, 179, 135, 0.95); }
|
|
}
|
|
/* "↓ N new" pill: shown when new rows arrive while the operator is
|
|
scrolled up; click to jump to bottom. Positioned by the wrapper's
|
|
`position: relative` (terminal-wrap supplies it; pages that skip the
|
|
wrapper must add their own positioned ancestor). */
|
|
.tail-pill {
|
|
position: absolute;
|
|
right: 1em;
|
|
bottom: 4.2em;
|
|
background: var(--amber);
|
|
color: #11111b;
|
|
font-family: inherit;
|
|
font-size: 0.8em;
|
|
font-weight: bold;
|
|
letter-spacing: 0.08em;
|
|
border: 0;
|
|
border-radius: 999px;
|
|
padding: 0.35em 0.9em;
|
|
cursor: pointer;
|
|
box-shadow: 0 0 14px -2px rgba(250, 179, 135, 0.85);
|
|
opacity: 0;
|
|
transform: translateY(6px);
|
|
pointer-events: none;
|
|
transition: opacity 160ms ease, transform 160ms ease;
|
|
}
|
|
.tail-pill.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
pointer-events: auto;
|
|
}
|
|
.tail-pill:hover { filter: brightness(1.1); }
|
|
/* Expandable rows reuse the flat-row prefix metrics (padding-left +
|
|
negative text-indent) so the disclosure glyph (`▸ / ▾`) lands in
|
|
exactly the same column as flat-row prefix glyphs (`→ ← · ◆ ✓ ✗`).
|
|
Summary text omits the per-row directional glyph (the row colour
|
|
already carries cyan = outbound tool, muted = inbound result) so
|
|
the prefix column doesn't have to fit two glyphs side-by-side. */
|
|
details.row {
|
|
white-space: normal;
|
|
}
|
|
details.row > summary {
|
|
cursor: pointer;
|
|
list-style: none;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
details.row > summary::before {
|
|
content: '▸ ';
|
|
color: inherit;
|
|
}
|
|
details.row[open] > summary::before { content: '▾ '; }
|
|
details.row > pre.diff-body,
|
|
details.row > pre.tool-body {
|
|
margin: 0.3em 0 0.4em 0;
|
|
padding: 0.4em 0.6em;
|
|
text-indent: 0;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
border-left: 2px solid var(--purple-dim);
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
max-height: 22em;
|
|
overflow-y: auto;
|
|
}
|
|
details.row > pre.tool-body { color: var(--fg); }
|
|
details.row > pre.diff-body .diff-add { color: var(--green); }
|
|
details.row > pre.diff-body .diff-del { color: var(--red); }
|
|
details.row > pre.diff-body .diff-ctx { color: var(--fg); }
|
|
/* Markdown body inside a row (assistant text, send/recv/ask/answer
|
|
message bodies). Inline elements get muted accents; block elements
|
|
reset the parent row's hanging indent so content lays out cleanly. */
|
|
.live .row .md p { margin: 0.2em 0; }
|
|
.live .row .md p:first-child { margin-top: 0; }
|
|
.live .row .md p:last-child { margin-bottom: 0; }
|
|
.live .row .md code {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
padding: 0.05em 0.3em;
|
|
border-radius: 3px;
|
|
font-size: 0.95em;
|
|
}
|
|
.live .row .md pre {
|
|
margin: 0.3em 0;
|
|
padding: 0.4em 0.6em;
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border-left: 2px solid var(--purple-dim);
|
|
text-indent: 0;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
.live .row .md pre code {
|
|
background: transparent;
|
|
padding: 0;
|
|
border-radius: 0;
|
|
}
|
|
.live .row .md a { color: var(--cyan); text-decoration: underline; }
|
|
/* Auto-linkified bare URLs in plain rows + tool-body blocks (issue #233). */
|
|
.live .row a { color: var(--cyan); text-decoration: underline; }
|
|
.live .row a:hover { color: var(--fg); }
|
|
.live .row .md strong { color: inherit; font-weight: bold; }
|
|
.live .row .md em { color: inherit; font-style: italic; }
|
|
.live .row .md ul, .live .row .md ol { margin: 0.2em 0 0.2em 1.4em; padding: 0; }
|
|
.live .row .md li { margin: 0.05em 0; }
|
|
.live .row .md blockquote {
|
|
margin: 0.2em 0;
|
|
padding-left: 0.6em;
|
|
border-left: 2px solid var(--purple-dim);
|
|
color: var(--muted);
|
|
}
|