Skip to content

feat(SuperChat): multi-participant chat with pluggable markdown render pipeline#259

Open
horner wants to merge 41 commits into
mainfrom
superchat
Open

feat(SuperChat): multi-participant chat with pluggable markdown render pipeline#259
horner wants to merge 41 commits into
mainfrom
superchat

Conversation

@horner

@horner horner commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

Adds SuperChat — a multi-participant (multi-human + multi-agent) chat component with a pluggable Markdown rendering pipeline — along with supporting maintainer docs and an AI stories refactor.

What's included

SuperChat component

  • SuperChat component with multi-participant conversations, @-mention addressing of agents, a composer with mention autocomplete, and a read-only mode.
  • Pluggable Markdown renderer (createMarkdownRenderer) with a render-context for streaming state and a plugin registry.

Render plugins

  • code — syntax-highlighted fenced code blocks with copy-to-clipboard.
  • math — KaTeX rendering via remark-math / rehype-katex.
  • genui — interactive generative-UI widgets via a registry.
  • mermaid — lazy-loaded, strict-mode diagram rendering for fenced mermaid blocks (SVG trust boundary in strict mode).
  • image — click-to-zoom Markdown images via Messaging's LightboxModal (portaled to document.body).
  • nitro-table — GFM tables rendered through the DataVis NITRO grid, lazy-loaded, degrading gracefully to a themed HTML table via an error boundary when datavis is unavailable.

All rich-render dependencies (react-markdown, remark/rehype plugins, katex, mermaid, datavis) are optional peer deps kept out of the base bundle; plugins are tree-shakeable via per-component tsup entries.

Docs & housekeeping

  • New MAINTAINERS.md for SuperChat plus several other components (AGGrid, AI, DataVisNITRO, ESheet, FloatingWindow, YChart).
  • AI stories split into focused files (AIChat, AIMessage, MCPToolCall) with shared storyData.
  • superchat-plan.md design/plan document.

Testing

  • 12 unit tests covering GFM markdown, sanitization, code highlighting, GenUI widgets + unknown-widget fallback, math, mermaid streaming pending-state, image click-to-zoom lightbox, NITRO-table HTML fallback, @-mention menu, send flow, and read-only mode. All passing.
  • Lint clean.
  • Verified live in Storybook (mermaid SVG, KaTeX, code+copy, GenUI KPI widget, NITRO table, image lightbox, mention autocomplete + send).

Notes

  • The datavis NITRO grid falls back to a themed HTML table in environments where the datavis submodule isn't checked out (or its deps fail to resolve) — this degradation is intentional and tested.

Screenshots

Multi-participant conversation — humans + agents, @-mention addressing, and Markdown (bold, lists, blockquotes).
Conversation overview

GenUI widget, reference chip & Mermaid diagram — interactive KPI card, a document reference, and a rendered mermaid flowchart.
GenUI widget and Mermaid diagram

Syntax-highlighted code & KaTeX math — fenced code with copy-to-clipboard and block/inline math.
Code block and math

NITRO table — GFM tables rendered through the DataVis NITRO pipeline (shown here degrading gracefully to a themed HTML table).
NITRO table

Image lightbox — click-to-zoom Markdown images via Messaging's LightboxModal (portaled to document.body).
Image lightbox

@-mention autocomplete — type @ in the composer to address a participant.
Mention autocomplete

Sources & Guards

A dedicated Sources & Guards Storybook story (SuperChat → Sources & Guards) documents, per visual, the exact Markdown source that produces it and the guard (trust boundary) that keeps untrusted model/agent output safe. The rendered column uses the production createMarkdownRenderer with every plugin enabled, so the doc stays in sync with the code.

Feature Source Guard (trust boundary)
Markdown core (GFM) **bold**, lists, > quote, [link](url) rehype-sanitize allow-list strips scripts/unknown tags; links forced to target="_blank" rel="noopener noreferrer".
code Fenced code blocks rehype-highlight .hljs-* classes are explicitly allow-listed on code/pre/span; sanitize runs after highlight so only those classes survive.
math $…$ / $$…$$ math.tsx allow-lists KaTeX's HTML+MathML tags/attributes; rehype-katex runs with throwOnError:false (malformed math degrades, never throws).
genui Fenced genui JSON blocks Widgets are host-registered, lazy, and schema-validated; only the <genui-widget> tag is allow-listed. Unknown/invalid widgets degrade to an inert code block; mount + data fetch gated on streaming.
mermaid Fenced mermaid blocks Loaded lazily and rendered with securityLevel:'strict' (labels sanitized, scripts stripped). The SVG bypasses rehype-sanitize, so strict mode is the trust boundary; gated on streaming.
image ![alt](url) src/alt are already protocol-restricted by rehype-sanitize; the plugin only adds the zoom affordance and portals LightboxModal to document.body.
nitro-table GFM tables Lazy-loads the DataVis grid only when a table appears; a GridErrorBoundary degrades to the themed HTML table if datavis is unavailable or the grid throws.

