.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 atsrc/components/canvas/editors/TiptapEditor.tsx (~230 LOC). It receives initialContent, format ('markdown' | 'html' | 'text'), and an editable flag from CanvasPanel.
Extensions loaded
FromTiptapEditor.tsx:78-125. The order matters — extensions can override each other:
| Extension | Purpose |
|---|---|
StarterKit | Bundle of standard nodes/marks (Heading, Bold, Italic, Link, History, etc.). Built-in codeBlock disabled — replaced by CodeBlockLowlight below. |
CodeBlockLowlight | Code blocks with Shiki-via-lowlight syntax highlighting. |
@tiptap/markdown | Provides setContent(content, { contentType: 'markdown' }) and editor.getMarkdown() — the markdown round-trip. |
Placeholder | Empty-state placeholder text. |
TaskList + TaskItem (nested) | GitHub-style task lists. |
Table family (Table, TableRow, TableCell, TableHeader) | Tables. |
Image | Inline images, base64 allowed. |
FileHandler | Drop/paste handlers — currently deferred (logs only). |
OrionVideo | Custom video node (extends Image for video sources). |
LinkShortcut | Cmd+K to add link to selection. |
SlashCommands | / menu for inserting blocks. |
CalloutExtension | Info/warning/danger callout blocks. |
WikiLink | [[entity]] autocomplete backed by useParaEntityIndex — links to PARA entities in the vault. |
Editor ref bridge — how AI updates the editor
The interceptor needs an imperative handle to TipTap. When an agent callsmcp__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):
Why the pending-content queue?
The agent may callset_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 inTiptapEditor.tsx:156-178:
- markdown
- html
- text
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:
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
OrionVideo
OrionVideo
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.WikiLink
WikiLink
[[entity]] autocomplete. As the user types [[, a popover lists PARA entities from useParaEntityIndex. Selecting an entity inserts a link with the entity’s slug and human-readable name. Renders as a TipTap mark.SlashCommands
SlashCommands
/ menu for block insertion: heading, list, task list, table, callout, code block, etc. Inspired by Notion’s slash menu.LinkShortcut
LinkShortcut
Cmd+K (or Ctrl+K) on a selection → opens an inline URL input → on enter, wraps selection in a
Link mark.CalloutExtension
CalloutExtension
Block-level callouts with severity (info / warning / danger / success). Renders with colored left border, icon, and
bg-orion-* token-aware styling.FileHandler (deferred)
FileHandler (deferred)
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 iseditable (no read-only enforcement from AI). The gate principle:
| AI move | User seam |
|---|---|
set_content overwrites the doc | User can immediately undo (Cmd+Z in History extension) or edit freely after — user owns the editor at rest |
| AI suggests inline edits | NOT IMPLEMENTED — TipTap supports it via marks but no Orion feature wired up |
| AI writes new doc to a file | Goes through openFile → editor.setContent (mounted) or setPendingSetContent (queued). User can close or save-as. |
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
src/components/canvas/editors/TiptapEditor.tsx
src/components/canvas/editors/TiptapEditor.tsx
The editor component. Extensions L78-125, format handling L156-178, editor ref bridge L182-206.
src/components/canvas/CanvasPanel.tsx
src/components/canvas/CanvasPanel.tsx
Auto-save logic L232-246 + L219-230 (flush on unmount). Mode-based rendering switch around L311-370.
src/components/canvas/editors/extensions/
src/components/canvas/editors/extensions/
Custom TipTap extensions:
OrionVideo, WikiLink, SlashCommands, LinkShortcut, CalloutExtension, FileHandler.src/stores/canvasStore.ts
src/stores/canvasStore.ts
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).