shared terminal + docs: migrate sticky-scroll + backfill prose (#714 batch 1)
Substantial prose migration from @hive/shared/terminal.js (the shared HiveTerminal factory backing #msgflow + #live across both dashboard and per-agent UIs) into a new docs subsection. Added to docs/web-ui.md as a new ### Shared terminal pane subsection under ## Shape (shared by both) — ~78 lines of new substantive prose: - **api shape**: row / details / detailsDiff factory contract - **Sticky-bottom + snap animation**: stickToBottom semantics + 140ms ease-out vs 500ms browser default + 24px short-circuit; per-frame target re-eval extends destination through mid-animation mutations - **Mid-animation scroll-event guard**: smoothScrollingUntil timestamp swallows the rAF-driven scroll events so the eased positions don't flip stickToBottom false partway - **Post-append MutationObserver**: catches renderer mutations after api.row returns (badges, multi-line bodies, tool panes) + why programmatic scrollTop writes don't feedback-loop - **Backfill + SSE**: history/stream envelope shape (seq, events), kind-aware seq dedupe at the boundary, .no-anim during replay, optional streamFactory for SharedWorker integration - **linkify**: text-node-only autolink, XSS-safe by construction, trailing-punctuation strip Collapsed in terminal.js (cookies en passant): - #400 (snap animation timing — closed) × 3 → docs pointers - #393 (post-append MutationObserver — closed) × 1 → docs pointer - #375 (pre-append nearBottom snapshot — closed) × 1 → docs pointer - #448 (streamFactory SharedWorker hook — closed) × 1 → docs pointer - #163 (seq dedupe + onStreamOpen resync — closed) × 2 → drop cookies; substance lives in docs - #233 (linkify) × 1 → docs pointer + terminal.css cookie scrub terminal.js: 8 → 0 #NNN cookies (100% reduction). terminal.css: 1 → 0 issue-ref cookies (remaining 1 match is a hex color literal). Net effect: ~50 lines of substantive WHY-prose moved out of shared frontend into docs/web-ui.md, where it documents the factory's contract for both consumer pages.
This commit is contained in:
parent
b15c534e67
commit
f60a90d752
3 changed files with 110 additions and 71 deletions
|
|
@ -72,6 +72,84 @@ since #753). Both are SPAs — `GET /` returns a static shell,
|
|||
bfcache-restore uses). Recovery is per-tab; pings are
|
||||
invisible on the healthy path.
|
||||
|
||||
### Shared terminal pane
|
||||
|
||||
Both surfaces' scrollable log streams (`#msgflow` on the dashboard,
|
||||
`#live` on the per-agent page) are backed by the shared terminal
|
||||
factory in `@hive/shared/terminal.js`. The factory wires up
|
||||
sticky-bottom auto-scroll, a "↓ N new" pill, history backfill, and
|
||||
SSE replay. Pages register a `kind → renderer` map; unknown kinds
|
||||
fall through to a JSON-dump note row. The factory ships three row
|
||||
shapes the renderers call:
|
||||
|
||||
- `api.row(cls, text)` — single-line row with an inline `linkify`
|
||||
pass over the text.
|
||||
- `api.details(cls, summary, body)` — collapsible `<details>` with
|
||||
a `<pre>` body (used by long tool-results and stack traces).
|
||||
- `api.detailsDiff(cls, summary, body)` — same shape, splits the
|
||||
body on newlines and tags each line as `diff-add` / `diff-del` /
|
||||
`diff-ctx` so the renderer's diff bodies get coloured without
|
||||
emitting raw HTML.
|
||||
|
||||
**Sticky-bottom + snap animation.** `stickToBottom` is the
|
||||
operator's intent: true means "keep snapping to bottom on every
|
||||
mutation", false means "I scrolled up, leave me alone". The flag
|
||||
flips when a scroll event lands further than `NEAR_BOTTOM_PX = 48`
|
||||
from the bottom. New rows then either snap to bottom (when sticky)
|
||||
or bump the unseen-count and surface the "↓ N new" pill. The snap
|
||||
is a brief 140ms ease-out (`SCROLL_ANIM_MS`) — the browser's
|
||||
default `behavior: 'smooth'` ~500ms reads as "still smooth, but
|
||||
visibly slow"; 140ms feels snap-y while still reading as motion
|
||||
rather than a jump. Distances under `SCROLL_SNAP_PX = 24`
|
||||
short-circuit to instant — animating a 12px nudge would just be
|
||||
jitter. Each new snap cancels the previous `requestAnimationFrame`
|
||||
so a burst of mutations coalesces into one ride to the latest
|
||||
bottom; the per-frame step re-reads `scrollHeight - clientHeight`
|
||||
so mutations landing mid-animation extend the destination smoothly
|
||||
rather than land short.
|
||||
|
||||
**Mid-animation scroll-event guard.** The scroll handler's
|
||||
`isNearBottom` check would flip `stickToBottom` false mid-snap as
|
||||
the smooth animation eases through positions that are technically
|
||||
"not near bottom yet", which would strand the operator partway. A
|
||||
`smoothScrollingUntil` timestamp gates the scroll handler — set to
|
||||
the animation end + ~80ms headroom, re-armed on each fresh snap.
|
||||
Programmatic `scrollTop` writes (the animation's per-frame update)
|
||||
fire scroll events that the gate swallows.
|
||||
|
||||
**Post-append `MutationObserver`.** Renderers commonly call
|
||||
`api.row(cls, text)` to create the row shell then append more
|
||||
children (badges, multi-line bodies, tool result panes) after the
|
||||
factory returned. The initial sticky-snap fires off the row's
|
||||
empty shape; the renderer's later appends grow the row past the
|
||||
visible bottom. A `MutationObserver` on the log subtree fires once
|
||||
per microtask after each batch of synchronous mutations and snaps
|
||||
again when `stickToBottom` is true. Programmatic `scrollTop`
|
||||
writes don't re-trigger the observer (scroll isn't a DOM
|
||||
mutation), so no feedback loop. The pre-append
|
||||
`nearBottomBeforeAppend` snapshot is still useful — it keeps the
|
||||
initial visual lag to one frame instead of one microtask + frame.
|
||||
|
||||
**Backfill + SSE.** Cold load fetches `historyUrl` (replay), then
|
||||
subscribes to `streamUrl` (live tail). Both endpoints return
|
||||
`{ seq, events }` so the client can dedupe — events with
|
||||
`seq <= snapshot.seq` from the SSE stream are dropped silently
|
||||
(the snapshot already covers them). History rows render with a
|
||||
`.no-anim` class so they don't stagger in like live events. The
|
||||
optional `streamFactory(url)` callback lets the dashboard hand
|
||||
the factory a `SharedWorker`-backed `EventSource` facade (so
|
||||
multiple tabs share one upstream connection — see *SSE
|
||||
multiplexing* above); when omitted, the factory falls back to a
|
||||
plain `new EventSource(url)`.
|
||||
|
||||
**`linkify` (text-node based).** Bare `http(s)://` URLs in row
|
||||
text get wrapped in `<a target="_blank" rel="noopener noreferrer">`
|
||||
inside a fresh text node, so the autolinker never touches
|
||||
`innerHTML` and untrusted row content can't smuggle markup. The
|
||||
trailing-punctuation strip keeps `.,;:` outside the link surface.
|
||||
Markdown bodies go through `marked` separately and get the same
|
||||
target rewrite.
|
||||
|
||||
The JS app handles all `form[data-async]` submissions via a delegated
|
||||
listener: read `data-confirm`, swap the button to a spinner, POST
|
||||
`application/x-www-form-urlencoded`, re-enable the button on success
|
||||
|
|
|
|||
Loading…
Reference in a new issue