MalloryEstate Briefing
M·021Design draft
← All briefings

Rich Feed

Rich Feed 2d — Diagrams & Widgets

Mermaid flow/sequence diagrams, read-only widget cards, deep-link launchers, and bar/line charts — one lazy-loaded, sanitised extension to the 2a frame.

Dated 2026-06-13Programme Rich Feed

Situation

2a's parseChips model — a trailing fence Mallory attaches, toSpeech already strips — extends to a full family of structured visual blocks: mermaid diagrams (flowchart/sequence only), read-only widget cards, deep-link launchers, and (added mid-build) bar/line charts. Widgets, launchers, and charts are JSON rendered by typed Svelte components with text interpolation only — no {@html}; mermaid is the one exception, rendered through strict-mode SVG passed through an extended DOMPurify profile.

The entire wave is client-side. The Worker's only change is a persona-prompt string; blocks ride inside the existing content string with no stream/DB/route change, preserving the edge-local invariant.

Key decisions

One parser, extended

parseBlocks peels all recognised structured fences (mermaid/widget/launch/chart) in document order after parseChips runs; blocks render appended after the prose, not spliced in-place.

Mermaid is lazy and sandboxed

await import('mermaid') only on first diagram render (never in the main bundle); securityLevel:'strict' disables HTML labels/click directives, and the rendered SVG is passed through a DOMPurify SVG profile forbidding script/foreignObject/event handlers before insertion.

Widgets/launchers are data, not HTML

Both are JSON parsed into typed Svelte components using text interpolation only, so the injection surface is just escaped text plus a validated launcher href.

Launcher targets are allow-listed

A launcher renders only for an internal route, obsidian:, tel:, or mailto: target (isLaunchTarget); anything else — including external http(s) — is filtered out rather than rendered as a dead button.

Invalid-but-recognised blocks are dropped, unrecognised fences pass through

A malformed widget/launch/mermaid/chart fence is dropped silently (never dumps raw JSON into prose); an unrelated fence like ``python`` is left untouched and renders as an ordinary code block.

Chart added mid-build (Amendment)

The original draft deferred bar/line charts to 2e; Alex requested they ship in 2d instead — added as a hand-rolled inline-SVG ChartBlock with no new dependency.

Architecture

A pure multi-fence parser feeds five purpose-built Svelte renderers; only mermaid needs a sandbox because only mermaid renders untrusted markup as SVG.

web/src/lib/replyBlocks.ts (parseBlocks)
Pure extraction of mermaid/widget/launch/chart fences into a typed FeedBlock union, in document order, after parseChips.
web/src/lib/markdown.ts (sanitizeSvg)
DOMPurify SVG profile forbidding script/foreignObject/event-handler attributes, applied to mermaid's rendered output before insertion.
MermaidBlock.svelte
Lazy-imports mermaid on mount, renders strict-mode SVG through sanitizeSvg; quiet placeholder while loading, fenced-source fallback on failure.
WidgetCard.svelte
Titled key/value card with an optional status dot; pure text interpolation, no {@html}.
LaunchButtons.svelte
Real focusable <button>/<a> per allow-listed target with discernible labels and visible focus states.
ChartBlock.svelte
Hand-rolled inline SVG bar/line chart from a validated {type,title,series} payload — no new dependency.
src/persona.ts
Sparing permission to attach a diagram/widget/launcher/chart and announce it in prose — the only Worker-side change.

Risks & guardrails

Diagram source as an injection vector

Mermaid source is model-authored but treated as untrusted by policy — strict mode plus a DOMPurify SVG pass defends against a compromised or replayed reply becoming a script-execution path.

Bundle cost

Mermaid (~500KB+) is dynamically imported only on first diagram render, and the resulting async chunk is auto-precached by the existing service worker, so most replies and offline cold-starts are unaffected.

Dead or booby-trapped launcher buttons

Failing entries are filtered out rather than rendered; a launch block with no valid targets left is dropped entirely.

Diagram accessibility

Rendered SVGs get role=img and an aria-label from an optional %% alt: comment or a generic fallback — flagged as an open question for richer alt text in a future pass.

Delivery

Pure block parsing

parseBlocks + FeedBlock union + isLaunchTarget, unit-tested for extraction, validation, ordering, and chip composition.

SVG sanitiser + lazy mermaid dependency

sanitizeSvg added to markdown.ts; mermaid added as a lazy dynamic-import dependency only.

Block components

MermaidBlock, WidgetCard, LaunchButtons (and ChartBlock per the amendment) built and wired.

Feed + persona wiring

Blocks rendered in document order after the prose in +page.svelte; one persona bullet permitting sparing use.

Out of scope

Mermaid types beyond flowchart/sequenceInteractive/stateful widgets (forms, toggles)External http(s) launchers — those stay prose linksPosition-precise block placement inside prosePie/scatter chart types or interactive tooltips
spec · docs/superpowers/specs/2026-06-13-rich-feed-2d-diagrams-widgets-design.md
plan · docs/superpowers/plans/2026-06-13-rich-feed-2d-diagrams-widgets.md
Mallory · Estate briefing