Moves the TermMsg rendering pipeline (Row.tsx, termMsg.ts, linkify.tsx,
markdown.ts) from @hive/agent into @hive/shared, so swarm-ui becomes a
second consumer of it instead of forking a copy -- CSS was already
shared (@hive/shared/terminal.css). marked+dompurify move from
@hive/agent's deps to @hive/shared's; swarm-ui picks them up
transitively, no new direct dep there.
New swarm-ui route /agents/:name/term (AgentTermPage), linked from
AgentsPage's detail panel via a "terminal" badge next to "link matrix
account". Consumes GET /api/agents/{name}/term/stream: unlike
@hive/agent's own useLiveStream (TermEnvelope-wrapped, history/backfill
dance), the swarm relay forwards one bare TermMsg per SSE event with no
envelope and no history endpoint -- useSwarmTermStream is a much
smaller hook for that shape (EventSource -> parse -> coalesce, nothing
to buffer/dedupe/backfill against).
Verified against a live SSE mock (screenshots in /agents/iris/state/screenshots/
3801-agents-detail-panel-terminal-badge.png and
3801-agent-term-page-live-rows.png -- real rows rendering through the
shared Row component, not just a build/typecheck pass).
30 lines
1.1 KiB
TypeScript
30 lines
1.1 KiB
TypeScript
// Markdown → sanitized HTML, ported from app.js's `mdNode`. Message
|
|
// bodies rendered into a live stream (assistant text, send/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);
|
|
}
|
|
}
|