6.1 KiB
| created | path | project | tags | type | ||
|---|---|---|---|---|---|---|
| 2026-07-02 | Sources/Dev | dispatch-render-upgrade |
|
project-plan |
Dispatch Render Upgrade
Goal
Upgrade the petal-dispatch text interface to allow for greater user interaction and rendering capability. Concretely: replace the plain markdown-to-text flow in the web client with a content-aware renderer that recognizes distinct content kinds and renders each appropriately. Tables were the surfacing example; the actual goal is a renderer that handles prose, tables, code, structured data, and diagrams — and slots in new kinds without a rewrite each time. Plus copy affordances: a per-code-block copy box and per-message (bubble) copy. The renderer and the copy boxes are both explicit v1 wants.
Constraint that shapes everything: the client is a single self-contained src/petal_dispatch/static/index.html (~2881 lines), Tailwind via CDN, vanilla inline JS, no build step. All additions must be CDN <script> tags. No toolchain, no bundler, no restart — served live on next load.
Locked Decisions
- Parser:
markdown-itvia CDN. Framework-agnostic, plugin architecture, cheap to re-render a growing buffer. GFM tables via core (kills the ASCII-table problem). - Highlighter:
highlight.jsvia CDN. Better language auto-detect than Prism; integrates with markdown-it's highlight hook. - Fence-dispatch table is the extensibility core. The parser tags every code fence with its language. Register widget handlers keyed by language string. Known lang → intercept, mount widget. Unknown → normal highlighted code block. New kind later = one table entry, zero changes to the stream loop.
- Streaming model: buffer-and-re-render. Each message holds its raw markdown string. On each SSE tick: append chunk to buffer, re-render that message from the buffer. The buffer is the throwaway view; DB stays truth. Ship the naive per-tick full re-render first; only add newline-boundary optimization if flicker or mid-stream selection-loss actually bothers in use.
- One data model powers three features. Per-message raw markdown buffer is the single invariant. Streaming re-render, per-block copy, and bubble copy are all views over that same stored string — not three subsystems.
- Copy semantics = raw, not rendered. Per-code-block copy returns the raw fence contents. Bubble copy returns the accumulated markdown buffer (source), not
innerTextof the DOM. - Bubble-copy mid-stream = accepted partial. Copying a still-streaming message yields the current (partial) buffer. This is intended behavior, not a bug — copy grabs current buffer state. Do not "fix" it into something that waits for stream completion.
htmlrendering: out of scope. Arbitrary HTML/JS from message content is a self-XSS surface. Not in v1. If ever added, sandboxed iframe only.
Open Items
- Charts (
chartfence → Chart.js): hatch only if trivial to wire alongside the other handlers; otherwise deferred. Don't build ahead of a chart spec actually showing up more than once. Decide at Phase 2 based on effort, not commitment. - Flicker / selection-loss on per-tick re-render: unknown until real use. If it bites, the fix is re-rendering only on block-closing token boundaries. Not pre-optimized.
Build Phases
Phase 0 — Parser swap (foundation)
- Add
markdown-it+highlight.jsCDN<script>tags toindex.htmlhead. - Wire markdown-it with GFM tables on and the highlight hook pointed at hljs.
- Convert the render loop from text-append to buffer-and-re-render: each message stores raw markdown, re-renders from buffer on SSE tick.
- Style the output via existing Tailwind + inline
<style>: tables (headers, zebra, overflow scroll), code blocks, lists, blockquotes. - Outcome: prose, tables, code, lists, blockquotes render properly. Screenshot problem fixed as a side effect.
Phase 1 — Fence-dispatch table + first widgets
- Build the handler registry:
{ lang: (rawFenceContents) => mountWidget }. - Hook it into markdown-it's fence rendering: known lang → handler, unknown → default code block.
- Add
jsonhandler → collapsible tree. - Add
mermaidhandler → Mermaid.js (CDN) SVG render. High-signal for infra/topology content. - Outcome: structured JSON and diagrams render as widgets; registry proven extensible.
Phase 2 — Copy affordances
- Per-code-block copy button: injected into each fence's rendered block, copies raw fence contents.
- Per-message bubble copy: copies that message's raw markdown buffer.
- Both read from the per-message raw-markdown model established in Phase 0 — no new state.
- Decide here: wire
chart/Chart.js if trivial, else leave the Open Item deferred. - Outcome: one-click copy of any code block and any full message, as source.
Notes
- A vs B (the render architecture choice): A-path = client parses markdown; server keeps streaming raw markdown over SSE (what it does today). B-path = server emits typed content blocks (
{type: "table", ...}) and the client is a dumb switch over block types. This plan is A. Rationale: markdown streams token-by-token cleanly, whereas B must buffer a whole block before it can emit the envelope — killing incremental paint. A also matches DB-as-truth / stream-as-display: the parser is a display concern, not a protocol change; markdown-over-SSE is already the transport. B stays available as a future move only if a genuinely non-streamable kind proves it needs it. - The fence-dispatch table is the whole extensibility bet. Everything below the "streamable" line (charts, diagrams, future kinds) is a spec you buffer until the fence closes, then mount. Uniform seam, one mechanism.
- Extensibility discipline: only
json+mermaidat v1. Charts gated on real recurrence.htmlout. Don't add handlers for content that hasn't appeared. - No-build-step is a hard constraint, not a preference — it's why the whole plan is CDN scripts and inline JS edits, and why there's no framework migration anywhere in scope.
- Execution routes to Lovebug against
src/petal_dispatch/static/index.html; static changes serve live, no restart.