Skip to content

Render Mosaic artifacts as native interactive UI#3681

Open
tamimbinhakim wants to merge 1 commit into
pingdotgg:mainfrom
tamimbinhakim:feat/mosaic-artifacts
Open

Render Mosaic artifacts as native interactive UI#3681
tamimbinhakim wants to merge 1 commit into
pingdotgg:mainfrom
tamimbinhakim:feat/mosaic-artifacts

Conversation

@tamimbinhakim

@tamimbinhakim tamimbinhakim commented Jul 3, 2026

Copy link
Copy Markdown

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 ```mosaic fence 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

agent reply  ->  ```mosaic fence  ->  parse  ->  host block set  ->  local reactive loop  ->  named intents
             (ChatMarkdown)          (@mosaicjs)   (blocks.tsx)        (state.* / expr)        (onIntent)
  • ChatMarkdown detects a ```mosaic fenced 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.
  • Every block is drawn by T3 Code's own mosaicComponents (defineComponents), so the artifact wears the app's design.
  • Host intents flow out through 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 ```mosaic fences 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 in McpHttpServer.
  • 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

  • The host owns the entire design. Every block renders through apps/web/src/components/ui/* - the artifact carries semantic tokens (tone="warn"), never raw styles.
  • The artifact cannot execute code. Expressions are AST-interpreted and statically bounded; the only way out is a named intent that T3 Code chooses whether to act on.
  • Embed is intentionally not mapped (denied by default), consistent with the format's trust model.
  • No new runtime surface beyond @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 for VegaChart and Canvas.

VegaChart and Canvas degrade to their alt on-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 Card padding and radius, and a slightly tighter vertical rhythm and tab spacing.

Testing

  • pnpm typecheck - clean.
  • pnpm lint - clean (no new warnings).
  • Unit tests: mcp/toolkits/mosaic/tools.test.ts, components/chat/mosaic/MosaicArtifact.test.tsx.

Dependencies

  • apps/web: @mosaicjs/core@^0.8.0, @mosaicjs/react@^0.8.0
  • apps/server: @mosaicjs/ai@^0.8.0

Screenshots

Follow-ups (not in this PR)

  • VegaChart via a lazy-loaded Vega-Lite runtime; Canvas via a sanitized-SVG path.
  • A custom host manifest declaring T3 Code's components_supported and 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: ChatMarkdown routes ```mosaic fences to a new MosaicArtifact path (streaming-aware, optional fence id=). Artifacts parse and run locally via @mosaicjs/react, with a large mosaicComponents map 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_validate tools (@mosaicjs/ai). On Claude and Codex instance startup, provisionMosaicSkill best-effort writes an MCP-aware Mosaic skill (SKILL.md + REFERENCE.md) into each provider’s skills directory.

Dependencies: @mosaicjs/core + @mosaicjs/react on web, @mosaicjs/ai on 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

  • ```mosaic fences in chat messages now render as interactive UI via MosaicArtifact instead of syntax-highlighted code, wired in ChatMarkdown.tsx.
  • Adds a full block component library in blocks.tsx covering ~50 block types (layout, form controls, data display, media, diagrams) mapped to the app's UI kit via @mosaicjs/react.
  • Intents emitted by Mosaic blocks surface as info toasts by default; hosts can override the sink via MosaicIntentProvider.
  • Invalid final artifacts report diagnostics once per session to an autocorrect sink; while streaming, diagnostics are suppressed and best-effort rendering continues.
  • Adds MCP tools mosaic_ls, mosaic_cat, and mosaic_validate to 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.

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.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5c6e9e86-ce58-4002-be84-62ebf0e8319e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 3, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 8 potential issues.

Fix All in Cursor

❌ 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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.

if (reportedArtifacts.has(key)) return;
reportedArtifacts.add(key);
autocorrect({ artifactId, source, diagnostics: formatted });
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.

>
{React.Children.count(children) > 0 ? children : str(props.href)}
</a>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mosaic links skip URL sanitization

Medium Severity

The mosaic Link block passes model-authored href strings straight to &lt;a href=…&gt;, 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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.

Calendar,
VegaChart,
Canvas,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.

tabIndex={clickable ? 0 : undefined}
>
<Flow node={node}>{children}</Flow>
</UICard>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.

const label = str(meta.label);
return (
<g key={`edge-${edge.from}-${edge.to}`}>
<path

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aabf4cd. Configure here.

Comment on lines +193 to +196
function Icon({ props }: MosaicBlockProps<BlockPropTypes["Icon"]>) {
if (!props.name) return null;
return <MosaicIcon name={props.name} className={cn(TONE_TEXT[props.tone ?? ""])} />;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

Suggested change
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_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.

🤖 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 (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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>`.

Comment on lines +400 to +414
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}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

Comment on lines +1196 to +1199
<div
className="rounded-t-sm bg-primary"
style={{ height: `${Math.max(Math.round((v / max) * 120), 3)}px` }}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

Suggested change
<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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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\`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

Suggested change
**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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

Suggested change
.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.

@macroscopeapp

macroscopeapp Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant