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)

  1. Done: Markdown rendering for generic chat-studio stream messages + 3 rendering tests;
  2. Done (2026-09-22, spike findings adopted and shipped directly): the Flutter uikit implements ImcoreMarkdownText on a flutter_markdown_plus 1.0.12 baseline (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 (isStreamPending or envelope.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.
  3. 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;
  4. 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.