Skip to content

feat(canvas): add MarkdownEmbedNode with FBS schema and .md file drops#627

Merged
softmarshmallow merged 8 commits intocanaryfrom
feature/ecstatic-antonelli
Apr 4, 2026
Merged

feat(canvas): add MarkdownEmbedNode with FBS schema and .md file drops#627
softmarshmallow merged 8 commits intocanaryfrom
feature/ecstatic-antonelli

Conversation

@softmarshmallow
Copy link
Copy Markdown
Member

@softmarshmallow softmarshmallow commented Apr 4, 2026

Summary

  • Introduces a first-class MarkdownEmbedNode type that renders GFM markdown directly on the canvas via the existing Skia-backed markdown painter
  • Previously, the AI tool make_from_markdown fell back to creating a plain TextSpanNode, so styled markdown never made it to the renderer — it now creates a proper MarkdownEmbedNode
  • Adds .md file drop + paste support on the canvas

What changed

Schema (format/grida.fbs)

  • Added MarkdownEmbedNode + MarkdownEmbedNodeProperties tables (markdown string, fill_paints, rectangular corner_radius)
  • Appended MarkdownEmbedNode to the Node union and NodeType enum (additive, schema-safe)

TS runtime (packages/grida-canvas-schema/grida.ts)

  • Added MarkdownEmbedNode interface (type: \"markdown_embed\")
  • Wired into LayerNode / ComputedNode / UnknownNode / NodePrototype unions

Rust IO bridges (crates/grida-canvas/src/io/)

  • JSON bridge: JSONMarkdownEmbedNodeMarkdownEmbedNodeRec
  • FBS decoder: decode_markdown_embed_node

TS FBS encoder (packages/grida-canvas-io/format.ts)

  • Added encoder case for \"markdown_embed\" with corner radius + fill paints

Editor + AI tool

  • Editor.createMarkdownEmbedNode(markdown) command
  • make_from_markdown tool now creates the styled embed node
  • initial-node factory case + DOM renderer background fill mapping

File drops

  • Added \"text/markdown\" to ValidFileType (detects MIME + .md extension)
  • insertMarkdown handler wired through insertFromFile and paste handler

Test plan

  • `cargo check -p cg -p grida-canvas-wasm -p grida-dev` passes
  • `cargo test -p cg --lib` passes (673 tests)
  • `pnpm turbo build --filter='./packages/*'` succeeds (16/16)
  • `pnpm turbo typecheck --filter='./packages/*' --filter=editor` succeeds (27/27)
  • Manually verify AI tool creates a styled markdown node (headings, lists, code blocks render)
  • Manually verify dragging a .md file onto the canvas creates a MarkdownEmbed node at the drop position
  • Manually verify pasting markdown via clipboard creates an embed node

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Markdown Embed nodes with support for GitHub Flavored Markdown features including headings, lists, code blocks, and tables.
    • Insert Markdown content directly from clipboard or drag-and-drop .md files.
    • New AI tool enables creating styled Markdown embed nodes from text input.

softmarshmallow and others added 4 commits April 5, 2026 03:52
Replace the standalone 877-line markdown→Skia renderer (painter/markdown.rs)
with the shared htmlcss pipeline: markdown is converted to HTML via
pulldown-cmark, wrapped with grida-markdown.css, and rendered through
Stylo CSS → Taffy layout → Skia paint — the same path as HTMLEmbedNode.

Key changes:
- Add `markdown_to_styled_html()` that converts GFM markdown to a
  self-contained HTML document with embedded CSS
- Add `grida-markdown.css` fixture (element-selector-only stylesheet
  targeting pulldown-cmark's bare HTML output)
- Add faux-table layout module: emulates CSS table via Taffy flex
  (table→flex-column, tr→flex-row, td→equal-width flex children)
- Auto-height on resize: MarkdownEmbed and HTMLEmbed nodes re-measure
  content height when width changes in grida-dev
- Measure content height at load time instead of using 100,000px
- Rename MarkdownNodeRec → MarkdownEmbedNodeRec to align with
  HTMLEmbedNodeRec naming pattern
- Delete painter/markdown.rs (877 lines removed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat(canvas): unify MarkdownEmbed to use htmlcss render pipeline
… and .md file drops

Introduces a first-class MarkdownEmbed node type that renders GFM markdown
directly on the canvas via the existing Skia-backed markdown painter. Prior
to this, the AI tool make_from_markdown fell back to creating a plain
TextSpanNode, so styled markdown never actually made it to the renderer.

- format/grida.fbs: add MarkdownEmbedNode + MarkdownEmbedNodeProperties
  tables, append to Node union and NodeType enum
- packages/grida-canvas-schema: add MarkdownEmbedNode interface and wire
  into LayerNode / ComputedNode / NodePrototype unions
- crates/grida-canvas: JSON bridge + FBS decoder for MarkdownEmbedNode
- packages/grida-canvas-io: TS FBS encoder for markdown_embed
- editor: createMarkdownEmbedNode command, initial-node factory case,
  DOM renderer background fill mapping
- ai/tools/canvas-use: make_from_markdown now creates MarkdownEmbedNode
- data-transfer: insertMarkdown handler + .md file drop/paste support
  (text/markdown MIME type added to ValidFileType)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Apr 4, 2026 9:32pm
6 Skipped Deployments
Project Deployment Actions Updated (UTC)
code Ignored Ignored Apr 4, 2026 9:32pm
legacy Ignored Ignored Apr 4, 2026 9:32pm
backgrounds Skipped Skipped Apr 4, 2026 9:32pm
blog Skipped Skipped Apr 4, 2026 9:32pm
grida Skipped Skipped Apr 4, 2026 9:32pm
viewer Skipped Skipped Apr 4, 2026 9:32pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 4, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ae144fa1-5f08-468d-98df-62aa85c70c4d

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

Walkthrough

This PR introduces comprehensive support for a new MarkdownEmbedNode type across the Grida canvas architecture, adding JSON/FlatBuffers serialization, editor factory methods, file and clipboard handling for markdown files, and updating the "make_from_markdown" tool to create styled markdown-rendered embed nodes instead of plain text.

Changes

Cohort / File(s) Summary
Schema & Format Definition
packages/grida-canvas-schema/grida.ts, format/grida.fbs
Introduced MarkdownEmbedNode and ComputedMarkdownEmbedNode TypeScript interfaces with type literals, and extended FlatBuffers schema to include MarkdownEmbedNodeProperties and MarkdownEmbedNode table definitions with markdown string, fill paints, and corner radius traits.
JSON & ID Conversion
crates/grida-canvas/src/io/io_grida.rs, crates/grida-canvas/src/io/id_converter.rs
Added JSONMarkdownEmbedNode struct with markdown field and flattened base properties, mapped serde deserialization to markdown_embed variant, and updated IdConverter to recognize the new JSON variant during node conversion.
FlatBuffers Serialization
crates/grida-canvas/src/io/io_grida_fbs.rs, packages/grida-canvas-io/format.ts
Implemented FBS decoder for MarkdownEmbedNode variant and encode/decode mappings between TypeScript and FlatBuffers representations, with logic for serializing fills, markdown strings, and corner radius properties.
Editor Core APIs
editor/grida-canvas/editor.i.ts, editor/grida-canvas/editor.ts
Added createMarkdownEmbedNode(markdown?) factory method to the editor API that generates nodes with default dimensions (400x300), absolute positioning, solid white fill, and returns a NodeProxy<MarkdownEmbedNode>.
Editor Tools & Initialization
editor/grida-canvas/reducers/tools/initial-node.ts, editor/grida-canvas-hosted/ai/tools/canvas-use.ts
Extended initial node factory to construct MarkdownEmbedNode with default markdown content and layout dimensions, and updated the "make_from_markdown" tool to create markdown embed nodes via createMarkdownEmbedNode instead of plain text nodes.
File & Clipboard Support
packages/grida-canvas-io/index.ts, editor/grida-canvas-react/use-data-transfer.ts
Extended file type detection to recognize text/markdown MIME type, added insertMarkdown callback for creating markdown embed nodes from file/clipboard content, and updated paste handling to route markdown files through the markdown insertion path.
React Rendering
editor/grida-canvas-react-renderer-dom/nodes/node.tsx
Added markdown_embed node type to the CSS fill behavior mapping, defaulting to "background" fill styling consistent with other styled node types.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • gridaco/grida#625: Introduces the core MarkdownEmbed node variant with conversions and rendering support that directly ties to this PR's serialization and editor integration.
  • gridaco/grida#620: Adds first-class markdown node support across IO, editor APIs, and rendering, making closely related changes to the same code layers.

Suggested labels

markdown, format, canvas, canvas/io, tools

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing MarkdownEmbedNode with FBS schema support and .md file drop functionality, which is the primary focus of all changes across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/ecstatic-antonelli

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 and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a4d733311

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1918 to +1919
case "markdown_embed": {
const mdNode = node as grida.program.nodes.MarkdownEmbedNode;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add MarkdownEmbed handling in TS FlatBuffers decoder

This new encoder path writes fbs.Node.MarkdownEmbedNode, but the TS FBS document decoder still has no case fbs.Node.MarkdownEmbedNode and falls back to its default group reconstruction. In any flow that round-trips through @grida/io decode (save/import/reopen), markdown embeds will be deserialized as groups and lose markdown-specific data (markdown, fill paints, corner radius), so this change currently introduces data loss for the new node type.

Useful? React with 👍 / 👎.

Comment thread packages/grida-canvas-io/index.ts Outdated
Comment on lines +309 to +313
} else if (
type === "text/markdown" ||
type === "md" ||
type === "markdown"
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate .md extension even when MIME is non-markdown

The new markdown detection checks type === "md" | "markdown", but type is assigned from file.type || extension, so those extension checks are skipped whenever the browser provides any MIME type. When .md files come through as text/plain (a common browser/OS behavior), they are rejected as unsupported and markdown file drop/paste silently fails in those environments.

Useful? React with 👍 / 👎.

create_packed_scene_document_from_prototype threw
"Unsupported node prototype type: markdown_embed" when the AI tool or .md
file drop tried to insert a markdown embed node. Adds the case alongside
the other simple scalar prototypes (iframe, richtext, tspan, etc.).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel vercel Bot temporarily deployed to Preview – backgrounds April 4, 2026 20:45 Inactive
@vercel vercel Bot temporarily deployed to Preview – viewer April 4, 2026 20:45 Inactive
@vercel vercel Bot temporarily deployed to Preview – blog April 4, 2026 20:45 Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/grida-canvas-io/index.ts (2)

332-340: ⚠️ Potential issue | 🟠 Major

text/markdown is declared, but async clipboard decode never emits it.

DecodedItem now includes "text/markdown", but decodeFromClipboardItems() only checks SVG/image MIME types and text/plain. Because there is no text/markdown probe there, markdown clipboard items on the async Clipboard API path are skipped.

💡 Suggested fix
@@
-        // Check for text/plain (may be SVG text or plain text)
+        // Check for text/markdown file-like payload
+        if (types.includes("text/markdown")) {
+          try {
+            const blob = await clipboardItem.getType("text/markdown");
+            const file = new File([blob], "clipboard.md", {
+              type: "text/markdown",
+            });
+            decodedItems.push({ type: "text/markdown", file });
+            continue;
+          } catch {}
+        }
+
+        // Check for text/plain (may be SVG text or plain text)
         if (types.includes("text/plain")) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/grida-canvas-io/index.ts` around lines 332 - 340, The DecodedItem
union includes "text/markdown" but decodeFromClipboardItems() only inspects
image/svg+xml, image/png/jpeg/gif/webp and text/plain, so markdown clipboard
items are never emitted; update decodeFromClipboardItems() to detect and handle
"text/markdown" by probing clipboardItem types for "text/markdown" (or falling
back to text/plain when markdown is expected), read the markdown payload (via
getType/getAsString or the async clipboard API) and emit a DecodedItem with type
"text/markdown" and the markdown string as the value so the declared DecodedItem
variants and runtime decoding stay consistent.

324-330: ⚠️ Potential issue | 🟠 Major

Add round-trip test coverage for MarkdownEmbed union member.

The schema includes MarkdownEmbedNode but neither the Rust test suite (crates/grida-canvas/tests/fbs_roundtrip.rs) nor the TypeScript test suite (packages/grida-canvas-io/__tests__/format-roundtrip.test.ts) has a corresponding round-trip test. Add a gen_markdown_embed function in Rust and a markdown embed test case in TypeScript following the same pattern as existing embed node tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/grida-canvas-io/index.ts` around lines 324 - 330, Add round-trip
test coverage for the MarkdownEmbed union member by implementing a Rust
generator function named gen_markdown_embed in the Rust test suite (used by
crates/grida-canvas/tests/fbs_roundtrip.rs) that creates a MarkdownEmbedNode
instance consistent with other embed generators, and add a corresponding test
case in the TypeScript suite
(packages/grida-canvas-io/__tests__/format-roundtrip.test.ts) that creates a
markdown embed node (matching the MarkdownEmbedNode shape) and asserts
successful round-trip using the existing test helpers; ensure field names and
types match the schema and include the markdown content and any file/type fields
so the node mirrors other embed tests.
editor/grida-canvas-react/use-data-transfer.ts (1)

426-445: ⚠️ Potential issue | 🟠 Major

text/plain can preempt markdown embeds during paste.

In mixed clipboard payloads, a "text" item may be encountered before "text/markdown", causing plain text insertion and early return. This can bypass the new markdown-embed flow.

Proposed fix
-      for (const item of items) {
+      const prioritizedItems = [
+        ...items.filter((item) => item.type !== "text"),
+        ...items.filter((item) => item.type === "text"),
+      ];
+      for (const item of prioritizedItems) {
         try {
           switch (item.type) {
             case "svg-text": {
               insertSVG("SVG", item.svg, position);
               return true;
             }
-            case "text": {
-              insertText(item.text, position);
-              return true;
-            }
             case "image/gif":
             case "image/jpeg":
             case "image/png":
             case "image/svg+xml":
             case "image/webp":
             case "text/markdown": {
               insertFromFile(item.type, item.file, position);
               return true;
             }
+            case "text": {
+              insertText(item.text, position);
+              return true;
+            }
           }
         } catch {}
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@editor/grida-canvas-react/use-data-transfer.ts` around lines 426 - 445, The
paste loop in use-data-transfer.ts currently handles a "text" item immediately
which can preempt a later "text/markdown" item; update the switch in the for
(const item of items) loop so that when encountering item.type === "text" you
first check whether items contains any "text/markdown" (e.g., items.some(i =>
i.type === "text/markdown")), and if so skip handling the plain text (continue)
so the markdown branch (which calls insertFromFile for "text/markdown") gets
priority; keep existing handlers insertText, insertFromFile, and insertSVG
unchanged and still return true after successful insertion.
packages/grida-canvas-io/format.ts (1)

393-413: ⚠️ Potential issue | 🟠 Major

Add round-trip test coverage for markdown_embed union member.

The codec mappings are correct, but MarkdownEmbed is missing from both the TypeScript base objects (packages/grida-canvas-io/__tests__/format-roundtrip.test.ts) and Rust test generators (crates/grida-canvas/tests/fbs_roundtrip.rs). Add test cases for the new union member in both files to prevent silent codec drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/grida-canvas-io/format.ts` around lines 393 - 413, Add round-trip
test coverage for the new MarkdownEmbed union member: update the TypeScript base
objects used by the format roundtrip test (refer to the test file symbol
format-roundtrip.test.ts and the base object fixtures used there) to include a
node with type "markdown_embed" (matching the NODE_TYPE_DECODE/NODE_TYPE_ENCODE
mapping) and update the Rust FBS roundtrip generator tests (refer to
crates/grida-canvas tests/fbs_roundtrip.rs) to emit a corresponding
fbs::MarkdownEmbed case so the serializer/deserializer path is exercised; ensure
the test objects use the same identifying property names as the existing union
members so the round-trip assertions will include markdown_embed.
🧹 Nitpick comments (4)
crates/grida-canvas/src/io/io_grida_fbs.rs (1)

2054-2088: Add explicit round-trip tests for markdown_embed.

The new decoder path should be covered by Rust round-trip tests in crates/grida-canvas/tests/fbs_roundtrip.rs and TypeScript round-trip tests in packages/grida-canvas-io/__tests__/format-roundtrip.test.ts to ensure markdown, fill_paints, and corner_radius survive encode→decode→encode cycles.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/grida-canvas/src/io/io_grida_fbs.rs` around lines 2054 - 2088, Add
round-trip tests that exercise the new markdown_embed decoder: in Rust, add
cases to crates/grida-canvas/tests/fbs_roundtrip.rs that construct an fbs
MarkdownEmbed node with non-empty markdown text, non-empty fill_paints, and a
corner_radius, encode it, decode via decode_markdown_embed_node (and related
decode functions), then re-encode and assert markdown, fill_paints (Paints) and
corner_radius remain equal; in TypeScript, add analogous tests to
packages/grida-canvas-io/__tests__/format-roundtrip.test.ts that build the same
markdown_embed payload, run encode→decode→encode and assert the markdown string,
fill_paints array and cornerRadius survive the round-trip. Ensure tests
reference the MarkdownEmbedNodeRec shape and use the same helper encode/decode
utilities used by other round-trip tests.
crates/grida-canvas/src/io/io_grida.rs (1)

1669-1715: Consider adding a focused conversion test for JSONMarkdownEmbedNode.

A small unit test asserting markdown, fills, corner radius, and layout mapping would reduce regression risk for this new conversion path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/grida-canvas/src/io/io_grida.rs` around lines 1669 - 1715, Add a
focused unit test that constructs a JSONMarkdownEmbedNode with non-default
values for markdown, base.fill and base.fill_paints, base.corner_radius and
rectangular corner radii, and base.layout_positioning, then converts it via
From<JSONMarkdownEmbedNode> (the impl for Node) and asserts the resulting Node
is Node::MarkdownEmbed containing a MarkdownEmbedNodeRec whose markdown string,
fills (check merge_paints result), corner_radius (check merge_corner_radius
result), and layout_child.layout_positioning/layout_grow match the expected
mapped values; target the conversion code paths referenced by
JSONMarkdownEmbedNode, MarkdownEmbedNodeRec, merge_paints, merge_corner_radius,
and LayoutChildStyle to catch regressions.
editor/grida-canvas-react/use-data-transfer.ts (2)

177-177: Filename normalization for markdown nodes is fragile.

file.name.split(".md")[0] is extension- and case-sensitive and can produce unexpected names. Prefer extension-aware trimming with a case-insensitive suffix check.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@editor/grida-canvas-react/use-data-transfer.ts` at line 177, The filename
normalization using file.name.split(".md")[0] is brittle and case-sensitive;
replace it with an extension-aware, case-insensitive trim (e.g., use a
case-insensitive regex to strip a trailing ".md" or use
path.parse(file.name).name) so the computed name variable correctly handles
".md", ".MD", and filenames containing ".md" in the middle; update the
assignment to name (in use-data-transfer.ts where const name = file.name...)
accordingly.

442-445: Add/refresh manual interaction tests for markdown paste/drop flows.

This changes user interaction behavior in grida-canvas; please add/update related manual test cases under the canvas test area for markdown paste and drag-drop.

As per coding guidelines: When fixing UX bugs or changing interaction behavior in grida-canvas*, check ../test/ for related manual test cases and add new ones when appropriate.

Also applies to: 561-659

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@editor/grida-canvas-react/use-data-transfer.ts` around lines 442 - 445, The
change adds handling for "text/markdown" by calling insertFromFile(item.type,
item.file, position) during paste/drag-drop, so update the manual interaction
tests for markdown paste and drag-drop in the canvas test suite: add/refresh
tests that exercise pasting markdown clipboard content and dragging-dropping
markdown files to verify insertFromFile is invoked and the markdown
renders/positions correctly (cover both paste and drop flows referenced around
the "text/markdown" branch and the surrounding useDataTransfer handler), and
ensure any related cases between the other referenced region are updated to
reflect the new UX behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/grida-canvas/src/io/id_converter.rs`:
- Line 172: Add round-trip coverage for the JSONNode::MarkdownEmbed ->
Node::from(JSONNode::MarkdownEmbed(...)) mapping by adding test cases that
construct a MarkdownEmbed union member and assert round-trip equality; update
the Rust round-trip test module (fbs_roundtrip.rs) to include a
JSONNode::MarkdownEmbed case in the same pattern as Image/BooleanOperation
tests, and add the corresponding case to the TypeScript format-roundtrip.test.ts
suite so the MarkdownEmbed payload is serialized and deserialized and compared
for equality.

In `@crates/grida-canvas/src/io/io_grida_fbs.rs`:
- Around line 378-384: The encode_node function is missing an arm to handle
fbs::Node::MarkdownEmbedNode so markdown-embed nodes get treated as UnknownNode
and lose their payload on re-save; add a matching encode arm for
fbs::Node::MarkdownEmbedNode that mirrors the decode path by calling the
corresponding encoder (the counterpart to decode_markdown_embed_node) and using
the same slot handling logic as other layer-node cases (see existing
decode_layer_node invocation and other encode arms) so markdown-specific fields
are preserved instead of falling back to UnknownNode.

In `@editor/grida-canvas-react-renderer-dom/nodes/node.tsx`:
- Line 221: The new mapping added for "markdown_embed" to behave like a fill is
incomplete because NodeElement's renderer switch lacks a "markdown_embed" branch
and thus throws "Unknown node type" at runtime; update the switch inside the
NodeElement renderer function (the switch handling node.type) to add a
"markdown_embed" case that delegates to the same rendering path used for other
background/fill nodes (or calls the existing background/fill render helper),
ensuring the node is rendered instead of falling through to the Unknown node
type error.

In `@format/grida.fbs`:
- Around line 1491-1508: The FlatBuffers addition of
MarkdownEmbedNode/MarkdownEmbedNodeProperties was not propagated to TypeScript
and tests: add a MarkdownEmbedNode type to packages/grida-canvas-schema/grida.ts
matching the schema (include properties with fill_paints, markdown,
corner_radius), implement encoder and decoder handling for the Node union
variant in packages/grida-canvas-io/format.ts (serialize/deserialize properties
using existing PaintStackItem and RectangularCornerRadiusTrait helpers and
register the union discriminant for MarkdownEmbedNode), bump the schema_version
constant where the FlatBuffers schema version is maintained to reflect the union
change, and add roundtrip tests: one FBS roundtrip in
crates/grida-canvas/tests/fbs_roundtrip.rs that exercises MarkdownEmbedNode and
one JS/TS roundtrip in
packages/grida-canvas-io/__tests__/format-roundtrip.test.ts that encodes and
decodes a MarkdownEmbedNode instance to ensure fidelity.

In `@packages/grida-canvas-io/format.ts`:
- Around line 1918-1971: The decode path for the encoded markdown_embed is
missing: update the fromFlatbuffer() switch to handle fbs.Node.MarkdownEmbedNode
(the MarkdownEmbedNode case) and reconstruct a
grida.program.nodes.MarkdownEmbedNode by reading properties added in format.ts
(fillPaints, markdown string, corner radius via
format.shape.decode.rectangularCornerRadiusTrait or equivalent decoder, and
system/layer traits). Ensure you map the flatbuffer fields back to the node
shape (markdown, fill paints, rectangular_corner_radius_* and corner_smoothing)
and return the correct node type instead of falling through to the default group
handling.

In `@packages/grida-canvas-io/index.ts`:
- Around line 309-315: The branch that detects markdown uses case-sensitive
checks on the local variable type, so uppercase extensions like ".MD" or "MD"
are missed; normalize type before comparisons (e.g., use a lowercased fallback
value and compare against "text/markdown", "md", and "markdown") by converting
type to lowercase once (e.g., const normalized = (type || "").toLowerCase()) and
use normalized in the existing conditional around the code that returns [true,
"text/markdown" as const].

In `@packages/grida-canvas-schema/grida.ts`:
- Around line 1390-1392: NodePrototype now supports markdown but
createNodeDataFromPrototypeWithoutChildren doesn’t handle it; add a
"markdown_embed" branch in the switch inside
createNodeDataFromPrototypeWithoutChildren to materialize MarkdownEmbedNode
prototypes into runtime node data. Implement the case to read the prototype
payload (the __TPrototypeNode / NodePrototype entry for MarkdownEmbedNode),
copy/merge relevant fields (id, type, metadata and the markdown-specific fields
from MarkdownEmbedNode), apply the same defaulting/omission logic used for other
node types, and return the constructed node object so prototype materialization
no longer throws.

---

Outside diff comments:
In `@editor/grida-canvas-react/use-data-transfer.ts`:
- Around line 426-445: The paste loop in use-data-transfer.ts currently handles
a "text" item immediately which can preempt a later "text/markdown" item; update
the switch in the for (const item of items) loop so that when encountering
item.type === "text" you first check whether items contains any "text/markdown"
(e.g., items.some(i => i.type === "text/markdown")), and if so skip handling the
plain text (continue) so the markdown branch (which calls insertFromFile for
"text/markdown") gets priority; keep existing handlers insertText,
insertFromFile, and insertSVG unchanged and still return true after successful
insertion.

In `@packages/grida-canvas-io/format.ts`:
- Around line 393-413: Add round-trip test coverage for the new MarkdownEmbed
union member: update the TypeScript base objects used by the format roundtrip
test (refer to the test file symbol format-roundtrip.test.ts and the base object
fixtures used there) to include a node with type "markdown_embed" (matching the
NODE_TYPE_DECODE/NODE_TYPE_ENCODE mapping) and update the Rust FBS roundtrip
generator tests (refer to crates/grida-canvas tests/fbs_roundtrip.rs) to emit a
corresponding fbs::MarkdownEmbed case so the serializer/deserializer path is
exercised; ensure the test objects use the same identifying property names as
the existing union members so the round-trip assertions will include
markdown_embed.

In `@packages/grida-canvas-io/index.ts`:
- Around line 332-340: The DecodedItem union includes "text/markdown" but
decodeFromClipboardItems() only inspects image/svg+xml, image/png/jpeg/gif/webp
and text/plain, so markdown clipboard items are never emitted; update
decodeFromClipboardItems() to detect and handle "text/markdown" by probing
clipboardItem types for "text/markdown" (or falling back to text/plain when
markdown is expected), read the markdown payload (via getType/getAsString or the
async clipboard API) and emit a DecodedItem with type "text/markdown" and the
markdown string as the value so the declared DecodedItem variants and runtime
decoding stay consistent.
- Around line 324-330: Add round-trip test coverage for the MarkdownEmbed union
member by implementing a Rust generator function named gen_markdown_embed in the
Rust test suite (used by crates/grida-canvas/tests/fbs_roundtrip.rs) that
creates a MarkdownEmbedNode instance consistent with other embed generators, and
add a corresponding test case in the TypeScript suite
(packages/grida-canvas-io/__tests__/format-roundtrip.test.ts) that creates a
markdown embed node (matching the MarkdownEmbedNode shape) and asserts
successful round-trip using the existing test helpers; ensure field names and
types match the schema and include the markdown content and any file/type fields
so the node mirrors other embed tests.

---

Nitpick comments:
In `@crates/grida-canvas/src/io/io_grida_fbs.rs`:
- Around line 2054-2088: Add round-trip tests that exercise the new
markdown_embed decoder: in Rust, add cases to
crates/grida-canvas/tests/fbs_roundtrip.rs that construct an fbs MarkdownEmbed
node with non-empty markdown text, non-empty fill_paints, and a corner_radius,
encode it, decode via decode_markdown_embed_node (and related decode functions),
then re-encode and assert markdown, fill_paints (Paints) and corner_radius
remain equal; in TypeScript, add analogous tests to
packages/grida-canvas-io/__tests__/format-roundtrip.test.ts that build the same
markdown_embed payload, run encode→decode→encode and assert the markdown string,
fill_paints array and cornerRadius survive the round-trip. Ensure tests
reference the MarkdownEmbedNodeRec shape and use the same helper encode/decode
utilities used by other round-trip tests.

In `@crates/grida-canvas/src/io/io_grida.rs`:
- Around line 1669-1715: Add a focused unit test that constructs a
JSONMarkdownEmbedNode with non-default values for markdown, base.fill and
base.fill_paints, base.corner_radius and rectangular corner radii, and
base.layout_positioning, then converts it via From<JSONMarkdownEmbedNode> (the
impl for Node) and asserts the resulting Node is Node::MarkdownEmbed containing
a MarkdownEmbedNodeRec whose markdown string, fills (check merge_paints result),
corner_radius (check merge_corner_radius result), and
layout_child.layout_positioning/layout_grow match the expected mapped values;
target the conversion code paths referenced by JSONMarkdownEmbedNode,
MarkdownEmbedNodeRec, merge_paints, merge_corner_radius, and LayoutChildStyle to
catch regressions.

In `@editor/grida-canvas-react/use-data-transfer.ts`:
- Line 177: The filename normalization using file.name.split(".md")[0] is
brittle and case-sensitive; replace it with an extension-aware, case-insensitive
trim (e.g., use a case-insensitive regex to strip a trailing ".md" or use
path.parse(file.name).name) so the computed name variable correctly handles
".md", ".MD", and filenames containing ".md" in the middle; update the
assignment to name (in use-data-transfer.ts where const name = file.name...)
accordingly.
- Around line 442-445: The change adds handling for "text/markdown" by calling
insertFromFile(item.type, item.file, position) during paste/drag-drop, so update
the manual interaction tests for markdown paste and drag-drop in the canvas test
suite: add/refresh tests that exercise pasting markdown clipboard content and
dragging-dropping markdown files to verify insertFromFile is invoked and the
markdown renders/positions correctly (cover both paste and drop flows referenced
around the "text/markdown" branch and the surrounding useDataTransfer handler),
and ensure any related cases between the other referenced region are updated to
reflect the new UX behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d920b64a-f906-4f71-88a9-412c93138933

📥 Commits

Reviewing files that changed from the base of the PR and between 319c175 and 2a4d733.

⛔ Files ignored due to path filters (1)
  • crates/grida-canvas/src/io/generated/grida.rs is excluded by !**/generated/**
📒 Files selected for processing (13)
  • crates/grida-canvas/src/io/id_converter.rs
  • crates/grida-canvas/src/io/io_grida.rs
  • crates/grida-canvas/src/io/io_grida_fbs.rs
  • editor/grida-canvas-hosted/ai/tools/canvas-use.ts
  • editor/grida-canvas-react-renderer-dom/nodes/node.tsx
  • editor/grida-canvas-react/use-data-transfer.ts
  • editor/grida-canvas/editor.i.ts
  • editor/grida-canvas/editor.ts
  • editor/grida-canvas/reducers/tools/initial-node.ts
  • format/grida.fbs
  • packages/grida-canvas-io/format.ts
  • packages/grida-canvas-io/index.ts
  • packages/grida-canvas-schema/grida.ts

JSONNode::TextSpan(text) => Node::TextSpan(TextSpanNodeRec::from(text)),
JSONNode::BooleanOperation(bool_op) => Node::from(JSONNode::BooleanOperation(bool_op)),
JSONNode::Image(image) => Node::from(JSONNode::Image(image)),
JSONNode::MarkdownEmbed(md) => Node::from(JSONNode::MarkdownEmbed(md)),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify MarkdownEmbed coverage exists in Rust and TS round-trip tests
rg -n --iglob '**/fbs_roundtrip.rs' --iglob '**/format-roundtrip.test.ts' 'MarkdownEmbed|markdown_embed'

Repository: gridaco/grida

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check if MarkdownEmbed exists in the schema
find . -name "grida.fbs" -type f

Repository: gridaco/grida

Length of output: 75


🏁 Script executed:

#!/bin/bash
# Look for round-trip test files
find . -name "fbs_roundtrip.rs" -o -name "format-roundtrip.test.ts" | head -20

Repository: gridaco/grida

Length of output: 163


🏁 Script executed:

#!/bin/bash
# Search for MarkdownEmbed in the codebase to see where it's defined
rg -i "MarkdownEmbed" --type rust --type ts -B 2 -A 2 | head -100

Repository: gridaco/grida

Length of output: 6170


🏁 Script executed:

#!/bin/bash
# Check the structure of the Rust round-trip test file
wc -l ./crates/grida-canvas/tests/fbs_roundtrip.rs

Repository: gridaco/grida

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Check the structure of the TS round-trip test file
wc -l ./packages/grida-canvas-io/__tests__/format-roundtrip.test.ts

Repository: gridaco/grida

Length of output: 123


🏁 Script executed:

#!/bin/bash
# Look at the Rust round-trip test to understand coverage patterns
head -100 ./crates/grida-canvas/tests/fbs_roundtrip.rs

Repository: gridaco/grida

Length of output: 3766


🏁 Script executed:

#!/bin/bash
# Check what union members ARE tested in the Rust round-trip test
rg -n "Node::" ./crates/grida-canvas/tests/fbs_roundtrip.rs | head -30

Repository: gridaco/grida

Length of output: 1717


🏁 Script executed:

#!/bin/bash
# Check what node types ARE covered in TS round-trip test
rg -n "text_node|container_node|shape_node" ./packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | head -30

Repository: gridaco/grida

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Find the Node enum definition to see all union members
rg -A 50 "^pub enum Node" crates/grida-canvas/src/node/schema.rs | head -80

Repository: gridaco/grida

Length of output: 2266


🏁 Script executed:

#!/bin/bash
# Look at the TS round-trip test file structure to understand what's being tested
head -200 ./packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | tail -100

Repository: gridaco/grida

Length of output: 2098


🏁 Script executed:

#!/bin/bash
# Search for actual test cases in the TS file to see node coverage
rg "describe|it\(" ./packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | head -40

Repository: gridaco/grida

Length of output: 2166


Add MarkdownEmbed coverage to round-trip tests.

The wiring at line 172 is correct, but MarkdownEmbed is missing from both the Rust (crates/grida-canvas/tests/fbs_roundtrip.rs) and TypeScript (packages/grida-canvas-io/__tests__/format-roundtrip.test.ts) round-trip test suites. Add test cases for this union member following the pattern of other node types (Image, BooleanOperation, etc.) already covered in those files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/grida-canvas/src/io/id_converter.rs` at line 172, Add round-trip
coverage for the JSONNode::MarkdownEmbed ->
Node::from(JSONNode::MarkdownEmbed(...)) mapping by adding test cases that
construct a MarkdownEmbed union member and assert round-trip equality; update
the Rust round-trip test module (fbs_roundtrip.rs) to include a
JSONNode::MarkdownEmbed case in the same pattern as Image/BooleanOperation
tests, and add the corresponding case to the TypeScript format-roundtrip.test.ts
suite so the MarkdownEmbed payload is serialized and deserialized and compared
for equality.

Comment on lines +378 to +384
fbs::Node::MarkdownEmbedNode => {
decode_layer_node!(
slot,
node_as_markdown_embed_node,
decode_markdown_embed_node
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing Node::MarkdownEmbed encode arm causes lossy saves.

Line 378 adds decode support, but encode_node still falls through to the UnknownNode fallback (Line 2538) for markdown embeds. That will drop markdown-specific payload on re-encode.

💡 Minimal fix direction
fn encode_node<'a, A: flatbuffers::Allocator + 'a>(
    fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>,
    node: &Node,
    node_id: &str,
    parent_id: &str,
    position: &str,
) -> flatbuffers::WIPOffset<fbs::NodeSlot<'a>> {
    match node {
+       Node::MarkdownEmbed(r) => {
+           encode_markdown_embed_node(fbb, r, node_id, parent_id, position)
+       }
        Node::Container(r) => encode_container_node(fbb, r, node_id, parent_id, position),
        // ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/grida-canvas/src/io/io_grida_fbs.rs` around lines 378 - 384, The
encode_node function is missing an arm to handle fbs::Node::MarkdownEmbedNode so
markdown-embed nodes get treated as UnknownNode and lose their payload on
re-save; add a matching encode arm for fbs::Node::MarkdownEmbedNode that mirrors
the decode path by calling the corresponding encoder (the counterpart to
decode_markdown_embed_node) and using the same slot handling logic as other
layer-node cases (see existing decode_layer_node invocation and other encode
arms) so markdown-specific fields are preserved instead of falling back to
UnknownNode.

component: "background",
iframe: "background",
richtext: "color",
markdown_embed: "background",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

markdown_embed is mapped for fill, but it is still unrenderable in NodeElement.

Line 221 adds fill behavior, but NodeElement still has no "markdown_embed" branch in the renderer switch, so it will hit Unknown node type at runtime.

Proposed fix
-      case "richtext": {
+      case "richtext":
+      case "markdown_embed": {
         return ReactNodeRenderers[node.type];
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@editor/grida-canvas-react-renderer-dom/nodes/node.tsx` at line 221, The new
mapping added for "markdown_embed" to behave like a fill is incomplete because
NodeElement's renderer switch lacks a "markdown_embed" branch and thus throws
"Unknown node type" at runtime; update the switch inside the NodeElement
renderer function (the switch handling node.type) to add a "markdown_embed" case
that delegates to the same rendering path used for other background/fill nodes
(or calls the existing background/fill render helper), ensuring the node is
rendered instead of falling through to the Unknown node type error.

Comment thread format/grida.fbs
Comment on lines +1491 to +1508
table MarkdownEmbedNodeProperties {
/// Background fill paints for the markdown container.
fill_paints:[PaintStackItem];
/// The GitHub Flavored Markdown source text.
markdown:string;
/// Per-corner rectangular corner radius.
corner_radius:RectangularCornerRadiusTrait;
}

/// Node variant: Markdown embed (render-only).
///
/// Stores GFM markdown source and renders it directly to a Skia Picture
/// at paint time. Not structurally editable — treated as a single text block.
table MarkdownEmbedNode {
node:SystemNodeTrait (required);
layer:LayerTrait (required);
properties:MarkdownEmbedNodeProperties;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Schema version definitions =="
rg -n "SCHEMA_VERSION|schema_version" --glob "*grida.ts" --glob "*document*" --glob "*format*"

echo
echo "== MarkdownEmbed round-trip coverage =="
rg -n "MarkdownEmbed|markdown_embed" --glob "*fbs_roundtrip.rs" --glob "*format-roundtrip.test.ts"

Repository: gridaco/grida

Length of output: 762


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Finding the union definition for MarkdownEmbedNode =="
rg -n "union.*Node|MarkdownEmbed" format/grida.fbs | head -20

echo
echo "== Checking CanvasDocument.schema_version in grida.fbs =="
rg -n "schema_version" format/grida.fbs -A 2 -B 2

echo
echo "== Reading roundtrip test files for MarkdownEmbed coverage =="
echo "--- Rust roundtrip test ---"
rg -n "MarkdownEmbed\|markdown_embed\|test.*node" crates/grida-canvas/tests/fbs_roundtrip.rs | head -20

echo
echo "--- TS roundtrip test ---"
rg -n "MarkdownEmbed\|markdown_embed\|it\(" packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | head -30

Repository: gridaco/grida

Length of output: 1027


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Reading grida.fbs to find actual schema_version value =="
sed -n '1620,1650p' format/grida.fbs

echo
echo "== Looking for schema_version assignment in grida.fbs =="
rg -n "schema_version.*=|\"0\.91|version" format/grida.fbs -B 2 -A 2 | head -30

echo
echo "== Checking for other node roundtrip tests (Rust) =="
rg -n "fn test_|assert.*Node|SceneNode\|ContainerNode" crates/grida-canvas/tests/fbs_roundtrip.rs | head -15

echo
echo "== Checking for other node roundtrip tests (TS) =="
rg -n "describe\(|it\(|MarkdownEmbed|Node" packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | head -20

Repository: gridaco/grida

Length of output: 2062


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== List all files in roundtrip test directories =="
find crates/grida-canvas/tests -name "*roundtrip*" -o -name "*test*" | head -20

echo
find packages/grida-canvas-io/__tests__ -name "*roundtrip*" | head -20

echo
echo "== Check content and structure of Rust roundtrip test =="
wc -l crates/grida-canvas/tests/fbs_roundtrip.rs
head -100 crates/grida-canvas/tests/fbs_roundtrip.rs

echo
echo "== Check for node test patterns in Rust =="
rg -n "Node\|node" crates/grida-canvas/tests/fbs_roundtrip.rs | head -20

echo
echo "== Check TS roundtrip test structure =="
wc -l packages/grida-canvas-io/__tests__/format-roundtrip.test.ts
head -80 packages/grida-canvas-io/__tests__/format-roundtrip.test.ts

Repository: gridaco/grida

Length of output: 4135


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for actual schema_version value assignments =="
rg -n "schema_version\s*=" format/grida.fbs

echo
echo "== Get line 1623 context from grida.fbs to find value =="
sed -n '1615,1635p' format/grida.fbs

echo
echo "== Search complete TS test file for MarkdownEmbed =="
rg "MarkdownEmbed" packages/grida-canvas-io/__tests__/format-roundtrip.test.ts

echo
echo "== List all test/describe/it functions in TS roundtrip test =="
rg "describe\(|it\(" packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | head -30

echo
echo "== List all test functions in Rust roundtrip test =="
rg "fn test_|fn gen_" crates/grida-canvas/tests/fbs_roundtrip.rs | head -30

echo
echo "== Search for markdown in Rust test (any variant) =="
rg -i "markdown" crates/grida-canvas/tests/fbs_roundtrip.rs

Repository: gridaco/grida

Length of output: 113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check where schema_version is actually set at encode time =="
rg -n "schema_version\|SCHEMA_VERSION" packages/grida-canvas-io/format.ts | head -20

echo
echo "== Get TS SCHEMA_VERSION value =="
sed -n '530,540p' packages/grida-canvas-schema/grida.ts

echo
echo "== List all gen_* or test_* functions in Rust roundtrip =="
rg "^fn (gen_|test_)" crates/grida-canvas/tests/fbs_roundtrip.rs

echo
echo "== Check if there's a recent change history in grida.ts SCHEMA_VERSION =="
git log --oneline -n 20 --all -- packages/grida-canvas-schema/grida.ts 2>/dev/null || echo "(git not available)"

echo
echo "== Check git diff to see if schema_version was changed in this PR =="
git diff HEAD~1 format/grida.fbs | grep -A 5 -B 5 "schema_version\|MarkdownEmbed" || echo "(no changes in schema_version area)"

Repository: gridaco/grida

Length of output: 120


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Read TS SCHEMA_VERSION directly =="
cat packages/grida-canvas-schema/grida.ts | grep -n "SCHEMA_VERSION" | head -5

echo
echo "== Check format.ts for schema_version handling =="
cat packages/grida-canvas-io/format.ts | grep -n "schema_version\|SCHEMA_VERSION" | head -10

echo
echo "== List all test function names in fbs_roundtrip.rs =="
cat crates/grida-canvas/tests/fbs_roundtrip.rs | grep -n "^fn " | head -40

echo
echo "== Check for MarkdownEmbed/markdown tests explicitly =="
cat crates/grida-canvas/tests/fbs_roundtrip.rs | grep -in "markdown"

echo
echo "== Check TS roundtrip test file exists and list tests =="
cat packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | grep -n "describe\|it(" | head -30

echo
echo "== Search for MarkdownEmbed in TS test =="
cat packages/grida-canvas-io/__tests__/format-roundtrip.test.ts | grep -in "markdown"

Repository: gridaco/grida

Length of output: 2167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check complete TS test file for any markdown pattern =="
wc -l packages/grida-canvas-io/__tests__/format-roundtrip.test.ts
tail -100 packages/grida-canvas-io/__tests__/format-roundtrip.test.ts

echo
echo "== Verify MarkdownEmbedNode is in the Node union =="
sed -n '1590,1610p' format/grida.fbs

echo
echo "== Check if there's a default value for schema_version in FBS =="
sed -n '1621,1626p' format/grida.fbs

echo
echo "== List how encode sets schema_version =="
rg -n "encode.*schema\|createCanvasDocument" packages/grida-canvas-io/format.ts -B 3 -A 5 | head -40

Repository: gridaco/grida

Length of output: 3725


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for where schema_version is written during encode =="
rg -n "schema_version.*=" packages/grida-canvas-io/format.ts -B 2 -A 2 | head -30

echo
echo "== Check encoder implementation for CanvasDocument creation =="
rg -n "CanvasDocument\|builder.*schema" packages/grida-canvas-io/format.ts -B 1 -A 3 | head -40

echo
echo "== Verify no MarkdownEmbed test exists anywhere in roundtrip =="
echo "Rust test:"
grep -c "MarkdownEmbed\|markdown_embed\|gen_markdown" crates/grida-canvas/tests/fbs_roundtrip.rs || echo "0 matches"

echo
echo "TS test:"
grep -c "MarkdownEmbed\|markdown_embed\|markdown" packages/grida-canvas-io/__tests__/format-roundtrip.test.ts || echo "0 matches"

echo
echo "== Check if this is the initial commit or if schema needs bump =="
git log --oneline -1 --all format/grida.fbs 2>/dev/null || echo "n/a"
git log --oneline -1 --all packages/grida-canvas-schema/grida.ts 2>/dev/null || echo "n/a"

Repository: gridaco/grida

Length of output: 119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Read the encoder function that creates CanvasDocument =="
rg -n "fn.*encode|impl.*Encode" packages/grida-canvas-io/format.ts -A 10 | head -50

echo
echo "== Check for any constant or version string in grida.fbs =="
grep -n "0\.91\|version\|Version" format/grida.fbs

echo
echo "== Search broadly for CanvasDocument builder in format.ts =="
rg -n "CanvasDocument\|CanvasDocumentBuilder" packages/grida-canvas-io/format.ts | head -20

echo
echo "== Check the io_grida_fbs.rs Rust encoder =="
find . -name "*io*grida*" -type f | grep -E "\.(rs|ts)$"
</parameter>

Repository: gridaco/grida

Length of output: 116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== List Rust encoder files =="
find crates/grida-canvas -name "*io*" -type f | head -10

echo
echo "== Check io_grida_fbs module =="
find . -path "*/io/io_grida_fbs*" -type f

echo
echo "== Read format.ts around line 4977 where SCHEMA_VERSION is used =="
sed -n '4970,4985p' packages/grida-canvas-io/format.ts

echo
echo "== Check if schema_version is set in the document creation =="
sed -n '4900,4980p' packages/grida-canvas-io/format.ts | grep -n "schemaVersion\|schema_version\|SCHEMA_VERSION" -B 5 -A 5

Repository: gridaco/grida

Length of output: 1856


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for where schemaVersion is passed to builder =="
sed -n '4977,5050p' packages/grida-canvas-io/format.ts | grep -n "schemaVersion\|CanvasDocument\|addSchema" -B 3 -A 3

echo
echo "== Search for CanvasDocument builder calls =="
rg -n "CanvasDocumentStart\|CanvasDocumentAdd" packages/grida-canvas-io/format.ts | head -10

echo
echo "== Read the io_grida_fbs encoder to see what schema version it uses =="
sed -n '1,50p' crates/grida-canvas/src/io/io_grida_fbs.rs

echo
echo "== Check if MarkdownEmbed appears in io_grida_fbs.rs at all =="
grep -n "MarkdownEmbed\|markdown" crates/grida-canvas/src/io/io_grida_fbs.rs

Repository: gridaco/grida

Length of output: 703


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for CanvasDocument builder in format.ts =="
rg -n "CanvasDocument\|CanvasDocumentStart\|CanvasDocumentAddSchemaVersion" packages/grida-canvas-io/format.ts -B 2 -A 5 | head -60

echo
echo "== Look for flatbuffers builder.finish and root =="
rg -n "builder.finish\|CanvasDocumentStart" packages/grida-canvas-io/format.ts -B 3 -A 3 | head -40

echo
echo "== Check what addSchemaVersion or similar is called =="
rg -n "addSchema|Schema.*Add" packages/grida-canvas-io/format.ts | head -10

Repository: gridaco/grida

Length of output: 109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Read more of the toFlatbuffer function to see complete implementation =="
sed -n '4977,5150p' packages/grida-canvas-io/format.ts

echo
echo "== Check the end of toFlatbuffer to see what it returns =="
sed -n '5100,5200p' packages/grida-canvas-io/format.ts

Repository: gridaco/grida

Length of output: 10297


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check if MarkdownEmbed decoder exists in format.ts =="
rg -n "markdown\|MarkdownEmbed" packages/grida-canvas-io/format.ts -B 2 -A 8 | head -50

echo
echo "== Check Rust schema.rs for MarkdownEmbed =="
rg -n "MarkdownEmbed\|markdown" crates/grida-canvas/src/node/schema.rs | head -20

echo
echo "== Check TS grida.ts for MarkdownEmbed node type =="
rg -n "MarkdownEmbed" packages/grida-canvas-schema/grida.ts | head -20

echo
echo "== Confirm schema_version in TS model matches the constant =="
sed -n '530,545p' packages/grida-canvas-schema/grida.ts

Repository: gridaco/grida

Length of output: 113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check if MarkdownEmbedNode type is defined in TS =="
rg -n "MarkdownEmbedNode\|type MarkdownEmbed" packages/grida-canvas-schema/grida.ts | head -20

echo
echo "== List all node types defined in TS grida.ts =="
rg -n "export type.*Node\s*=" packages/grida-canvas-schema/grida.ts | tail -30

echo
echo "== Check what node types are in the union type =="
sed -n '500,700p' packages/grida-canvas-schema/grida.ts | grep -n "Node\|Union"

echo
echo "== Check if encoder/decoder code exists for MarkdownEmbed in format.ts =="
wc -l packages/grida-canvas-io/format.ts
tail -500 packages/grida-canvas-io/format.ts | head -100

Repository: gridaco/grida

Length of output: 111


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== List ALL node type definitions in grida.ts =="
rg -n "export.*\bNode\b" packages/grida-canvas-schema/grida.ts | grep "type\|interface" | head -40

echo
echo "== Check the union type definition in grida.ts =="
sed -n '600,900p' packages/grida-canvas-schema/grida.ts

echo
echo "== Search for all 'Node' in grida.ts to see structure =="
grep -n "^\s*export type\|^\s*\| Node\s*\|^\s*interface" packages/grida-canvas-schema/grida.ts | tail -60

Repository: gridaco/grida

Length of output: 12061


Add missing TypeScript codec and roundtrip test coverage for MarkdownEmbedNode.

The FlatBuffers schema adds MarkdownEmbedNode to the Node union (line 1604), but the TypeScript model in packages/grida-canvas-schema/grida.ts lacks a corresponding MarkdownEmbedNode type, and packages/grida-canvas-io/format.ts has no encoder/decoder for it. Additionally:

  • No roundtrip test exists for MarkdownEmbedNode in crates/grida-canvas/tests/fbs_roundtrip.rs or packages/grida-canvas-io/__tests__/format-roundtrip.test.ts
  • schema_version in the FlatBuffers schema was not bumped to reflect this union member addition

Implement the TS type, add encoder/decoder in format.ts, bump schema_version, and add explicit roundtrip coverage for the new variant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@format/grida.fbs` around lines 1491 - 1508, The FlatBuffers addition of
MarkdownEmbedNode/MarkdownEmbedNodeProperties was not propagated to TypeScript
and tests: add a MarkdownEmbedNode type to packages/grida-canvas-schema/grida.ts
matching the schema (include properties with fill_paints, markdown,
corner_radius), implement encoder and decoder handling for the Node union
variant in packages/grida-canvas-io/format.ts (serialize/deserialize properties
using existing PaintStackItem and RectangularCornerRadiusTrait helpers and
register the union discriminant for MarkdownEmbedNode), bump the schema_version
constant where the FlatBuffers schema version is maintained to reflect the union
change, and add roundtrip tests: one FBS roundtrip in
crates/grida-canvas/tests/fbs_roundtrip.rs that exercises MarkdownEmbedNode and
one JS/TS roundtrip in
packages/grida-canvas-io/__tests__/format-roundtrip.test.ts that encodes and
decodes a MarkdownEmbedNode instance to ensure fidelity.

Comment on lines +1918 to +1971
case "markdown_embed": {
const mdNode = node as grida.program.nodes.MarkdownEmbedNode;

const fillPaintsFiltered = paints(mdNode, "fill");
const fillPaintsOffset = format.paint.encode.fillPaints(
builder,
fillPaintsFiltered,
fbs.MarkdownEmbedNodeProperties.createFillPaintsVector
);
const markdownOffset = builder.createString(mdNode.markdown ?? "");
const mdCornerUniform = mdNode.corner_radius;
const cornerRadiusOffset =
format.shape.encode.rectangularCornerRadiusTrait(builder, {
rectangular_corner_radius_top_left:
mdNode.rectangular_corner_radius_top_left ?? mdCornerUniform,
rectangular_corner_radius_top_right:
mdNode.rectangular_corner_radius_top_right ?? mdCornerUniform,
rectangular_corner_radius_bottom_left:
mdNode.rectangular_corner_radius_bottom_left ??
mdCornerUniform,
rectangular_corner_radius_bottom_right:
mdNode.rectangular_corner_radius_bottom_right ??
mdCornerUniform,
corner_smoothing: mdNode.corner_smoothing,
});

fbs.MarkdownEmbedNodeProperties.startMarkdownEmbedNodeProperties(
builder
);
fbs.MarkdownEmbedNodeProperties.addFillPaints(
builder,
fillPaintsOffset
);
fbs.MarkdownEmbedNodeProperties.addMarkdown(
builder,
markdownOffset
);
fbs.MarkdownEmbedNodeProperties.addCornerRadius(
builder,
cornerRadiusOffset
);
const propertiesOffset =
fbs.MarkdownEmbedNodeProperties.endMarkdownEmbedNodeProperties(
builder
);

fbs.MarkdownEmbedNode.startMarkdownEmbedNode(builder);
fbs.MarkdownEmbedNode.addNode(builder, systemNodeTraitOffset);
fbs.MarkdownEmbedNode.addLayer(builder, layerOffset);
fbs.MarkdownEmbedNode.addProperties(builder, propertiesOffset);
nodeOffset = fbs.MarkdownEmbedNode.endMarkdownEmbedNode(builder);
nodeType = fbs.Node.MarkdownEmbedNode;
break;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

markdown_embed is encoded at Line [1918], but TypeScript decode never reconstructs it.

fromFlatbuffer() has no fbs.Node.MarkdownEmbedNode case, so markdown nodes fall through to the default group path, dropping markdown content and style properties.

🔧 Proposed fix (add TS decode path for MarkdownEmbedNode)
@@ export namespace document.decode.nodeTypes {
+        export function markdownEmbed(
+          n: fbs.MarkdownEmbedNode,
+          id: string,
+          systemNode: fbs.SystemNodeTrait,
+          layer: fbs.LayerTrait | null,
+          opacity: number,
+          layoutFields: ReturnType<typeof format.layout.decode.nodeLayout>,
+          effects?: grida.program.nodes.i.IEffects
+        ): grida.program.nodes.MarkdownEmbedNode {
+          const props = n.properties();
+          const fillPaints = props ? format.paint.decode.fillPaints(props) : undefined;
+          const cr = format.shape.decode.rectangularCornerRadiusTrait(
+            props?.cornerRadius() ?? null
+          );
+
+          return {
+            type: "markdown_embed",
+            id,
+            name: systemNode.name() ?? "markdown",
+            active: systemNode.active() ?? true,
+            locked: systemNode.locked() ?? false,
+            opacity,
+            z_index: 0,
+            ...layoutFields,
+            markdown: props?.markdown() ?? "",
+            ...(fillPaints ? { fill_paints: fillPaints } : {}),
+            rectangular_corner_radius_top_left: cr.rectangular_corner_radius_top_left,
+            rectangular_corner_radius_top_right: cr.rectangular_corner_radius_top_right,
+            rectangular_corner_radius_bottom_left: cr.rectangular_corner_radius_bottom_left,
+            rectangular_corner_radius_bottom_right: cr.rectangular_corner_radius_bottom_right,
+            corner_smoothing: cr.corner_smoothing,
+            ...(effects || {}),
+          } satisfies grida.program.nodes.MarkdownEmbedNode;
+        }
@@ type NodeWithLayer =
-            | fbs.GroupNode;
+            | fbs.GroupNode
+            | fbs.MarkdownEmbedNode;
@@ switch (nodeType) {
+            case fbs.Node.MarkdownEmbedNode:
+              nodes[id] = nodeTypes.markdownEmbed(
+                typedNode as fbs.MarkdownEmbedNode,
+                id,
+                systemNode,
+                layer,
+                opacity,
+                layoutFields,
+                decodedEffects
+              );
+              break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/grida-canvas-io/format.ts` around lines 1918 - 1971, The decode path
for the encoded markdown_embed is missing: update the fromFlatbuffer() switch to
handle fbs.Node.MarkdownEmbedNode (the MarkdownEmbedNode case) and reconstruct a
grida.program.nodes.MarkdownEmbedNode by reading properties added in format.ts
(fillPaints, markdown string, corner radius via
format.shape.decode.rectangularCornerRadiusTrait or equivalent decoder, and
system/layer traits). Ensure you map the flatbuffer fields back to the node
shape (markdown, fill paints, rectangular_corner_radius_* and corner_smoothing)
and return the correct node type instead of falling through to the default group
handling.

Comment thread packages/grida-canvas-io/index.ts Outdated
Comment on lines 309 to 315
} else if (
type === "text/markdown" ||
type === "md" ||
type === "markdown"
) {
return [true, "text/markdown" as const];
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize extension matching to avoid missing uppercase markdown extensions.

When file.type is empty, .MD currently won’t match because the extension checks are case-sensitive. Lowercasing the fallback type/extension avoids false negatives.

💡 Suggested fix
-      const type = file.type || file.name.split(".").pop() || file.name;
+      const type = (
+        file.type ||
+        file.name.split(".").pop() ||
+        file.name
+      ).toLowerCase();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/grida-canvas-io/index.ts` around lines 309 - 315, The branch that
detects markdown uses case-sensitive checks on the local variable type, so
uppercase extensions like ".MD" or "MD" are missed; normalize type before
comparisons (e.g., use a lowercased fallback value and compare against
"text/markdown", "md", and "markdown") by converting type to lowercase once
(e.g., const normalized = (type || "").toLowerCase()) and use normalized in the
existing conditional around the code that returns [true, "text/markdown" as
const].

Comment thread packages/grida-canvas-schema/grida.ts
CI cargo build with --examples failed with non-exhaustive match in
examples/tool_io_grida.rs classify_json_node after MarkdownEmbed variant
was added to JSONNode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel vercel Bot temporarily deployed to Preview – blog April 4, 2026 21:03 Inactive
@vercel vercel Bot temporarily deployed to Preview – viewer April 4, 2026 21:03 Inactive
@vercel vercel Bot temporarily deployed to Preview – backgrounds April 4, 2026 21:03 Inactive
@vercel vercel Bot temporarily deployed to Preview – grida April 4, 2026 21:03 Inactive
…nd edge cases

Addresses CodeRabbit review findings on PR #627.

Critical (prevents data loss on save/reopen):
- Add Rust FBS encoder (encode_markdown_embed_node) so Node::MarkdownEmbed
  no longer falls through to the UnknownNode fallback in encode_node.
- Add TS FBS decoder (nodeTypes.markdownEmbed) + fromFlatbuffer match arm
  so MarkdownEmbedNode round-trips through @grida/io without being
  reconstructed as a group.

DOM renderer safety:
- Add MarkdownEmbedWidget as a simple DOM fallback (monospace pre-wrap
  block) and wire markdown_embed into NodeElement's renderer switch.
  The canonical render path is still canvas/Skia — this just prevents
  "Unknown node type" in DOM-rendered subtrees (previews, tests).

.md file detection:
- filetype() now lowercases MIME + extension and checks extension FIRST,
  so .md files still resolve as text/markdown when browsers serve them
  as text/plain, application/octet-stream, or with empty MIME. Also
  accepts .MD, .markdown, .mdown, .mkd extensions.
- File name trimming on drop uses a case-insensitive regex to strip
  trailing .md/.markdown/.mdown/.mkd instead of a brittle split(".md").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel vercel Bot temporarily deployed to Preview – blog April 4, 2026 21:31 Inactive
@vercel vercel Bot temporarily deployed to Preview – viewer April 4, 2026 21:31 Inactive
@vercel vercel Bot temporarily deployed to Preview – backgrounds April 4, 2026 21:31 Inactive
@vercel vercel Bot temporarily deployed to Preview – grida April 4, 2026 21:31 Inactive
@softmarshmallow softmarshmallow merged commit f3bbc11 into canary Apr 4, 2026
15 checks passed
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.

1 participant