The trust-boundary ordering (plugin rehype transforms → highlight/katex → rehype-sanitize with the merged allow-list) lives in src/components/SuperChat/render/createMarkdownRenderer.tsx; the trusted flag is the only way to skip sanitization, and only for host-authored content.

Sources and Guards

Copilot AI review requested due to automatic review settings June 8, 2026 04:43
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 8, 2026

Copy link
Copy Markdown

Deploying ui with  Cloudflare Pages  Cloudflare Pages

Latest commit: 86cb250
Status: ✅  Deploy successful!
Preview URL: https://af54ccd3.ui-6d0.pages.dev
Branch Preview URL: https://superchat.ui-6d0.pages.dev

View logs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new SuperChat feature module to @mieweb/ui: a multi-participant chat surface that composes the existing AI module and introduces a pluggable Markdown rendering pipeline with opt-in rich-render plugins (code/math/genui/mermaid/image/nitro-table). The PR also refactors AI Storybook stories into separate autodocs pages with shared fixtures, and introduces/updates maintainer/provider documentation to clarify repo conventions, submodules, and optional peer dependencies.

Changes:

  • Introduces SuperChat (component + types) and createMarkdownRenderer with a plugin registry and render-time context for streaming-aware nodes.
  • Adds opt-in rich Markdown plugins (code highlight + copy, KaTeX math, GenUI widgets, Mermaid, image lightbox, NITRO table) and corresponding tests/stories.
  • Adds/updates provider documentation (CONTRIBUTING.md, MAINTAINERS notes) and updates build configuration/peer deps to keep heavy dependencies optional and externalized.

Reviewed changes

