hyperhive/frontend/packages/agent/src/agent.css
iris 6c74c6dafa feat(agent): model quick-picker in overflow menu
Adds three one-click model shortcuts (haiku / sonnet / opus) at the
bottom of the agent overflow menu (⋯), below a separator and a small
'model' section label.

- The active model alias is highlighted with .active (purple border +
  tint) so the operator can see at a glance which model is current.
  Matching tolerates both short aliases (haiku) and full API names
  (claude-3-5-haiku-20241022) via an endsWith check.
- Clicking the active model is a no-op (closes menu, no POST).
- Clicking a different alias calls postModel() and closes the menu.
- renderModelChip() keeps the picker buttons in sync on every
  /api/state refresh without rebuilding the menu.

Before this change the operator had to type /model <name> in the
compose box. The overflow menu provides a discoverability path for
operators who don't remember slash commands.
2026-06-05 13:14:17 +02:00

1162 lines
33 KiB
CSS

/* Shared Catppuccin palette + body typography + terminal pane styles.
Bundled in front of the agent-only rules below via esbuild. */
@import "@hive/shared/base.css";
@import "@hive/shared/terminal.css";
/* ─── full-screen layout overrides ─────────────────────────────────
The agent page mounts a full-viewport terminal under a fixed
frosted-glass header + composer pair. See docs/web-ui.md::Per-agent
page for the layer / pill / side-panel structure; rules below
override the in-page defaults from @hive/shared. */
:root {
/* 6em min-height accommodates the full-height agent icon (5em +
padding) alongside the two-row main column (title + nav-links
on top, state strip below). */
--agent-header-h: 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. screen-shell is also excluded — it
manages its own full-viewport layout below. */
body:not(.agent-shell):not(.screen-shell) {
max-width: 110em;
margin: 1.5em auto;
padding: 0 1.5em;
height: auto;
}
.banner {
text-align: center;
margin: 0 0 1em 0;
font-size: 0.95em;
overflow-x: auto;
background: linear-gradient(
90deg,
var(--purple-dim) 0%,
var(--purple) 50%,
var(--purple-dim) 100%
);
background-size: 200% 100%;
background-position: 50% 0;
-webkit-background-clip: text;
background-clip: text;
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);
}
.agent-header {
position: fixed;
top: 0;
left: 0;
right: 0;
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;
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);
}
/* Main column: title row on top, state strip below. Centred
vertically against the full-height icon on the left. */
.agent-header-main {
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.45em;
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 {
margin: 0;
line-height: 1;
}
.agent-nav {
display: flex;
flex-wrap: wrap;
gap: 0.4em 0.8em;
margin: 0;
}
.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 {
color: var(--purple);
text-transform: uppercase;
letter-spacing: 0.15em;
text-shadow: 0 0 8px rgba(203, 166, 247, 0.4);
}
.agent-icon {
/* Square identity anchor — explicit 5em sizing + align-self.
See docs/web-ui.md::Per-agent page (Agent icon) for the
intrinsic-dim-pushes-parent-flex-open + sticks-to-top
rationale. */
width: 5em;
height: 5em;
flex-shrink: 0;
align-self: flex-start;
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; 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. */
.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 + logout (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. See docs/web-ui.md::Per-agent page (Overflow button)
for the `:not([hidden])` scoping rationale. */
.overflow-menu:not([hidden]) {
display: flex;
flex-direction: column;
gap: 0.15em;
}
.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);
border-radius: 6px;
padding: 0.35em;
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;
}
.overflow-sep {
height: 1px;
background: var(--border);
margin: 0.3em 0.4em;
}
.overflow-section-label {
font-size: 0.72em;
letter-spacing: 0.1em;
color: var(--muted);
padding: 0 0.7em 0.1em;
text-transform: uppercase;
}
.overflow-item-model {
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.overflow-item-model:hover {
color: var(--fg);
background: rgba(203, 166, 247, 0.08);
border-color: var(--purple-dim);
}
.overflow-item-model.active {
color: var(--purple);
border-color: var(--purple-dim);
background: rgba(203, 166, 247, 0.06);
}
/* 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); }
.status-needs-login { color: var(--amber); text-shadow: 0 0 6px rgba(250, 179, 135, 0.55); }
code { background: rgba(203, 166, 247, 0.12); padding: 0.05em 0.3em; border-radius: 2px; }
a {
color: var(--cyan);
text-shadow: 0 0 4px rgba(137, 220, 235, 0.5);
}
a:hover { color: var(--fg); text-shadow: 0 0 12px rgba(137, 220, 235, 0.9); }
.btn {
font-family: inherit;
font-size: 1em;
background: var(--bg);
border: 1px solid var(--purple);
color: var(--purple);
padding: 0.25em 0.8em;
cursor: pointer;
letter-spacing: 0.1em;
}
.btn {
text-shadow: 0 0 4px currentColor;
transition: box-shadow 0.15s ease, text-shadow 0.15s ease;
}
.btn:hover {
background: rgba(205, 214, 244, 0.06);
text-shadow: 0 0 10px currentColor;
box-shadow: 0 0 10px -2px currentColor;
}
.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; }
/* Orphaned rules — left here as a tombstone so a search for the
class name finds them. Live consumers gone:
- `.btn-rebuild` was a per-agent header chip (covered now by
`.overflow-item-rebuild` in the overflow menu).
- `.btn-send` was a green send-button variant — the dashboard's
compose form that used it is retired. */
.sendform { display: flex; gap: 0.6em; margin-top: 0.5em; }
.sendform input {
font-family: inherit; font-size: 1em;
background: rgba(255, 255, 255, 0.04);
color: var(--fg);
border: 1px solid var(--purple-dim);
padding: 0.4em 0.6em;
flex: 1;
}
.sendform input:focus { outline: 1px solid var(--purple); }
.loginform { display: flex; gap: 0.6em; margin-top: 0.5em; align-items: stretch; }
.loginform input {
font-family: inherit; font-size: 1em;
background: rgba(255, 255, 255, 0.04);
color: var(--fg);
border: 1px solid var(--purple-dim);
padding: 0.4em 0.6em;
flex: 1;
}
.loginform input:focus { outline: 1px solid var(--purple); }
/* Show / hide toggle for the masked OAuth-code input. Quiet by
default (muted border + transparent bg), lights amber on hover
/ when pressed (`aria-pressed="true"`) so the operator sees at
a glance whether the code is currently visible. */
.loginform-reveal {
font-family: inherit;
font-size: 1em;
background: transparent;
color: var(--muted);
border: 1px solid var(--purple-dim);
border-radius: 3px;
padding: 0 0.6em;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.loginform-reveal:hover,
.loginform-reveal:focus-visible {
color: var(--amber);
border-color: var(--amber);
outline: none;
}
.loginform-reveal[aria-pressed="true"] {
color: var(--amber);
border-color: var(--amber);
box-shadow: 0 0 8px -2px var(--amber);
}
pre.diff {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--purple-dim);
padding: 0.6em 0.8em;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
max-height: 30em;
}
/* 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. */
.agent-inbox {
margin: 0.4em 0;
font-size: 0.85em;
color: var(--muted);
}
.agent-inbox > summary {
cursor: pointer;
letter-spacing: 0.05em;
list-style: none;
}
.agent-inbox > summary::marker { content: ''; }
.agent-inbox[open] > summary > span::before { content: ''; }
.agent-inbox ul {
list-style: none;
padding: 0.4em 0.8em;
margin: 0.3em 0 0;
background: rgba(255, 255, 255, 0.02);
border-left: 2px solid var(--purple-dim);
max-height: 16em;
overflow-y: auto;
}
/* Inbox / loose-ends rows: header (from / sep / ts) on one line,
body on its own line below — gives the body the full panel width
instead of squeezing it into a fourth grid column that wrapped
long messages over many narrow lines. */
.agent-inbox li {
padding: 0.4em 0;
display: block;
}
.agent-inbox .inbox-ts { color: var(--muted); font-size: 0.9em; margin-left: 0.5em; }
.agent-inbox .inbox-from { color: var(--amber); }
.agent-inbox .inbox-sep { color: var(--muted); margin-left: 0.4em; }
.agent-inbox .inbox-body {
display: block;
color: var(--fg);
white-space: pre-wrap;
word-break: break-word;
margin-top: 0.3em;
padding-left: 0.8em;
border-left: 2px solid var(--purple-dim);
}
.agent-inbox li.inbox-reply {
padding-left: 1em;
border-left: 2px solid var(--border);
margin-left: 0.4em;
}
.agent-inbox .inbox-reply-tag { color: var(--muted); font-size: 0.85em; }
.agent-inbox .answer-form {
/* Block-level under the new layout — `grid-column: 1 / -1` was
for the legacy grid; under block layout the form naturally
starts on its own row. */
display: flex;
gap: 0.4em;
align-items: flex-start;
margin-top: 0.5em;
padding-left: 0.8em;
}
.agent-inbox .answer-form textarea {
flex: 1;
font-family: inherit;
font-size: inherit;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0.3em;
resize: vertical;
}
.agent-inbox .answer-form button {
font-family: inherit;
font-size: inherit;
background: var(--bg-elev);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0.3em 0.7em;
cursor: pointer;
white-space: nowrap;
}
.agent-inbox .answer-form button:hover:not(:disabled) {
border-color: var(--purple);
color: var(--purple);
}
.agent-inbox .answer-form button:disabled { opacity: 0.5; cursor: default; }
.agent-inbox .answer-status { color: var(--muted); align-self: center; }
/* Inline answer slot mounted under each `ask → operator` row in
the live terminal stream. Mirrors the side-panel `.answer-form`
look-and-feel — same textarea, same send button — so the operator
doesn't have to context-switch between "answering in the panel"
and "answering inline". Empty slot collapses to nothing (no
margin) so pre-bind rows stay tidy; populated slot gets a thin
top divider to separate the question body from the form. A
`[resolved]` tag (neutral, covers answered / cancelled /
TTL-expired uniformly) replaces the form once the question
leaves the pending list. */
.live .ask-answer-inline-slot:empty { display: none; }
.live .ask-answer-inline-slot {
margin-top: 0.5em;
padding-top: 0.5em;
border-top: 1px dashed var(--border);
}
.live .ask-answer-inline-slot .answer-form {
display: flex;
gap: 0.4em;
align-items: flex-start;
}
.live .ask-answer-inline-slot .answer-form textarea {
flex: 1;
font-family: inherit;
font-size: inherit;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0.3em;
resize: vertical;
}
.live .ask-answer-inline-slot .answer-form button {
font-family: inherit;
font-size: inherit;
background: var(--bg-elev);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0.3em 0.7em;
cursor: pointer;
white-space: nowrap;
}
.live .ask-answer-inline-slot .answer-form button:hover:not(:disabled) {
border-color: var(--purple);
color: var(--purple);
}
.live .ask-answer-inline-slot .answer-form button:disabled {
opacity: 0.5;
cursor: default;
}
.live .ask-answer-inline-slot .answer-status {
color: var(--muted);
align-self: center;
}
.live .ask-answer-inline-slot .ask-resolved-tag {
color: var(--muted);
font-style: italic;
text-decoration: line-through;
text-decoration-color: var(--muted);
}
/* "mark all read" header row sits above the recent-messages list
in the inbox side-panel flyout. Same look as the answer-form
button (mauve hover, bg-elev background) so they read as part
of the same affordance family. */
.agent-inbox .inbox-mark-all-row {
display: flex;
gap: 0.6em;
align-items: center;
padding: 0.4em 0.2em 0.6em;
margin-bottom: 0.5em;
border-bottom: 1px solid var(--border);
}
.agent-inbox .inbox-mark-all-btn {
font-family: inherit;
font-size: inherit;
background: var(--bg-elev);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0.3em 0.7em;
cursor: pointer;
white-space: nowrap;
}
.agent-inbox .inbox-mark-all-btn:hover:not(:disabled) {
border-color: var(--purple);
color: var(--purple);
}
.agent-inbox .inbox-mark-all-btn:disabled {
opacity: 0.5;
cursor: default;
}
.agent-inbox .inbox-mark-status {
color: var(--muted);
font-size: 0.9em;
}
.last-turn {
color: var(--muted);
font-size: 0.8em;
letter-spacing: 0.05em;
}
.model-chip {
display: inline-block;
padding: 0.1em 0.6em;
border: 1px solid var(--purple-dim);
border-radius: 999px;
color: var(--cyan);
font-size: 0.78em;
letter-spacing: 0.04em;
}
/* Context-window badge. Mirrors Claude Code's bottom-right "N tokens"
chip — single primary number (total prompt tokens in use), full
breakdown on hover. Sized/coloured like a peer of model-chip so
the state row reads as one row of chrome. */
.ctx-badge {
display: inline-block;
padding: 0.1em 0.6em;
border: 1px solid var(--purple-dim);
border-radius: 999px;
color: var(--green);
font-size: 0.78em;
letter-spacing: 0.04em;
cursor: default;
white-space: pre-line;
}
/* Harness reachability badge. Same chip shape + sizing as
`.state-badge` / `.model-chip` so the state row stays visually
uniform; colour communicates the actual reachability state. */
.status-badge {
display: inline-block;
padding: 0.25em 0.8em;
border: 1px solid;
border-radius: 999px;
font-size: 0.85em;
letter-spacing: 0.05em;
}
.status-badge.status-loading { color: var(--muted); border-color: var(--purple-dim); }
.status-badge.status-online { color: var(--green); border-color: var(--green);
text-shadow: 0 0 6px rgba(166, 227, 161, 0.55); }
.status-badge.status-rate-limited { color: var(--red); border-color: var(--red);
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 tombstone — `.btn-dashlink` chip that lived beside the
title was moved into the overflow menu (`.overflow-item-dashboard`
covers it now). */
.btn-cancel-turn {
font-family: inherit;
font-size: 0.8em;
letter-spacing: 0.08em;
background: transparent;
color: var(--red);
border: 1px solid var(--red);
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-cancel-turn:hover {
background: rgba(243, 139, 168, 0.1);
box-shadow: 0 0 10px -2px currentColor;
}
/* Orphaned tombstone — `.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). */
.state-badge {
display: inline-block;
padding: 0.25em 0.8em;
border: 1px solid;
border-radius: 999px;
font-size: 0.85em;
letter-spacing: 0.05em;
transition: color 280ms ease, border-color 280ms ease,
box-shadow 280ms ease, background 280ms ease;
}
.state-badge.state-loading {
color: var(--muted); border-color: var(--purple-dim);
}
.state-badge.state-offline {
color: var(--muted); border-color: var(--muted);
}
.state-badge.state-idle {
color: var(--cyan); border-color: var(--cyan);
text-shadow: 0 0 6px rgba(137, 220, 235, 0.55);
}
.state-badge.state-thinking {
color: var(--amber); border-color: var(--amber);
text-shadow: 0 0 6px rgba(250, 179, 135, 0.65);
animation: badge-pulse 1.8s ease-in-out infinite;
}
.state-badge.state-compacting {
color: var(--purple); border-color: var(--purple);
text-shadow: 0 0 6px rgba(203, 166, 247, 0.65);
animation: badge-pulse 1.8s ease-in-out infinite;
}
.state-badge.state-just-changed {
animation: state-flash 600ms ease-out;
}
@keyframes state-flash {
0% { box-shadow: 0 0 0 0 currentColor, 0 0 0 0 currentColor; }
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. z-index bumped above
the composer (z-30) so the pill sits on the top layer instead of
being clipped by the floating chrome. (Pill is anchored in
`.agent-main` rather than `.terminal-wrap` — see
docs/web-ui.md::Per-agent page Terminal-wrap paragraph for the
backdrop-filter stacking-context rationale.) */
.agent-main .tail-pill {
bottom: calc(var(--agent-composer-h) + 0.6em);
z-index: 35;
}
/* 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;
}
.term-input .prompt, .term-input .submit-hint {
padding-top: 0.25em;
}
.term-input .prompt {
color: var(--green);
text-shadow: 0 0 6px rgba(166, 227, 161, 0.6);
user-select: none;
flex: 0 0 auto;
}
.term-input textarea {
flex: 1;
background: transparent;
border: 0;
outline: 0;
color: var(--fg);
font-family: inherit;
font-size: 1em;
padding: 0.2em 0;
caret-color: var(--green);
resize: none;
overflow-y: auto;
line-height: 1.4;
min-height: 1.4em;
}
.term-input textarea::placeholder { color: var(--muted); }
.term-input .submit-hint { color: var(--muted); font-size: 0.8em; flex: 0 0 auto; }
.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.
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;
}
/* Hive identity row — shows "swarm / hive" label beneath the title.
Hidden until JS populates from /api/state (hive_name + swarm_name).
Mirrors the `.banner-thin` style in dashboard.css but kept tight
(no letter-spacing, smaller text) so it reads as a sub-label. */
.agent-hive-row {
/* Omit the default row gap when the row is hidden to avoid a
blank gap between the title row and the state row. */
gap: 0;
}
.agent-hive-label {
font-size: 0.72em;
letter-spacing: 0.1em;
color: var(--purple-dim);
text-transform: uppercase;
}
/* ─── /stats page ────────────────────────────────────────────────────────── */
.stats-nav {
display: flex;
gap: 0.75rem;
align-items: baseline;
margin-bottom: 0.5rem;
}
.stats-nav h2 { margin: 0; }
.stats-nav a { color: var(--cyan); text-decoration: none; }
.stats-nav a:hover { text-decoration: underline; }
.window-tabs {
display: flex;
gap: 0.4rem;
margin: 0.5rem 0 1rem;
}
.window-tabs button {
background: var(--bg-elev);
color: var(--fg);
border: 1px solid var(--border);
padding: 0.3rem 0.8rem;
font-family: inherit;
cursor: pointer;
}
.window-tabs button.active {
background: var(--purple-dim);
border-color: var(--purple);
color: var(--purple);
}
.summary {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.summary .chip {
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
background: var(--bg-elev);
border: 1px solid var(--border);
padding: 0.5rem 0.9rem;
border-radius: 4px;
box-sizing: border-box;
min-width: 9rem;
height: 3.4rem;
line-height: 1.2;
}
.summary .chip .label {
color: var(--muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.summary .chip .value {
color: var(--cyan);
font-weight: bold;
font-size: 1.05rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
gap: 1rem;
}
.stats-card {
background: var(--bg-elev);
border: 1px solid var(--border);
padding: 0.75rem 1rem 1rem;
border-radius: 4px;
}
.stats-card h3 {
margin: 0 0 0.5rem;
color: var(--purple);
font-size: 0.95rem;
font-weight: normal;
}
.stats-card .chart-wrap { position: relative; height: 220px; }
.stats-card.wide { grid-column: 1 / -1; }
.stats-card.wide .chart-wrap { height: 260px; }
.stats-empty-note { color: var(--muted); font-style: italic; }
/* ─── /screen page ─── */
/* Full-screen VNC canvas viewer (screen.html). All rules scoped to
body.screen-shell so they can coexist with the agent/stats rules. */
body.screen-shell {
font-size: 14px;
overflow: hidden;
}
/* Toolbar strip at the top of the screen page. */
#toolbar {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.4rem 0.75rem;
background: var(--bg-elev);
border-bottom: 1px solid var(--border);
}
#toolbar a {
color: var(--blue);
text-decoration: none;
font-size: 0.85rem;
}
#toolbar a:hover { text-decoration: underline; }
/* Toolbar toggle buttons (fit / match-size / debug). */
.tbtn {
padding: 0.15rem 0.5rem;
font-size: 0.72rem;
font-family: inherit;
background: var(--border);
color: var(--subtext0);
border: 1px solid var(--purple-dim);
border-radius: 4px;
cursor: pointer;
}
.tbtn.active { color: var(--green); border-color: var(--green); }
.tbtn:disabled { opacity: 0.4; cursor: default; }
/* Connection status chip at the right end of the toolbar. */
#status {
margin-left: auto;
font-size: 0.75rem;
color: var(--subtext0);
}
#status.connected { color: var(--green); }
#status.error { color: var(--red); }
/* RFB debug log — fixed panel at the bottom, hidden by default. */
#debug-log {
position: fixed;
bottom: 0; left: 0; right: 0;
max-height: 40vh;
overflow-y: auto;
background: rgba(17, 17, 27, 0.95);
border-top: 1px solid var(--purple-dim);
font-size: 0.72rem;
font-family: ui-monospace, monospace;
padding: 0.4rem 0.6rem;
z-index: 100;
display: none; /* toggled by the debug button */
}
#debug-log .dbg-line { color: var(--subtext0); margin: 1px 0; white-space: pre; }
#debug-log .dbg-line.err { color: var(--red); }
#debug-log .dbg-line.ok { color: var(--green); }
#debug-log .dbg-line.send { color: var(--blue); }
/* Canvas container — centres the VNC framebuffer. */
#canvas-wrap {
display: flex;
justify-content: center;
align-items: flex-start;
width: 100%;
height: calc(100% - 36px);
overflow: auto;
background: var(--crust);
}
/* Fit mode: canvas scaled in JS to fill the container. */
#canvas-wrap.fit { align-items: center; overflow: hidden; }
#canvas-wrap canvas { display: block; cursor: default; }
/* Pin the canvas to the JS-calculated size; prevents flex-item
min-width:auto from silently expanding beyond the scaled size. */
#canvas-wrap.fit canvas { flex: none; min-width: 0; min-height: 0; }
/* Transient flash message (copy-text, disconnect, etc.). */
#msg {
position: fixed;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
background: var(--border);
color: var(--yellow);
border-radius: 6px;
padding: 0.4rem 0.9rem;
font-size: 0.8rem;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}