UIKit Markdown Rendering
Status: investigation (spike) complete, plus the chat-studio-side gap fix already landed (2026-09-22). Background: written for 2026-09 tech-scan TODO P1-5 (original signal: Stream Chat Android 6.44.0/7.11.0 renders message text as Markdown in the Compose UI Kit; imcore’s AI/streamed replies are naturally Markdown-dense).
1. Current State by Platform
| Platform | Current state | Gap |
|---|---|---|
| chat-studio (web demo client) | MarkdownText.tsx: react-markdown + remark-gfm (tables) + remark-breaks; raw HTML disabled by default; the default urlTransform only allows http/https/mailto etc.; links always open in a new window with noopener |
Only senderID.startsWith('bot:') renders as Markdown — generic stream messages (third-party REST producers, e.g. agent:xxx) rendered as plain text in both the typewriter and final states. Fixed in this round: the streaming local flag and the envelope.stream final-state flag now also trigger Markdown rendering (with tests) |
| Flutter uikit | Plain Text rendering, no Markdown capability |
Code blocks, lists, and tables in bot/streamed replies lose all formatting — the main gap in the mobile look and feel of the AI SKU |
| web/imcore-sdk (TS SDK) | No UIKit-layer rendering responsibility | Not applicable (the SDK layer does not render) |
2. Common Safe-Subset Specification (Every Platform Implementation Must Match)
Allowed: headings #~######, bold/italic/strikethrough, inline code, fenced code blocks,
ordered/unordered lists, task lists, blockquotes, links (http/https/mailto, new window +
noopener noreferrer), tables (GFM), hard line breaks (GFM breaks).
Rejected: raw HTML / dangerouslySetInnerHTML equivalents, javascript:/data: URLs,
scripts and inline event handlers, links with unknown protocols.
Policy items (configurable per platform; defaults must be aligned):
- External images: chat-studio currently relies on the react-markdown default (http(s) images
allowed). Tracking pixels are a real risk (images are rare in AI replies), so the recommendation
is to disable
<img>by default and offer it as an off-by-default toggle (chat-studio’s current behavior is retained for now; the Flutter implementation disables it by default from the start; to be unified later); - Math/LaTeX: kept out of the subset for now (revisit only if the AI SKU needs it, to avoid expanding the parsing surface).
Tolerance for half-finished syntax (unclosed code fences/tables mid-stream): following the ChatGPT convention, unclosed portions render as plain text until they close — no errors, no swallowed content. chat-studio gets this for free from react-markdown’s full re-parse; the Flutter selection must verify this behavior.
3. Flutter Library Selection (Verified 2026-09)
flutter_markdown (Google first-party) has been unmaintained since 2025 (marked
discontinued on pub.dev) and is not an option. Candidates:
| Candidate | Positioning | Assessment |
|---|---|---|
| flutter_markdown_plus (recommended) | Community continuation of flutter_markdown (Foresight Mobile), API-compatible, four levels: basic/CommonMark/GFM/extended | Lowest migration cost, complete syntax coverage; half-finished-streaming tolerance needs spike verification |
| gpt_markdown | Markdown+LaTeX rendering built for LLM conversations, with text selection | Fits the conversation scenario, but LaTeX exceeds our safe subset and must be switched off |
| flutter_markdown_stream / flutter_streaming_text_markdown | Streaming-specific: incremental per-token rendering, no flicker, graceful degradation for unclosed syntax | Overlaps with the uikit’s built-in typewriter drain; pick one to avoid double animation |
Recommendation: run the spike with flutter_markdown_plus as the baseline (safe-subset
rendering + half-finished tolerance + long-message scroll performance), and keep the typewriter
animation on the uikit’s own drain layer (consistent with chat-studio); if the spike shows the
full re-parse dropping frames on long messages (≥2k chars), revisit the streaming-specific
packages.
Sources: flutter_markdown discontinuation notice (pub.dev), flutter_markdown_plus continuation notes (Foresight Mobile), flutter_streaming_text_markdown (pub.dev).
4. Rollout Plan (Execution Status)
- Done: Markdown rendering for generic chat-studio stream messages + 3 rendering tests;
- Done (2026-09-22, spike findings adopted and shipped directly): the Flutter uikit
implements
ImcoreMarkdownTexton aflutter_markdown_plus 1.0.12baseline (safe subset: GFM tables/task lists/strikethrough; images disabled by default; the link callback only allows http/https/mailto and lets the host open links, no url_launcher introduced; inline HTML rendered as plain text). Wiring semantics match chat-studio: bot (bot:prefix) / streaming bubbles (isStreamPendingorenvelope.stream) render as Markdown, human messages as plain text; the streaming cursor character is appended with the drain and is naturally replaced by the final text. 6 new tests, the full suite of 268 green, zero analyzer warnings; the golden vector tests are pure logic comparisons with no pixel baseline impact. - Spike measurements and baseline conclusion: the first full build + layout of 2,267 characters (40 paragraphs + code blocks + tables) takes about 310ms in debug mode. Typical LLM replies (<1k chars) are effortless; very long streamed messages will drop frames under a 33ms full re-parse, currently mitigated by “accelerating the drain based on backlog” (converges in roughly 6–20 ticks). Follow-up: if the AI SKU routinely produces 2k+ character replies, revisit streaming-specific rendering (flutter_markdown_stream and the like) or incremental parsing;
- Images-disabled-by-default has landed on the Flutter side; the chat-studio side (react-markdown allows http(s) images by default) will be tightened when the two platforms unify the policy.