Copilot reviewed 33 out of 34 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tsup.config.ts Adds tree-shakeable entry points for SuperChat and its plugin subpath; externalizes new optional deps.
superchat-plan.md Design/architecture plan document for SuperChat participant model and render pipeline.
src/components/YChart/MAINTAINERS.md Provider notes clarifying YChart is story-only and submodule-backed.
src/components/SuperChat/types.ts Defines SuperChat participant + conversation/message model and render/plugin/GenUI contracts.
src/components/SuperChat/SuperChat.tsx Implements the SuperChat shell: sidebar, thread, message rendering, composer with mention autocomplete.
src/components/SuperChat/SuperChat.test.tsx Unit tests for Markdown rendering, sanitization, plugins, mention UX, send flow, and read-only mode.
src/components/SuperChat/SuperChat.stories.tsx Autodocs stories demonstrating Markdown core and rich plugins, including a stateful host wrapper.
src/components/SuperChat/render/renderContext.ts Render-time context for messageId/streaming used by custom Markdown nodes.
src/components/SuperChat/render/createMarkdownRenderer.tsx Markdown renderer composer (GFM + sanitize) that merges plugin rehype/remark/components and sanitize schema contributions.
src/components/SuperChat/plugins/nitroTableGrid.tsx Lazy-loaded NITRO grid wrapper for rendering parsed GFM tables via DataVis source/grid.
src/components/SuperChat/plugins/nitroTable.tsx Plugin that overrides table nodes to render via lazy NITRO grid with HTML fallback/error boundary.
src/components/SuperChat/plugins/mermaid.tsx Mermaid plugin: rehype transform + lazy render in strict mode with streaming gating and fallback.
src/components/SuperChat/plugins/math.tsx KaTeX plugin: remark-math + rehype-katex and sanitize allow-list extensions.
src/components/SuperChat/plugins/index.ts Public plugin subpath exports for opt-in rich rendering.
src/components/SuperChat/plugins/image.tsx Image plugin: click-to-zoom using Messaging LightboxModal via portal.
src/components/SuperChat/plugins/genui.tsx GenUI plugin: fenced JSON payload → host-registered lazy widgets with schema validation + prefetch policies.
src/components/SuperChat/plugins/code.tsx Code plugin: rehype-highlight + copy-to-clipboard pre wrapper.
src/components/SuperChat/MAINTAINERS.md Provider notes for SuperChat: decisions, bundle layout, plugin gotchas, and submodule/peer-dep behavior.
src/components/SuperChat/index.ts Entry exports for SuperChat shell, renderer, context, and public types.
src/components/FloatingWindow/MAINTAINERS.md Adds provider notes and invariants for FloatingWindow behavior and gestures.
src/components/ESheet/MAINTAINERS.md Adds provider notes clarifying submodule-based implementation and build/test expectations.
src/components/DataVisNITRO/MAINTAINERS.md Adds provider notes about NITRO architecture, deps, and “prefer NITRO over AGGrid”.
src/components/AI/storyData.ts Extracts shared Storybook fixtures into a non-story module.
src/components/AI/MCPToolCall.stories.tsx New focused autodocs story file for MCPToolCall using shared fixtures.
src/components/AI/MAINTAINERS.md Updates provider notes with autodocs split + renderTextContent seam guidance.
src/components/AI/AIMessage.stories.tsx New focused autodocs story file for AIMessage using shared fixtures.
src/components/AI/AIChat.stories.tsx New focused autodocs story file for AIChat using shared fixtures.
src/components/AI/AI.stories.tsx Removes the old combined multi-component story file.
src/components/AGGrid/MAINTAINERS.md Adds/updates provider notes emphasizing NITRO preference and AGGrid invariants.
README.md Replaces contributor workflow section with a pointer to CONTRIBUTING.md and a pnpm/submodule quick start.
package.json Adds optional peer deps for markdown/math/mermaid rendering pipeline; adds dev deps for local builds/tests.
lessons/README.md Clarifies lessons folder is consumer/adoption docs; points maintainers to CONTRIBUTING.md.
CONTRIBUTING.md Adds a provider/maintainer guide: repo layout, commands, quality gates, autodocs conventions, entries/tree-shaking, submodules, optional peers.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/SuperChat/SuperChat.tsx Outdated
Comment thread src/components/SuperChat/SuperChat.tsx Outdated
Comment thread src/components/SuperChat/SuperChat.tsx Outdated
Comment thread src/components/SuperChat/plugins/code.tsx
Comment thread src/components/SuperChat/plugins/genui.tsx
Comment thread src/components/SuperChat/plugins/mermaid.tsx
Comment thread src/components/SuperChat/plugins/math.tsx
Comment thread src/components/SuperChat/MAINTAINERS.md
Comment thread src/components/SuperChat/plugins/genui.tsx Outdated
Comment thread src/components/SuperChat/plugins/mermaid.tsx
@wreiske

wreiske commented Jun 8, 2026

Copy link
Copy Markdown
Member

@copilot resolve the merge conflicts in this pull request also fix any copilot comments.

Copilot AI review requested due to automatic review settings June 8, 2026 22:46
@horner horner review requested due to automatic review settings June 8, 2026 22:46
Copilot AI review requested due to automatic review settings June 8, 2026 22:51
@horner horner review requested due to automatic review settings June 8, 2026 22:51

Copilot AI commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request also fix any copilot comments.

Done. I resolved the PR merge conflicts in commit ff65e02 and fixed the Copilot review items (plus the related CI typecheck failure) in commit 0403be4. I also re-ran lint, typecheck, tests, build, secret scanning, and CodeQL (no alerts).

Copilot AI requested a review from wreiske June 8, 2026 22:53
Copilot AI review requested due to automatic review settings June 9, 2026 07:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 20 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread src/components/SuperChat/plugins/code.tsx
Comment thread superchat-plan.md Outdated
@horner

horner commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

Harmonize with this: #233

horner and others added 12 commits June 13, 2026 14:11
Add a native SuperChat module that composes the AI module's renderTextContent
seam (Decisions 1-3 of superchat-plan.md):

- Participant model (human/agent/system) generalizing AI + chat-component roles;
  chat-component-compatible conversation/thread/ref/linkBuilder shape
- createMarkdownRenderer: composes render plugins into one renderTextContent;
  Markdown core (remark-gfm) with rehype-sanitize on untrusted output
- Opt-in rich plugins behind a subpath: code (rehype-highlight + copy),
  math (KaTeX), genui (fenced JSON -> host-registered, lazy, schema-validated
  widget registry with component-vs-data prefetch)
- SuperChat shell: sidebar/thread/composer, controlled props, timestamp-ordered
  interleaved replies, per-participant color/avatar cues, read-only
