Render Mosaic artifacts as native interactive UI#3681
Conversation
Detect a ```mosaic fence in an agent reply and draw it as a live,
interactive interface using T3 Code's own components. The artifact is
data, not code: a bounded expression language drives derived values on
the client, and anything beyond local state leaves as a named host intent.
Web: the chat/mosaic renderer (streaming, onIntent, validate -> autocorrect,
raw-source fallback), the full host block set drawn through the ui kit, and
ChatMarkdown fence routing.
Server: the mosaic_ls/cat/validate MCP tools and the Mosaic agent skill
provisioned into each provider's skills directory (Claude, Codex).
Adds @mosaicjs/{core,react} to apps/web and @mosaicjs/ai to apps/server.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 8 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.
| const bound = Boolean(setValue); | ||
| const [local, setLocal] = React.useState(0); | ||
| const current = bound ? num(value as PV) : local; | ||
| const set = (n: number) => (setValue ? setValue(n) : setLocal(n)); |
There was a problem hiding this comment.
FilePicker ignores unbound value
Medium Severity
FilePicker and Rating components, when unbound, don't initialize their local state from props.value. This causes FilePicker to not display its initial value and Rating to always show zero stars, unlike other controls that correctly seed local state from props.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.
| const source = `<Card><Text>First</Text><Text>Second</Text></Card>`; | ||
| const markup = renderToStaticMarkup(<MosaicArtifact source={source} />); | ||
| expect(markup).toContain("p-3.5"); | ||
| expect(markup).toContain("mt-2"); // text -> text default rhythm |
There was a problem hiding this comment.
Card padding test mismatch
Low Severity
The layout test expects compact Card markup to include Tailwind class p-3.5, but the Card host block applies p-3 on UICard. The assertion does not match the implementation in this commit.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.
| if (reportedArtifacts.has(key)) return; | ||
| reportedArtifacts.add(key); | ||
| autocorrect({ artifactId, source, diagnostics: formatted }); | ||
| }, |
There was a problem hiding this comment.
Autocorrect channel never wired
Medium Severity
MosaicArtifact reports validation failures through useMosaicAutocorrect, but nothing in this PR mounts MosaicAutocorrectProvider, so the hook is always null and onDiagnostics returns without sending anything. The PR describes diagnostics flowing back to the agent; that path never runs, and formatCorrectionPrompt is unused.
Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.
| > | ||
| {React.Children.count(children) > 0 ? children : str(props.href)} | ||
| </a> | ||
| ); |
There was a problem hiding this comment.
Mosaic links skip URL sanitization
Medium Severity
The mosaic Link block passes model-authored href strings straight to <a href=…>, while main chat markdown uses defaultUrlTransform to block dangerous URL schemes. A mosaic artifact can expose javascript: or other disallowed links that normal chat markdown would strip.
Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.
| Calendar, | ||
| VegaChart, | ||
| Canvas, | ||
| }); |
There was a problem hiding this comment.
AvatarGroup block not mapped
Medium Severity
The provisioned Mosaic skill lists AvatarGroup as a composable content block, but mosaicComponents maps Avatar only and omits AvatarGroup. Artifacts that follow the skill and emit AvatarGroup won’t render with the host UI kit.
Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.
| tabIndex={clickable ? 0 : undefined} | ||
| > | ||
| <Flow node={node}>{children}</Flow> | ||
| </UICard> |
There was a problem hiding this comment.
Clickable card bubbles button clicks
Medium Severity
When a mosaic Card has a click intent, onClick is attached to the outer card surface without stopping propagation from nested Button children, so activating a button inside the card can also fire the card’s click handler.
Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.
| const formatted = diagnostics.map(formatDiagnostic).join("\n"); | ||
| const key = `${artifactId ?? source} ${formatted}`; | ||
| if (reportedArtifacts.has(key)) return; | ||
| reportedArtifacts.add(key); |
There was a problem hiding this comment.
Session dedupe blocks repeat autocorrect
Low Severity
Module-level reportedArtifacts never clears. Once a given artifact id (or source) and diagnostic set is reported, a later message that emits the same broken artifact again will not trigger autocorrect even after the provider is wired.
Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.
| const label = str(meta.label); | ||
| return ( | ||
| <g key={`edge-${edge.from}-${edge.to}`}> | ||
| <path |
There was a problem hiding this comment.
Parallel diagram edges share keys
Low Severity
Diagram edges use React keys edge-${from}-${to} only, so multiple edges between the same node pair collide and React may drop or mis-render one of them.
Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.
| function Icon({ props }: MosaicBlockProps<BlockPropTypes["Icon"]>) { | ||
| if (!props.name) return null; | ||
| return <MosaicIcon name={props.name} className={cn(TONE_TEXT[props.tone ?? ""])} />; | ||
| } |
There was a problem hiding this comment.
🟡 Medium mosaic/blocks.tsx:193
Icon passes props.name straight to MosaicIcon without validating it is a string. When a schema-mismatched artifact supplies a truthy non-string name (e.g. <Icon name={123} />), resolveIconName calls .trim() on the value and throws a TypeError, crashing the block's render instead of degrading gracefully. Consider coercing props.name to a string (or guarding with typeof props.name === "string") before forwarding it.
| function Icon({ props }: MosaicBlockProps<BlockPropTypes["Icon"]>) { | |
| if (!props.name) return null; | |
| return <MosaicIcon name={props.name} className={cn(TONE_TEXT[props.tone ?? ""])} />; | |
| } | |
| function Icon({ props }: MosaicBlockProps<BlockPropTypes["Icon"]>) { | |
| if (!props.name) return null; | |
| return <MosaicIcon name={String(props.name)} className={cn(TONE_TEXT[props.tone ?? ""])} />; |
Also found in 1 other location(s)
apps/server/src/mcp/toolkits/mosaic/handlers.ts:10
mosaic_lsdereferencesinput.kindwithout theinput ?? {}guard that the other optional-parameter MCP handlers use. Callingmosaic_lswith no arguments—the normal way to list all blocks, per the tool description—passesundefined, soinput.kindthrows aTypeErrorand the tool invocation fails instead of returning the full block list.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/mosaic/blocks.tsx around lines 193-196:
`Icon` passes `props.name` straight to `MosaicIcon` without validating it is a string. When a schema-mismatched artifact supplies a truthy non-string `name` (e.g. `<Icon name={123} />`), `resolveIconName` calls `.trim()` on the value and throws a `TypeError`, crashing the block's render instead of degrading gracefully. Consider coercing `props.name` to a string (or guarding with `typeof props.name === "string"`) before forwarding it.
Also found in 1 other location(s):
- apps/server/src/mcp/toolkits/mosaic/handlers.ts:10 -- `mosaic_ls` dereferences `input.kind` without the `input ?? {}` guard that the other optional-parameter MCP handlers use. Calling `mosaic_ls` with no arguments—the normal way to list all blocks, per the tool description—passes `undefined`, so `input.kind` throws a `TypeError` and the tool invocation fails instead of returning the full block list.
| // to the Mosaic renderer, which draws it with t3code's own components. | ||
| // Streaming state matters: a partial fence never compiles, so the | ||
| // renderer waits for the final text before reporting diagnostics. | ||
| return ( |
There was a problem hiding this comment.
🟡 Medium components/ChatMarkdown.tsx:1506
Routing ```mosaic fences to <MosaicArtifact> breaks copy-paste of artifact source. handleCopy serializes the rendered DOM by reconstructing fenced code from <pre> elements or nodes carrying data-markdown-copy, but <MosaicArtifact> renders interactive <div> content with neither, so copying a rendered artifact yields flattened visible text instead of the original ```mosaic fence source. Users can no longer copy or re-share the artifact source from chat messages. Consider wrapping the <MosaicArtifact> in a <pre data-markdown-copy> element (or stamping data-markdown-copy on the rendered output) so the clipboard path recovers the original fence.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 1506:
Routing ` [code fence]mosaic ` fences to `<MosaicArtifact>` breaks copy-paste of artifact source. `handleCopy` serializes the rendered DOM by reconstructing fenced code from `<pre>` elements or nodes carrying `data-markdown-copy`, but `<MosaicArtifact>` renders interactive `<div>` content with neither, so copying a rendered artifact yields flattened visible text instead of the original ` [code fence]mosaic ` fence source. Users can no longer copy or re-share the artifact source from chat messages. Consider wrapping the `<MosaicArtifact>` in a `<pre data-markdown-copy>` element (or stamping `data-markdown-copy` on the rendered output) so the clipboard path recovers the original fence.
| function Link({ props, children }: MosaicBlockProps) { | ||
| return ( | ||
| <a | ||
| href={str(props.href)} |
There was a problem hiding this comment.
🟡 Medium mosaic/blocks.tsx:507
Link passes str(props.href) directly to the <a href> attribute without sanitizing the URL scheme. In react@19, rendering an <a> with a javascript: URL throws an error during render, so a Mosaic artifact that emits href: "javascript:..." crashes the Link block instead of rendering a usable link. Consider sanitizing the href (e.g., only allowing http:, https:, mailto:, and relative URLs) before passing it to <a>.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/mosaic/blocks.tsx around line 507:
`Link` passes `str(props.href)` directly to the `<a href>` attribute without sanitizing the URL scheme. In `react@19`, rendering an `<a>` with a `javascript:` URL throws an error during render, so a Mosaic artifact that emits `href: "javascript:..."` crashes the `Link` block instead of rendering a usable link. Consider sanitizing the `href` (e.g., only allowing `http:`, `https:`, `mailto:`, and relative URLs) before passing it to `<a>`.
| function Card({ node, props, children, events }: MosaicBlockProps<BlockPropTypes["Card"]>) { | ||
| const clickable = Boolean(events.click); | ||
| return ( | ||
| <UICard | ||
| className={cn( | ||
| // Tighter than a standalone card: an artifact is embedded in a chat | ||
| // message, so it reads as a compact surface, not a full page panel. | ||
| "rounded-xl p-3", | ||
| CARD_TONE[props.tone ?? ""], | ||
| clickable && "cursor-pointer transition-colors hover:border-ring/40", | ||
| )} | ||
| onClick={events.click} | ||
| onKeyUp={clickable ? (e) => e.key === "Enter" && events.click?.() : undefined} | ||
| tabIndex={clickable ? 0 : undefined} | ||
| > |
There was a problem hiding this comment.
🟡 Medium mosaic/blocks.tsx:400
When a clickable Card is rendered, it uses tabIndex={0} on a plain <div> with only an onKeyUp handler for the Enter key. Pressing Space does not activate the card, so keyboard users cannot trigger events.click with the standard activation key. The element also lacks a role, so assistive tech does not announce it as interactive. Consider adding a role="button" and a Space-key handler (or rendering a <button> element) so the card is fully keyboard-accessible.
- onClick={events.click}
- onKeyUp={clickable ? (e) => e.key === "Enter" && events.click?.() : undefined}
- tabIndex={clickable ? 0 : undefined}
+ onClick={events.click}
+ onKeyUp={clickable ? (e) => (e.key === "Enter" || e.key === " ") && events.click?.() : undefined}
+ role={clickable ? "button" : undefined}
+ tabIndex={clickable ? 0 : undefined}🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/mosaic/blocks.tsx around lines 400-414:
When a clickable `Card` is rendered, it uses `tabIndex={0}` on a plain `<div>` with only an `onKeyUp` handler for the Enter key. Pressing Space does not activate the card, so keyboard users cannot trigger `events.click` with the standard activation key. The element also lacks a `role`, so assistive tech does not announce it as interactive. Consider adding a `role="button"` and a Space-key handler (or rendering a `<button>` element) so the card is fully keyboard-accessible.
| (diagnostics: ValidationDiagnostic[]): void => { | ||
| if (isStreaming || autocorrect === null || diagnostics.length === 0) return; | ||
| const formatted = diagnostics.map(formatDiagnostic).join("\n"); | ||
| const key = `${artifactId ?? source} ${formatted}`; |
There was a problem hiding this comment.
🟡 Medium mosaic/MosaicArtifact.tsx:76
reportedArtifacts is a module-global Set, and the dedupe key at line 76 only includes artifactId ?? source plus the formatted diagnostics. If two different chat messages render the same broken artifact in one browser session, the second one is treated as already reported and autocorrect(...) is skipped entirely. Because correction prompts are conversation-local follow-up turns, suppressing later reports means those later artifacts never get a repair request. Consider scoping the Set to the conversation (or including a conversation/thread id in the dedupe key) so each message can report its own artifacts.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/mosaic/MosaicArtifact.tsx around line 76:
`reportedArtifacts` is a module-global `Set`, and the dedupe key at line 76 only includes `artifactId ?? source` plus the formatted diagnostics. If two different chat messages render the same broken artifact in one browser session, the second one is treated as already reported and `autocorrect(...)` is skipped entirely. Because correction prompts are conversation-local follow-up turns, suppressing later reports means those later artifacts never get a repair request. Consider scoping the `Set` to the conversation (or including a conversation/thread id in the dedupe key) so each message can report its own artifacts.
| <div | ||
| className="rounded-t-sm bg-primary" | ||
| style={{ height: `${Math.max(Math.round((v / max) * 120), 3)}px` }} | ||
| /> |
There was a problem hiding this comment.
🟡 Medium mosaic/blocks.tsx:1196
The Chart component uses Math.max(Math.round((v / max) * 120), 3) for bar height, where max is Math.max(...values, 1). For negative data points, v / max is negative, so Math.round produces a negative value that gets clamped to 3px. This means -5 and -500 both render as the same 3px bar, so charts with negative values are materially misrepresented — the relative magnitude of negative points is lost. Consider using Math.abs(v) / max (or otherwise accounting for sign) so negative values reflect their magnitude.
| <div | |
| className="rounded-t-sm bg-primary" | |
| style={{ height: `${Math.max(Math.round((v / max) * 120), 3)}px` }} | |
| /> | |
| style={{ height: `${Math.max(Math.round((Math.abs(v) / max) * 120), 3)}px` }} |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/mosaic/blocks.tsx around lines 1196-1199:
The `Chart` component uses `Math.max(Math.round((v / max) * 120), 3)` for bar height, where `max` is `Math.max(...values, 1)`. For negative data points, `v / max` is negative, so `Math.round` produces a negative value that gets clamped to `3px`. This means `-5` and `-500` both render as the same `3px` bar, so charts with negative values are materially misrepresented — the relative magnitude of negative points is lost. Consider using `Math.abs(v) / max` (or otherwise accounting for sign) so negative values reflect their magnitude.
| function Grid({ props, children }: MosaicBlockProps<BlockPropTypes["Grid"]>) { | ||
| // Children without explicit spans divide the grid equally, so a 12-col Grid | ||
| // with three Stats renders three real columns (never twelve thin ones). | ||
| const count = Math.max(React.Children.count(children), 1); |
There was a problem hiding this comment.
🟠 High mosaic/blocks.tsx:374
React.Children.count(children) counts null, undefined, and boolean children, so a conditional child like {cond && <Stat .../>} still increments count even when it renders nothing. When cond is false, <Grid cols={3}>{cond && <Stat/>}<Stat/><Stat/></Grid> produces count === 3, creating an extra empty grid track and leaving the two visible items in an uneven layout. Consider filtering out falsy children before counting, e.g. React.Children.toArray(children).filter(Boolean).length.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/mosaic/blocks.tsx around line 374:
`React.Children.count(children)` counts `null`, `undefined`, and boolean children, so a conditional child like `{cond && <Stat .../>}` still increments `count` even when it renders nothing. When `cond` is false, `<Grid cols={3}>{cond && <Stat/>}<Stat/><Stat/></Grid>` produces `count === 3`, creating an extra empty grid track and leaving the two visible items in an uneven layout. Consider filtering out falsy children before counting, e.g. `React.Children.toArray(children).filter(Boolean).length`.
| ); | ||
| } | ||
|
|
||
| function Checkbox({ props, value, setValue }: MosaicBlockProps) { |
There was a problem hiding this comment.
🟡 Medium mosaic/blocks.tsx:647
UICheckbox renders @base-ui/react's Checkbox.Root, which outputs a <span> by default, so the <label htmlFor={id}> does nothing — clicking the label neither toggles nor focuses the checkbox. Base UI requires nativeButton with render={<button />} for sibling labels to work. Consider adding those props to UICheckbox (or document the limitation).
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/mosaic/blocks.tsx around line 647:
`UICheckbox` renders `@base-ui/react`'s `Checkbox.Root`, which outputs a `<span>` by default, so the `<label htmlFor={id}>` does nothing — clicking the label neither toggles nor focuses the checkbox. Base UI requires `nativeButton` with `render={<button />}` for sibling labels to work. Consider adding those props to `UICheckbox` (or document the limitation).
| **Controls.** \`Button\` \`Input\` \`Select\` \`MultiSelect\` \`Autocomplete\` \`Checkbox\` \`Radio\` \`Toggle\` \`Slider\` \`DatePicker\` \`ColorPicker\` \`FilePicker\` \`Rating\` \`TagInput\` \`Field\` \`Disclosure\` \`Accordion\` | ||
| **Structure.** \`Tabs\` \`Steps\` \`SegmentedControl\` \`Progress\` \`Empty\` | ||
| **Media.** \`Video\` \`Audio\` \`Carousel\` | ||
| **Data & viz.** \`DataTable\` \`List\` \`Tree\` \`Board\` \`Timeline\` \`Calendar\` \`Stat\` \`Chart\` \`VegaChart\` \`Diagram\` \`Canvas\` \`Embed\` |
There was a problem hiding this comment.
🟡 Medium provider/MosaicSkill.ts:70
The skill text lists Embed under Data & viz. blocks, but the web renderer has no registered Embed component in mosaicComponents. An agent following this skill can emit <Embed>, pass mosaic_validate, and produce an artifact that fails to render because the host has no mapping for that block. Remove Embed from the advertised block list.
| **Data & viz.** \`DataTable\` \`List\` \`Tree\` \`Board\` \`Timeline\` \`Calendar\` \`Stat\` \`Chart\` \`VegaChart\` \`Diagram\` \`Canvas\` \`Embed\` | |
| +**Data & viz.** `DataTable` `List` `Tree` `Board` `Timeline` `Calendar` `Stat` `Chart` `VegaChart` `Diagram` `Canvas` |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/MosaicSkill.ts around line 70:
The skill text lists `Embed` under `Data & viz.` blocks, but the web renderer has no registered `Embed` component in `mosaicComponents`. An agent following this skill can emit `<Embed>`, pass `mosaic_validate`, and produce an artifact that fails to render because the host has no mapping for that block. Remove `Embed` from the advertised block list.
| const o = (it && typeof it === "object" && !Array.isArray(it) ? it : {}) as Record<string, PV>; | ||
| return { date: str(o.date), title: str(o.title) }; | ||
| }) | ||
| .filter((it) => /^\d{4}-\d{2}-\d{2}/.test(it.date)); |
There was a problem hiding this comment.
🟡 Medium mosaic/blocks.tsx:1783
The items filter only checks the ^\d{4}-\d{2}-\d{2} prefix, so impossible dates like 2024-13-01 or 2024-02-31 still pass. new Date(\${earliest}T00:00:00`)then yieldsInvalid Date, making monthLabelrender asInvalid Date, daysInMonth NaN, and the calendar body empty — instead of skipping the bad item. Consider validating that the parsed date is not Invalid Date(e.g., checkNumber.isNaN(d.getTime())`) when filtering items, so invalid dates are excluded.
| .filter((it) => /^\d{4}-\d{2}-\d{2}/.test(it.date)); | |
| .filter((it) => /^\d{4}-\d{2}-\d{2}/.test(it.date) && !Number.isNaN(new Date(`${it.date.slice(0, 10)}T00:00:00`).getTime())); |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/mosaic/blocks.tsx around line 1783:
The `items` filter only checks the `^\d{4}-\d{2}-\d{2}` prefix, so impossible dates like `2024-13-01` or `2024-02-31` still pass. `new Date(\`${earliest}T00:00:00\`)` then yields `Invalid Date`, making `monthLabel` render as `Invalid Date`, `daysInMonth` `NaN`, and the calendar body empty — instead of skipping the bad item. Consider validating that the parsed date is not `Invalid Date` (e.g., check `Number.isNaN(d.getTime())`) when filtering items, so invalid dates are excluded.
ApprovabilityVerdict: Needs human review 10 blocking correctness issues found. This PR introduces a substantial new feature (~2850 lines) for rendering interactive Mosaic artifacts in chat, including new MCP tools, skill provisioning, and a full block component library. Multiple unresolved review comments identify bugs and a security concern (URL sanitization allowing javascript: URLs). The scope and open issues warrant human review. You can customize Macroscope's approvability policy. Learn more. |


Summary
Much of what a coding agent produces is spatial, not linear - a plan with risks beside it, a diff summary, a cost breakdown across providers, an approval prompt.
Today that gets flattened into prose or a code block.
This PR teaches T3 Code to render Mosaic artifacts: when an agent emits a
```mosaicfence in its reply, T3 Code draws it as a live, interactive interface using its own components, in its own look.The artifact is data, never code.
A bounded, CEL-class expression language drives derived values, conditionals, and lists on the client, so a slider updates a total with no round-trip.
Anything beyond local state leaves as a named host intent that T3 Code routes through its existing command surface.
There is nothing to sandbox - no iframe, no
eval, no live data pull.How it works
ChatMarkdowndetects a```mosaicfenced block and hands it to<MosaicArtifact>instead of syntax-highlighting it.<MosaicArtifact>renders through@mosaicjs/react's<Mosaic>, which owns parsing, the reactive loop, streaming completion, and per-node error boundaries.mosaicComponents(defineComponents), so the artifact wears the app's design.useMosaicIntent; validation diagnostics flow back to the agent once through the autocorrect channel.What's included
Web (
apps/web)components/chat/mosaic/MosaicArtifact.tsx- the render surface: streaming-aware,onIntent,onDiagnostics(validate -> agent autocorrect), and a raw-source fallback for a partial or unparseable fence.components/chat/mosaic/blocks.tsx- the host block set: every Mosaic block drawn through T3 Code's UI kit (shadcn-on-Base-UI). Covers the full catalog (see below).components/chat/mosaic/intent.tsx,autocorrect.ts,index.ts- intent routing and the validation-to-agent loop.components/ChatMarkdown.tsx- routes```mosaicfences to the renderer, streaming-aware.Server (
apps/server)mcp/toolkits/mosaic/- the three introspection tools (mosaic_ls,mosaic_cat,mosaic_validate) built on@mosaicjs/ai, registered inMcpHttpServer.provider/MosaicSkill.ts- provisions the Mosaic agent skill (the MCP-aware variant) into each provider's skills directory, so the runtime discovers it natively and auto-activates from its description - no always-on prompt injection.provider/Drivers/{Claude,Codex}Driver.ts- deliver the skill (best-effort; a skill-write failure never blocks the provider from starting).Design and safety
apps/web/src/components/ui/*- the artifact carries semantic tokens (tone="warn"), never raw styles.Embedis intentionally not mapped (denied by default), consistent with the format's trust model.@mosaicjs/*. No vega, no image pipeline, no SVG injection are pulled in.Block coverage
The host map covers the full Mosaic catalog - layout, content, every control, structure/status, media, and data/viz (52 blocks).
This PR completes the previously-partial set by adding:
Image,Video,Audio,Carousel,Rating,FilePicker,Disclosure,Accordion,Tree,Board,Calendar, plus graceful entries forVegaChartandCanvas.VegaChartandCanvasdegrade to theiralton-brand rather than rendering: a full Vega-Lite runtime and a sanitized-SVG pipeline are heavier than this renderer should pull in.Both are noted as follow-ups.
Embedded polish
An artifact lives inside a chat message, so the surfaces are tuned to read as compact embedded content, not standalone pages: tighter
Cardpadding and radius, and a slightly tighter vertical rhythm and tab spacing.Testing
pnpm typecheck- clean.pnpm lint- clean (no new warnings).mcp/toolkits/mosaic/tools.test.ts,components/chat/mosaic/MosaicArtifact.test.tsx.Dependencies
apps/web:@mosaicjs/core@^0.8.0,@mosaicjs/react@^0.8.0apps/server:@mosaicjs/ai@^0.8.0Screenshots
Follow-ups (not in this PR)
VegaChartvia a lazy-loaded Vega-Lite runtime;Canvasvia a sanitized-SVG path.components_supportedand permissions, instead of the default.Note
Medium Risk
Large new chat rendering surface and provider filesystem writes on startup, but artifacts use bounded expression evaluation (not arbitrary code) and skill provisioning is best-effort and non-blocking.
Overview
This PR wires Mosaic through T3 Code so agent replies can ship as live, in-app UI instead of prose or highlighted code.
Chat rendering:
ChatMarkdownroutes```mosaicfences to a newMosaicArtifactpath (streaming-aware, optional fenceid=). Artifacts parse and run locally via@mosaicjs/react, with a largemosaicComponentsmap that draws the full block catalog in the app’s UI kit. Validation issues are reported once to an optional autocorrect channel; user actions leave as named intents (default: info toast).Agent enablement: The MCP server registers read-only
mosaic_ls/mosaic_cat/mosaic_validatetools (@mosaicjs/ai). On Claude and Codex instance startup,provisionMosaicSkillbest-effort writes an MCP-aware Mosaic skill (SKILL.md+REFERENCE.md) into each provider’s skills directory.Dependencies:
@mosaicjs/core+@mosaicjs/reacton web,@mosaicjs/aion server (^0.8.0), with unit tests for tool JSON schemas and artifact rendering.Reviewed by Cursor Bugbot for commit aabf4cd. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Render Mosaic artifacts as native interactive UI in chat messages
```mosaicfences in chat messages now render as interactive UI viaMosaicArtifactinstead of syntax-highlighted code, wired in ChatMarkdown.tsx.@mosaicjs/react.MosaicIntentProvider.mosaic_ls,mosaic_cat, andmosaic_validateto the server, and provisions Mosaic skill files into Claude/Codex home directories on provider startup.📊 Macroscope summarized aabf4cd. 10 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.