Ports the row taxonomy in docs/terminal-rendering.md (app.js's
renderStream/renderRichToolUse/renderToolResult/renderTaskEvent) to
real Preact components + hooks:
- lib/streamRow.ts: plain-data StreamRow model (one row = one line/
panel), lib/classifyEvent.ts: raw stream-json event -> StreamRow[],
almost entirely dispatching on the backend's precomputed _icon/
_summary/_category/_body/_body_type fields, same as the old client.
- lib/markdown.ts, lib/linkify.tsx: sanitized-markdown + auto-link
helpers, same behavior as app.js's mdNode/terminal.js's linkify.
- hooks/useLiveStream.ts: backfill + SSE + seq-dedupe + coalescing,
reduced to a plain StreamRow[] — deliberately has no opinion on
scroll position, only on what rows exist.
- components/Row.tsx: renders one StreamRow (flat or details).
- components/LiveStream.tsx: owns the scrollable DOM node + a from-
scratch sticky-bottom implementation — not the old MutationObserver
+ rAF snap-animation + smoothScrollingUntil gate. stickToBottom is
plain state driven by the scroll handler; snapping is an instant
scrollTop write in a useLayoutEffect that runs after Preact has
already committed the new rows, so it always sees the real
scrollHeight. A load-older prepend is told apart from a normal
append via an explicit ref set right before calling loadMore(),
rather than inferred from DOM mutation shape after the fact.
Wired into Root.tsx below Header/StatusChips; turn_start/turn_end
also nudge useAgentState's refresh() for a snappier badge update than
the plain poll interval.
Reuses @hive/shared/terminal.css's existing row-kind classes as-is —
the taxonomy's visual language isn't what changed, the component
model underneath it is.