Repository navigation
feat(apollo-react): add the Wind chat markdown renderer - #1375
BenGSchulz wants to merge 1 commit into
Conversation
|
Apollo Coded App preview deployments are ready.
|
Dependency License Review
License distribution
Excluded packages
|
📊 Coverage + size by packagePer-package coverage and bundle size on this PR. New-line coverage = of the source lines this PR adds or changes, the % hit by tests.
"Coverage" is each package's own |
5b84536 to
23d4aff
Compare
23d4aff to
679e00e
Compare
679e00e to
ef4e143
Compare
Render message bodies in the Wind chat with the apollo-wind Markdown primitive: citations as Citation pills with hover preview, cited-range highlighting and the citation-click pre-hook, resource tokens as inline chips, a Sources list under replies, and code blocks with expand and an opt-in sandboxed HTML preview. Hoist the renderer-agnostic markdown logic into src/chat/headless and switch Material to it: streamed content with chunk scheduling, incomplete-token buffering and fake streams, the cited-range highlighter, citation opening and the group sources. MST-16564 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ef4e143 to
ef15b14
Compare
Summary
Third PR of the Wind chat renderer stack (MST-16564): message bodies render as markdown on apollo-wind's
Markdownprimitive, replacing R-2's plain-text stand-in as the WindDEFAULT_MESSAGE_RENDERER.Source:
contentPartsbecome citation markup throughcontentPartsToMarkdown, andcitationPluginandresourceTokenPluginrun as extraremarkPlugins. Their custom nodes map throughcomponents.Streaming: same as Material, through the hoisted
useStreamedMessageContent:[[resource-token:text is held back until it closes;fakeStreammessages are revealed ten characters every 50ms;Only the renderer re-renders per chunk. The primitive's caret and
aria-busyshow only while the message streams or fake-streams. Material shows no caret; Wind keeps it by decision.Citation URLs: only
http:,https:and relative URLs are opened (PDFs keep#page=N).javascript:,data:,blob:,vbscript:and anything else are refused. Thecitation-clickpre-hook still runs with the unchanged citation so a host can handle it, and pills and source items are always buttons, never anchors with the raw href. The check lives in the hoistedgetCitationUrl, so it covers Material too, which opened any scheme before.Citations: apollo-wind
Citationpills (title, plus the page for PDFs) with a hover and focus preview.citation-clickpre-hook with the same payload as Material, then opens the URL (PDFs at#page=N) unless the hook declines.Sources: under the last reply of a group, an apollo-wind
Sourcescollapsible lists the group's citations once each. It is hidden while streaming or while the host setsshouldWaitForMoreMessages. Items run the same pre-hook. The trigger reads "Sources", as in Material, without a count. Intended difference: Material shows the first two sources and a "Show more" button; Wind lists them all once expanded.Resource tokens: a minimal inline chip with the resource's icon (lucide name, via
ServiceIcon) and name.Code blocks: the primitive's
CodeBlockwithcodeWrapon.renderCodeActionsadds:htmlblocks, Preview (opt-in, see below), which opens the dialog on asandbox="allow-scripts"iframe (neverallow-same-origin) and can switch between code and preview. Preview is disabled while the message streams.The dialog is an apollo-wind
Dialog. Focus returns to the button that opened it, since neither button is aDialogTrigger.HTML preview document: the snippet gets a
no-referrermeta and a CSP (default-src 'none'; img-src data: https:; font-src data: https:; style-src 'unsafe-inline'; script-src 'unsafe-inline') after its doctype, through the hoistedbuildHtmlPreviewDocument. Inline styles, scripts and data/https images run; external scripts, stylesheets and network requests do not. Material's preview gets theno-referrermeta only. The iframe renders only while the block is previewable and not streaming, else the dialog shows the code.Cited-range highlight: not activated while the message streams, and removed before new content renders, because the walker moves React-owned text nodes. The CSS Custom Highlight API is the post-parity replacement.
Overlay node: modal dialogs portal to a node outside the chat root that carries the chat's theme class and density, at z-index 1300 (MUI's modal level). It sits in the host's
portalContainerwhen one is given, otherwise indocument.body, and is removed on unmount. A modal therefore covers the page, as Material's does, instead of being confined to the panel's stacking context. Without a host container only dialogs use it; menus, tooltips and hover cards keep the in-root boundary from R-1. With a host container every Wind overlay goes there, as before. Material'sportalContainerhandling is unchanged.HTML preview is gated by
disabledFeatures.htmlPreview, which the service disables by default, so it is opt-in as in Material.Math and images: math is MathML via the primitive's KaTeX, with
singleDollarTextMath: false. Images stay links (the primitive's defaultimages="none").Not here: fenced ```math blocks are W-9 (MST-16784).
Hoisted into
src/chat/headlessmarkdown/incomplete-tokens.ts:INCOMPLETE_TOKEN_PATTERNS,extractRenderableContentmarkdown.tsxhooks/use-streamed-message-content.ts:useStreamedMessageContentcontentParts, fake stream, StopResponse, the 50ms chunk queue with token buffering. Returns{ content, fakeStreaming }markdown.tsxuses it (178 lines removed)markdown/citation-highlight.ts:highlightCitedRangeCHAT_CITATION_STARTtree walker; returns the cleanupcitation.tsxuses itmarkdown/citations.ts:citationFromMarker,getCitationUrl,getGroupCitationscitation.tsx,chat-sources.tsxhooks/use-citations.ts:useOpenCitation,useMessageSourceswindow.open; the group's sources, followed through Response, SendChunk and StopResponsecitation.tsx,chat-sources.tsxOne small behaviour change in Material: a citation with neither
urlnordownload_urlno longer opensundefinedin a new tab.Storybook
Run
pnpm storybook:designand open Apollo React / Chat / Playground. Compare renderers shows Wind and Material on the same content:Markdown corpus: the corpus now also has task lists, a table with
<br>line breaks and a literal$, and a quote with a nested list. A shorter reply streams afterwards.Citations:
Hover a pill to see the preview and the highlighted range (not while the reply streams). Click a pill or a source to run the pre-hook, and turn on
preHooksto see it ask.Code and HTML preview: TypeScript, Python, Bash and a plain block with inline code, then a streamed HTML block.
Resource tokens: inline tokens, tokens in a list, and tokens split across streamed chunks. Wind gets lucide icon names.
New story HTML preview: Compare with
disableHtmlPreviewoff and the code scenario. Preview is in the code block header in Wind and inline in Material. The Wind dialog covers both panels in both themes.Clear chat: a host-side button beside the panels (in a host bar in FullScreen). It cancels everything the story still has scheduled (scenario steps, streams, auto replies) and calls
newChat()on every chat in the story, without a remount. It is a button because args cannot trigger an action.Tests
vitest --run src/chat src/material/components/ap-chat: 36 files, 210 tests pass. New tests cover:useStreamedMessageContent: content parts, a token split across chunks, the 50ms queue flushed on done, fake stream reveal;useOpenCitation: payload, declining hook, and the URL rule (http, a relative PDF with a page, and a refusedjavascript:URL that still calls the hook);useMessageSourcesfollowing streamed parts;citation-clickpayload parity test;tsc --noEmit: clean.biome checkon touched files: one pre-existingnoExplicitAnywarning in Materialmarkdown.tsx(thecoderenderer's props).lingui extractstill stops at the knownautopilot-chat.error.multiple-filesduplicate. A source scan shows none of the ids used here has conflicting messages.en.jsononly:message.html-preview,message.dialog-close,message.footnote-back,message.task-done,message.task-open. Everything else reuses Material's ids with Material's source messages.Stack
Jira: MST-16564
🤖 Generated with Claude Code