Ported from app.js's `renderTermInput`/`handleSlashCommand`/
`completeSlash`: prompt + auto-growing textarea, Enter sends (Shift+Enter
newline), Tab cycles slash-command completion. Same command set
(/help, /clear, /cancel, /compact, /model, /effort, /new-session,
/logout), same routes (`api/cancel`, `api/compact`, `api/new-session`,
`api/logout`, `send`) via new `lib/termActions.ts` (same `{ok, detail}`
shape as modelEffort.ts's POST helper).
One deliberate UX change: `/new-session` and `/logout` used to pop the
old shadow-DOM `themedConfirm` modal before firing — this rewrite's
destructive actions all avoid that widget family already (SidePanel,
useConfirmClick), and a modal doesn't fit a text-input flow anyway.
Typing the command once arms it (a local note explains what confirming
does); typing it again fires it — a keyboard-native two-step confirm.
`/help`/`/clear` need to reach into LiveStream's row list (local-only
echo rows, never sent anywhere) without lifting that state up to Root —
`useLiveStream` gained `pushLocalNote`/`clearLocal`, exposed off
`LiveStream` via `forwardRef`+`useImperativeHandle` (preact/compat),
same shape as app.js's old `termAPI` object but scoped as a ref handle
instead of a module-level variable.
Screenshot-verified end to end: typed "/help" + Enter into a real
mounted composer, confirmed the textarea clears and the local note rows
(command list) append to the live pane.
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.