When the canvas opens a .md (or other document/plaintext mode file), TiptapEditor renders it as a rich-text editor. This page covers the extension stack, how AI-driven set_content commands reach the editor imperatively, and the auto-save story.

Component shape

The editor lives at src/components/canvas/editors/TiptapEditor.tsx (~230 LOC). It receives initialContent, format ('markdown' | 'html' | 'text'), and an editable flag from CanvasPanel.

Extensions loaded

From TiptapEditor.tsx:78-125. The order matters — extensions can override each other:
ExtensionPurpose
StarterKitBundle of standard nodes/marks (Heading, Bold, Italic, Link, History, etc.). Built-in codeBlock disabled — replaced by CodeBlockLowlight below.
CodeBlockLowlightCode blocks with Shiki-via-lowlight syntax highlighting.
@tiptap/markdownProvides setContent(content, { contentType: 'markdown' }) and editor.getMarkdown() — the markdown round-trip.
PlaceholderEmpty-state placeholder text.
TaskList + TaskItem (nested)GitHub-style task lists.
Table family (Table, TableRow, TableCell, TableHeader)Tables.
ImageInline images, base64 allowed.
FileHandlerDrop/paste handlers — currently deferred (logs only).
OrionVideoCustom video node (extends Image for video sources).
LinkShortcutCmd+K to add link to selection.
SlashCommands/ menu for inserting blocks.
CalloutExtensionInfo/warning/danger callout blocks.
WikiLink[[entity]] autocomplete backed by useParaEntityIndex — links to PARA entities in the vault.
✓ VERIFIED.

Editor ref bridge — how AI updates the editor

The interceptor needs an imperative handle to TipTap. When an agent calls mcp__canvas__canvas(action: 'set_content', body: "..."), the interceptor cannot directly mutate React state — it needs to call editor.commands.setContent() on the live TipTap instance. The bridge (TiptapEditor.tsx:182-206):
useEffect(() => {
  if (!editor) return;
  const store = useCanvasStore.getState();
  store.editorRef.current = editor;

  // Flush any set_content that arrived BEFORE the editor mounted
  if (store.pendingSetContent) {
    editor.commands.setContent(store.pendingSetContent.content, ...);
    store.setPendingSetContent(null);
  }

  return () => {
    // unmount: clear ref so interceptor falls back to pending
    if (store.editorRef.current === editor) {
      store.editorRef.current = null;
    }
  };
}, [editor]);

Why the pending-content queue?

The agent may call set_content BEFORE the editor mounts (e.g., immediately after openFile for a file that doesn’t exist yet). Without the queue, the call would no-op. The interceptor checks editorRef.current — if null, it falls back to setPendingSetContent in the store. Next mount, the editor flushes the queue. ✓ VERIFIED.

Format handling

Three load paths in TiptapEditor.tsx:156-178:
Normalizes inline tasks (- [ ] A - [ ] B → separate lines), then setContent(normalized, { contentType: 'markdown' }).

Auto-save

handleEditorUpdate is the onUpdate callback for the editor. Debounced 2000ms via AUTO_SAVE_DELAY_MS in CanvasPanel.tsx:232-246:
const handleEditorUpdate = useCallback(({ editor }) => {
  if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
  saveTimerRef.current = setTimeout(async () => {
    const output = format === 'markdown' ? editor.getMarkdown() : editor.getHTML();
    await writeCanvasFile(activeFile.filePath, output);
    setLastSaved(Date.now());
  }, AUTO_SAVE_DELAY_MS);
}, [activeFile, format]);
On unmount, the timer flushes (CanvasPanel.tsx:219-230) to prevent data loss when switching files or closing the canvas. ✓ VERIFIED.
Spreadsheets do NOT have auto-save (gap GAP-C3). User must explicitly save. If the app closes while isDirty, changes are lost. See Trust → other gaps.

Custom nodes — what each adds

Custom TipTap node that renders <video controls> for inline video. Extends Image semantics but routes to resolveMediaSrc like the canvas widget catalog does. Allows agents to embed video inline in a document.
/ menu for block insertion: heading, list, task list, table, callout, code block, etc. Inspired by Notion’s slash menu.
Cmd+K (or Ctrl+K) on a selection → opens an inline URL input → on enter, wraps selection in a Link mark.
Block-level callouts with severity (info / warning / danger / success). Renders with colored left border, icon, and bg-orion-* token-aware styling.
Drop/paste handlers registered but currently log-only. Future: paste an image → uploads to vault Resources/media/images/ and inserts inline.

AI-WITH-YOU at the document level

The editor is editable (no read-only enforcement from AI). The gate principle:
AI moveUser seam
set_content overwrites the docUser can immediately undo (Cmd+Z in History extension) or edit freely after — user owns the editor at rest
AI suggests inline editsNOT IMPLEMENTED — TipTap supports it via marks but no Orion feature wired up
AI writes new doc to a fileGoes through openFileeditor.setContent (mounted) or setPendingSetContent (queued). User can close or save-as.
There’s no AI-driven silent rewrite path — every change is either a tool call (which is logged in the activity stream) or a direct file write (also logged). The user owns the editor.

Open questions

  • Markdown round-trip fidelity — when editor.getMarkdown() round-trips a complex doc through TipTap and back, do custom nodes (Callout, WikiLink) survive? Verified to work for standard markdown, but Callout and WikiLink may use Orion-specific extension serializers that aren’t standard markdown.
  • Concurrent edit + AI write — what happens if the user is typing while the agent calls set_content? Does the agent’s content overwrite mid-keystroke? Worth testing.

Key files

The editor component. Extensions L78-125, format handling L156-178, editor ref bridge L182-206.
Auto-save logic L232-246 + L219-230 (flush on unmount). Mode-based rendering switch around L311-370.
Custom TipTap extensions: OrionVideo, WikiLink, SlashCommands, LinkShortcut, CalloutExtension, FileHandler.
editorRef.current, pendingSetContent, setPendingSetContent. The bridge state.

Next

Trust & Known Gaps

Honest safety appendix including GAP-C1 (provenance), GAP-C2 (≤10 elements), GAP-C3 (spreadsheet no auto-save), GAP-C4 (drop silent fail).