- Composer @-mention menu (keyboard + mouse) and password-manager opt-out attrs
- Stories (Markdown core / rich plugins / read-only), MAINTAINERS.md, 9 tests
- Wire package.json optional peers + tsup subpath entries; not in base bundle
…plugins (Milestone 4)

- mermaid: lazy-loaded strict-mode diagram rendering for fenced mermaid blocks
- image: click-to-zoom Markdown images via Messaging LightboxModal (portaled to body)
- nitro-table: GFM tables through DataVis NITRO grid, lazy-loaded, degrades to
  themed HTML table via GridErrorBoundary when datavis is unavailable
- wire plugins into plugins/index.ts, tsup externals, and optional mermaid peer dep
- add rich-plugins story + 3 tests (12 total), update MAINTAINERS.md
react-i18next → html-parse-stringify → void-elements live only in the
pnpm virtual store, so Vite served void-elements raw without CJS→ESM
default-export interop, breaking the datavis NITRO grid (GridErrorBoundary).
Alias and force-include the chain in optimizeDeps, resolved against the
workspace-local .pnpm/node_modules store.
… README

A11y: landmark roles and accessible names on every structural region —
group root, complementary sidebar, labelled main section, participants
group, role=log live message thread, per-message article labels, and a
proper mention combobox wired to its listbox. Add semantic data-slot
hooks (header/thread/composer/bubble/meta/etc.) for styling and tests.

Docs: new consumer README with a getting-started guide, a vocabulary
glossary mapping each subcomponent term to its data-slot/role/name, an
annotated visual-layout diagram, and cross-references to the AI,
Messaging, and standalone chat-component surfaces. Point MAINTAINERS at it.
…ns, and SuperChatInbox

Break the monolithic SuperChat (sidebar + main panel) into three composable
components sharing one folder and one import path
(@mieweb/ui/components/SuperChat):

- SuperChat        — single-conversation panel (takes one `conversation`);
                     root data-slot="superchat".
- SuperChatConversations — the conversation list (sidebar) with controlled/
                     uncontrolled selection; root data-slot="superchat-conversations".
- SuperChatInbox   — wrapper composing the two; accepts the original full
                     SuperChatProps so it is a drop-in for the old component;
                     root data-slot="superchat-inbox".

Shared presentational pieces and helpers (ParticipantAvatar, ReferenceChip,
MessageRow, Composer, sidebarItem, time/mention utils) move to an internal
parts.tsx (not exported). No package.json/tsup change — the existing single
entry + ./components/* wildcard cover the new exports.

Stories: split per component (Panel / Conversations / Inbox) with shared
sample data in storyData.tsx; add args-driven Playground stories so the
Controls panel is populated. Tests: retarget panel tests to a single
conversation and add SuperChatConversations + SuperChatInbox coverage
(21 passing). Docs: README + MAINTAINERS remapped to the three-component
model and new data-slot vocabulary.
…tories

- Add SuperChat.mdx Overview page that renders README.md verbatim (?raw),
  rewriting relative .md links to absolute GitHub URLs so they stay clickable
  in Storybook while remaining correct on GitHub/npm.
- Trim duplicated component descriptions in the Panel/Inbox story metas to
  short blurbs that defer to the Overview (DRY).
- Enable the full render-plugin set on the Playground stories so math/code/
  GenUI/mermaid/image/table render; rename Panel 'MarkdownCore' -> 'CoreNoPlugins'
  (the lone plugin-less baseline, with an explanatory note).
- Drop redundant stories (WithRichPlugins, Panel ReadOnly/Closable, Inbox
  Default/ReadOnly); keep Inbox Sources & Guards security reference.
… table contrast

- Repoint code-badge and doc-link colors to injected --mieweb-primary-400
  and hover backgrounds to --mieweb-muted (the previously referenced
  --mieweb-primary / --mieweb-accent tokens are never injected, so they
  always fell back to hardcoded blue/grey regardless of brand).
- Add dark-mode prose-table rules for Markdown/README docs using injected
  --mieweb-foreground/--mieweb-card/--mieweb-border tokens.
- Manager brand-theme switcher button now uses brand.primary so it reflects
  the active brand instead of the muted toolbar grey.
Copilot AI review requested due to automatic review settings June 13, 2026 18:37
Copilot AI review requested due to automatic review settings June 14, 2026 02:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 32 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread src/tailwind-preset.ts
Comment on lines +337 to +348
// SuperChat (participant chips, unread badge, active/hover states)
'bg-primary-100',
'bg-primary-600',
'hover:bg-primary-700',
'dark:bg-primary-900/40',
'text-primary-900',
'dark:text-primary-100',
'dark:text-primary-200',
'hover:border-primary-300',
'focus:border-primary-500',
'focus:ring-primary-500',
'text-[10px]',
Comment on lines +68 to +82
it('keeps syntax-highlight token classes through sanitization (code plugin)', () => {
const r = createMarkdownRenderer({ plugins: [createCodePlugin()] });
const { container } = renderText(
r('```js\nconst x = 1;\n```', {
messageId: 'm3',
streaming: false,
role: 'assistant',
})
);
expect(
container.querySelector('code.hljs, code[class*="language-"]')
).not.toBeNull();
});
});

…kdown / plain)

Every content message gets a margin copy control (left for incoming, right for
own) that appears on hover/focus. The primary Copy writes both text/html and
text/plain in one clipboard write so the paste target decides; explicit
'Copy as Markdown' and 'Copy as plain text' options are also offered.
Copilot AI review requested due to automatic review settings June 14, 2026 03:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 32 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

horner added 2 commits June 13, 2026 23:22
… bottom

On long messages the copy affordance now sticks to the bottom of the viewport
(self-aligned to the message's end) so it stays reachable while scrolling and
settles at the message bottom once fully in view. The menu opens upward to suit
the low anchor.
Copilot AI review requested due to automatic review settings June 14, 2026 03:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 32 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment on lines +74 to +85
function baseSanitizeSchema(): SanitizeSchema {
const schema = JSON.parse(JSON.stringify(defaultSchema)) as SanitizeSchema;
const attributes = (schema.attributes ?? {}) as Record<string, unknown[]>;

const allowClass = (tag: string) => {
const existing = (attributes[tag] ?? []) as unknown[];
// Drop any prior restricted className rule, allow className broadly.
const withoutClass = existing.filter(
(a) => !(Array.isArray(a) && a[0] === 'className')
);
attributes[tag] = [...withoutClass, 'className'];
};
Copilot AI review requested due to automatic review settings June 14, 2026 03:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 32 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment on lines +254 to +262
void entry
.component()
.then((m) => {
if (active) setLoaded(() => m.default);
})
.catch(() => {
if (active)
setValidated({ ok: false, message: 'Failed to load widget' });
});
Copilot AI review requested due to automatic review settings June 14, 2026 04:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 32 changed files in this pull request and generated 5 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread .storybook/main.ts
Comment on lines +96 to +99
const workspacePnpmVirtualNodeModulesDir = path.join(
rootNodeModulesDir,
'.pnpm/node_modules',
);
Comment on lines +347 to +350
const run = (fn: () => Promise<void>) => {
setOpen(false);
void fn().then(flash);
};
Comment on lines +143 to +146
const orderedThread = React.useMemo(() => {
const sorted = [...conversation.thread].sort(byTime);
return order === 'desc' ? sorted.reverse() : sorted;
}, [conversation, order]);
Comment on lines +176 to +190
code: ({ node: _node, ...props }) => {
const isBlock =
typeof props.className === 'string' &&
props.className.includes('language-');
return (
<code
{...props}
className={cn(
!isBlock &&
'rounded bg-neutral-200 px-1 py-0.5 text-[0.85em] dark:bg-neutral-700',
props.className
)}
/>
);
},
Comment on lines +316 to +322
const writeText = async (value: string) => {
try {
await navigator.clipboard?.writeText(value);
} catch {
// Clipboard may be unavailable (insecure context / denied permission).
}
};
Copilot AI review requested due to automatic review settings June 14, 2026 04:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 35 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment on lines +176 to +190
code: ({ node: _node, ...props }) => {
const isBlock =
typeof props.className === 'string' &&
props.className.includes('language-');
return (
<code
{...props}
className={cn(
!isBlock &&
'rounded bg-neutral-200 px-1 py-0.5 text-[0.85em] dark:bg-neutral-700',
props.className
)}
/>
);
},
Comment on lines +383 to +397
function parsePayload(raw: string): AttachmentBlockPayload | null {
try {
const parsed = JSON.parse(raw) as Partial<AttachmentBlockPayload>;
if (!parsed || typeof parsed.type !== 'string') return null;
if (!parsed.id && !parsed.src) return null;
return {
id: typeof parsed.id === 'string' ? parsed.id : undefined,
type: parsed.type,
name: typeof parsed.name === 'string' ? parsed.name : 'attachment',
src: typeof parsed.src === 'string' ? parsed.src : undefined,
};
} catch {
return null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants