agent/app.js + docs: migrate stacking-context + OAuth + nav-link prose to docs (#713 batch 1)

First batch of #713 (agent per-container UI prose migration).
Moves 3 substantive WHY-explanations from app.js into
docs/web-ui.md::Per-agent page, replaces each in-code mention
with a brief pointer.

Moved to docs:
- **Terminal-wrap pill anchor** (~11 lines): why the `↓ N new`
  pill is anchored in `.agent-main` rather than the default
  `.terminal-wrap` parent — backdrop-filter creates a stacking
  context, anchoring inside it traps the pill's z-index below the
  composer.
- **OAuth code input** (~14 lines across two blocks): masked
  password + reveal toggle + `autocomplete="one-time-code"`
  rationale (shoulder-surfer / screenshot exposure + WHATWG
  semantic value + suppresses browser save-password prompt).
- **NavLink kind resolution + XSS-safe DOM-build** (~8 lines):
  Container → same-origin, Forge → `http://<host>:3000<url>`,
  External → already absolute; agent-declared strings never reach
  innerHTML.

Collapsed in app.js: each block becomes a 3-4 line pointer to the
matching docs section. Drops #262 / #568 / #592 cookies en passant
since their substance now lives in docs. The `#14` cookie on
`historyUrl` / `streamUrl` is also scrubbed — the path-relative
shape is the convention, not an open issue. `#375` (agent.css ID
ref) drops as part of the pill-anchor block collapse since it was
sole-purpose pointing inside that comment.

app.js: 27 → 24 refs. Net ~35 lines of substantive prose moved out
of app.js into docs/web-ui.md::Per-agent page, where it belongs.
This commit is contained in:
iris 2026-05-31 14:28:46 +02:00 committed by mara
commit 8366c3739b
2 changed files with 39 additions and 35 deletions

View file

@ -823,7 +823,13 @@ through. Three flex columns (#394 redesign):
agent has a forge account; any `hyperhive.dashboardLinks` extras
(`kind = External`). A `↑ dashboard` link is prepended by the JS
so the host dashboard is one click away. `GET /api/agent/{name}/links`
is the single source of truth.
is the single source of truth. Each `NavLink.kind` resolves
differently in the frontend: `Container` → same-origin path
(the agent page is itself container-local); `Forge`
`http://<host>:3000<url>`; `External` → already absolute.
All anchors are built via `el()` — agent-declared icon /
label / url strings never reach `innerHTML` (XSS-safe by
construction).
- Row 2 (`.agent-state-row`): alive badge + state badge + model chip
+ ctx badge + cost badge + last-turn chip + cancel button.
- Alive badge: `● alive` (green) / `⊘ rate limited` (red) /
@ -871,11 +877,26 @@ header `<h2 id="title">` stays short (#589 phase A).
and scrolls behind the fixed header + footer.
- `#status` overlay: empty when online; shows the login form / OAuth
URL when `status` is `needs_login_*`. The OAuth code input is
`type="password"` with a `👁 reveal` toggle (#568 — avoids
accidental on-screen token exposure; `autocomplete="one-time-code"`
for password-manager suppression).
`type="password"` with a `👁 reveal` toggle that flips it back to
`text` on press so the operator can sanity-check the paste before
submit — avoids accidental on-screen token exposure to
shoulder-surfers or screenshots. `autocomplete="one-time-code"`
is the semantic value for OAuth codes (per WHATWG): browsers may
silently ignore `autocomplete="off"` on `type="password"`, but
`one-time-code` is honoured and suppresses the "save password
for this site?" prompt that would otherwise fire on submit.
- Terminal-wrap: live event tail (sticky-bottom auto-scroll +
`↓ N new` pill when not at bottom).
`↓ N new` pill when not at bottom). The pill is **anchored in
`.agent-main`**, not in `log.parentElement = .terminal-wrap`:
`.terminal-wrap` applies `backdrop-filter: blur` for the frost
effect, which creates a CSS stacking context — anchoring the
pill inside that context would trap its `z-index` below the
fixed composer in the root stacking context, and it'd never
float. `.agent-main` has no backdrop-filter (no stacking-context
creators), so the pill's `z-index` reaches the root and properly
composites above the composer. Geometry is unchanged —
`.agent-main` and `.terminal-wrap` both `inset: 0` fill the same
area.
**Fixed-overlay footer** (`<footer class="agent-composer">`): frosted
glass, symmetric with the header. Contains the operator-input

View file

@ -360,20 +360,15 @@ window.marked = marked;
const code = el('form', {
action: 'login/code', method: 'POST', class: 'loginform', 'data-async': '',
});
// #568: OAuth code is a sensitive secret — mask the input with
// type="password" so a shoulder-surfer / screenshot doesn't
// capture it. The reveal button flips it back to text on press
// so the operator can sanity-check the paste before submit.
// OAuth code input — masked password + reveal toggle, semantic
// autocomplete. See docs/web-ui.md::Per-agent page (#status
// overlay) for the shoulder-surfer + WHATWG one-time-code
// rationale.
const codeInput = el('input', {
name: 'code',
type: 'password',
placeholder: 'paste OAuth code here (hidden)',
required: '',
// `one-time-code` is the semantic value for OAuth codes (per
// WHATWG / argus #592 review): browsers may silently ignore
// `autocomplete="off"` on `type="password"` inputs, but
// `one-time-code` is honoured + suppresses the "save password
// for this site?" prompt that would otherwise fire on submit.
autocomplete: 'one-time-code',
spellcheck: 'false',
});
@ -1091,14 +1086,9 @@ window.marked = marked;
const s = await resp.json();
if (!headerSet) { setHeader(s.label, s.qualified_label, s.dashboard_port); headerSet = true; }
currentLabel = s.label;
// Render server-supplied navigation links — stats, screen, the
// forge profile, the agent-configs mirror, plus any
// agent-declared `dashboardLinks` extras (issue #262). Each
// NavLink's `kind` says how to resolve `url`: Container →
// same-origin path (the agent page is itself container-local);
// Forge → `http://<host>:3000<url>`; External → already
// absolute. DOM-built via el() — agent-declared icon / label /
// url strings must NEVER reach innerHTML.
// Render server-supplied navigation links — see
// docs/web-ui.md::Per-agent page (meta-nav) for the NavLink
// kind → URL resolution table + XSS-safe DOM-build rationale.
const metaLinks = $('meta-links');
if (metaLinks && Array.isArray(s.links)) {
metaLinks.replaceChildren();
@ -1506,20 +1496,13 @@ window.marked = marked;
const term = HiveTerminal.create({
logEl: log,
// Anchor the `↓ N new` pill in `.agent-main` (NOT the default
// `log.parentElement` = `.terminal-wrap`). `.terminal-wrap`
// applies `backdrop-filter` for the frost effect, which
// creates a CSS stacking context — the pill's z-index 35
// (agent.css #375) then gets TRAPPED inside that context and
// can't compete with the fixed composer's z-index 30 in the
// root stacking context. Anchoring in `.agent-main` (no
// backdrop-filter, no other stacking-context creators) lets
// the pill's z-index reach the root and properly float above
// the composer. Geometry unchanged — `.agent-main` and
// `.terminal-wrap` both `inset: 0` fill the same area.
// Anchor the `↓ N new` pill in `.agent-main` rather than the
// default `.terminal-wrap` parent — see docs/web-ui.md::Per-agent
// page (Terminal-wrap) for the backdrop-filter stacking-context
// gotcha.
pillAnchor: $('agent-main'),
// Path-relative so the page mounted under a nginx prefix
// (e.g. /agent/<name>/) still hits the right SSE upstream (#14).
// Path-relative URLs so the page mounted under a nginx prefix
// (e.g. /agent/<name>/) still hits the right SSE upstream.
historyUrl: 'events/history',
streamUrl: 'events/stream',
renderers: {