agent: LiveStream/Row/DetailsRow — Preact live event 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.
This commit is contained in:
parent
908f479372
commit
8521c6becb
9 changed files with 789 additions and 41 deletions
25
frontend/packages/agent/src/lib/markdown.ts
Normal file
25
frontend/packages/agent/src/lib/markdown.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Markdown → sanitized HTML, ported from app.js's `mdNode`. Message
|
||||
// bodies rendered into the live stream (assistant text, send/ask/answer/
|
||||
// recv payloads) are untrusted (peer-agent / matrix-relayed content,
|
||||
// agent-authored files) — `marked` itself no longer sanitizes (v5+
|
||||
// dropped the built-in sanitizer), so every parse is run through
|
||||
// DOMPurify before it's ever handed to `dangerouslySetInnerHTML`.
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
|
||||
const ESCAPE_RE = /[&<>"]/g;
|
||||
const ESCAPE_MAP: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"' };
|
||||
|
||||
/** Render `text` as sanitized markdown HTML. Falls back to escaped plain
|
||||
* text if `marked` throws (mirrors app.js's try/catch fallback). */
|
||||
export function renderMarkdown(text: string | null | undefined): string {
|
||||
const src = String(text ?? '');
|
||||
try {
|
||||
return DOMPurify.sanitize(marked.parse(src) as string);
|
||||
} catch (err) {
|
||||
console.warn('marked failed', err);
|
||||
return src.replace(ESCAPE_RE, (c) => ESCAPE_MAP[c] ?? c);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue