Skip to content

feat(files): inline rich markdown editor#5133

Open
waleedlatif1 wants to merge 22 commits into
stagingfrom
feature/inline-rich-markdown-editor
Open

feat(files): inline rich markdown editor#5133
waleedlatif1 wants to merge 22 commits into
stagingfrom
feature/inline-rich-markdown-editor

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replace the raw-markdown / preview split for markdown files with a Linear-style inline WYSIWYG editor (TipTap/ProseMirror) — edits transform inline as you type
  • Bubble menu (selection formatting), / slash menu, code-block language picker with Prism syntax highlighting + line-wrap, resizable images (sized images serialize to HTML <img>), GFM tables, task lists
  • Frontmatter is held byte-exact out of band; a round-trip preflight gate (decided once per open) opens any file that can't be edited losslessly (footnotes, raw HTML, HTML comments/entities, >128KB) as a read-only rich preview rather than risk a lossy edit. Markdown no longer routes to Monaco at all — the rich editor fully replaces it; round-trip-safe files (the overwhelming majority, incl. badges/linked images now) stay fully editable
  • Shared autosave engine hardened (no edit lost when a keystroke lands mid-save), and the <img>/entity/heading-hardbreak/table-<br> data-loss paths are all closed and gated

Type of Change

  • New feature

Testing

  • 67 editor unit tests + 206 file-viewer tests passing (round-trip fidelity, gate safety across ~150 markdown constructs, language detection, reducer); typecheck, biome, and api-validation all green
  • Tested manually in the files view

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@waleedlatif1 waleedlatif1 requested a review from a team as a code owner June 19, 2026 00:32
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 19, 2026 12:29pm

Request Review

@cursor

cursor Bot commented Jun 19, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches persisted file content and autosave concurrency; mitigated by round-trip gating, frontmatter handling, and extensive tests, but the new editor surface is large and user-facing.

Overview
Markdown in workspace files no longer uses the Monaco raw/split/preview flow. They open in a new TipTap-based inline WYSIWYG (RichMarkdownEditor) with bubble formatting, / slash blocks, Prism-highlighted code blocks (language picker, wrap, copy), resizable/linked images, GFM tables and task lists, markdown paste, and image paste/drop uploads. YAML frontmatter stays out of the document and is re-attached on save; isRoundTripSafe runs once at open and forces read-only editing when markdown would not round-trip cleanly (footnotes, raw HTML, oversized files, etc.).

Shared editing pipeline: load/fetch/stream reconcile, autosave, dirty state, and saveRef move into useEditableFileContent, used by both Monaco TextEditor and the rich editor. save-success only updates the saved baseline so keystrokes during an in-flight save are not reverted; useAutosave serializes unmount flushes after any active save.

Surrounding UX: file and mothership toolbars hide split/preview toggles for markdown; agent streaming passes streamingContent only while status is streaming so the editor reconciles to the server write after the agent finishes. Breadcrumb path popover closes before navigation and stops reopening from pointer-move on click.

Adds @tiptap/*, @floating-ui/dom, and broad unit tests for round-trip, editability gate, and editor behavior.

Reviewed by Cursor Bugbot for commit f8ac591. Configure here.

Comment thread apps/sim/hooks/use-autosave.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the raw-markdown/preview split for .md files with a full inline WYSIWYG editor (TipTap/ProseMirror): bubble menu, / slash menu, resizable images, GFM tables, task lists, and Prism code highlighting. Round-trip-unsafe files open read-only rather than risk silent data corruption; frontmatter is held byte-exact out-of-band. The shared autosave engine is hardened to prevent keystroke loss mid-save and out-of-order clobber on unmount.

  • New RichMarkdownEditor: single WYSIWYG surface for all markdown files; isRoundTripSafe gates editability once per open so unsafe constructs (footnotes, raw HTML, HTML comments, >128 KB) never corrupt a file.
  • Autosave hardening (use-autosave.ts + text-editor-state.ts): inFlightRef chains the unmount flush after any in-flight save; save-success no longer overwrites content with the saved snapshot, so a keystroke typed mid-save stays alive and triggers a trailing save.
  • useEditableFileContent: the entire load/stream/reconcile/save engine is extracted from TextEditor into a shared hook, used by both Monaco and the new rich editor without duplication.

Confidence Score: 5/5

This is a large, well-architected feature addition with no changes to existing behavior outside markdown files; the autosave hardening and keystroke-loss fix are strictly improvements.

The round-trip safety gate, frontmatter handling, and autosave chain-on-unmount are all carefully designed with tests covering the key invariants. The two findings are minor UX glitches (link editor reachable in read-only mode via Cmd+K; slash menu popup uses stale caret rect on scroll) that don't affect data integrity, persistence, or security.

No files require special attention; bubble-menu.tsx and slash-command.ts have the two small UX fixes noted above.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx Main WYSIWYG editor component: mounts TipTap, holds frontmatter aside, computes round-trip safety once at open, owns image upload/insert, handles Cmd+S and Cmd+click-to-open-link
apps/sim/hooks/use-autosave.ts Autosave hardened: unmountedRef suppresses post-unmount setState, inFlightRef ensures unmount flush chains after the in-flight save to prevent out-of-order clobber, displayTimerRef cleared on unmount
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts New shared content engine extracted from TextEditor: owns fetched/streamed reconciliation, debounced autosave, dirty/save-status/saveRef bridge — used by both Monaco and RichMarkdownEditor
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/round-trip-safety.ts Round-trip gate: pattern-checks stripped code for known-lossy constructs, then verifies serialize(serialize(x)) === serialize(x) idempotency; editor.destroy() in finally (fixes previously-flagged leak)
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx Floating formatting toolbar: Cmd+K listener on editor.view.dom doesn't guard on editor.isEditable, allowing the link editing UI to open in read-only mode; all commands silently no-op in that state so no data change occurs
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/slash-command.ts Slash command with Floating UI autoUpdate; the autoUpdate callback captures props.clientRect from onStart, so scroll/resize repositioning uses the initial caret rect rather than the live one
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx ResizableImage node: linked-image tokenizer, href sanitized through normalizeLinkHref before rendering anchor, pointer-capture resize with AbortController cleanup, HTML img serialization for sized images
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx Routes markdown files through RichMarkdownEditor (both editable and read-only shares), removes Monaco fallback for markdown per design decision; isMarkdownFile exported to hide mode controls in toolbar
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor-state.ts save-success no longer overwrites content with action.content, preventing a keystroke typed mid-save from being silently dropped; savedContent advances, content stays ahead and triggers a trailing autosave
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-fidelity.ts Frontmatter split/re-attach (byte-exact), normalizeLinkHref sanitizes dangerous schemes (javascript:/data:/vbscript:/file:), postProcessSerializedMarkdown restores escaped callout markers and trims trailing newlines

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    FV[FileViewer] -->|isMarkdownFile?| RME[RichMarkdownEditor]
    FV -->|other text| TE[TextEditor / Monaco]
    FV -->|CSV / PDF / etc| Other[Specialized viewers]

    RME --> UEFC[useEditableFileContent]
    TE --> UEFC

    UEFC --> FCS[useFileContentState\nreducer]
    UEFC --> AS[useAutosave]
    UEFC --> WFC[useWorkspaceFileContent\nfetch]

    RME -->|content loaded| LRME[LoadedRichMarkdownEditor]
    LRME -->|isRoundTripSafe| Gate{Round-trip\nsafe?}
    Gate -->|yes + canEdit| EditorEditable[TipTap editor\neditable=true]
    Gate -->|no or !canEdit| EditorReadOnly[TipTap editor\neditable=false]

    AS -->|debounce 1s| SaveAPI[PATCH file content]
    AS -->|unmount flush\nchained after inFlightRef| SaveAPI
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    FV[FileViewer] -->|isMarkdownFile?| RME[RichMarkdownEditor]
    FV -->|other text| TE[TextEditor / Monaco]
    FV -->|CSV / PDF / etc| Other[Specialized viewers]

    RME --> UEFC[useEditableFileContent]
    TE --> UEFC

    UEFC --> FCS[useFileContentState\nreducer]
    UEFC --> AS[useAutosave]
    UEFC --> WFC[useWorkspaceFileContent\nfetch]

    RME -->|content loaded| LRME[LoadedRichMarkdownEditor]
    LRME -->|isRoundTripSafe| Gate{Round-trip\nsafe?}
    Gate -->|yes + canEdit| EditorEditable[TipTap editor\neditable=true]
    Gate -->|no or !canEdit| EditorReadOnly[TipTap editor\neditable=false]

    AS -->|debounce 1s| SaveAPI[PATCH file content]
    AS -->|unmount flush\nchained after inFlightRef| SaveAPI
Loading

Reviews (9): Last reviewed commit: "fix(file-viewer): sanitize linked-image ..." | Re-trigger Greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 2ca63c2. Configure here.

Replace the raw/preview split for markdown files with a Linear-style inline WYSIWYG editor (TipTap/ProseMirror): bubble + slash menus, code-block language picker with Prism highlighting and line-wrap, resizable images (HTML <img>), GFM tables, and frontmatter held byte-exact out of band.

A round-trip preflight gate (decided once per open) falls back to the raw Monaco editor for any file that can't be edited losslessly, so the rich editor never silently corrupts a file.
The unmount flush no longer fires a concurrent PUT alongside an in-flight save; it awaits the in-flight save and then writes the latest content sequentially, so an out-of-order completion can't clobber newer edits with a stale snapshot (addresses Cursor Bugbot).
Some browsers expose a pasted or copied image only via DataTransfer.items (with an empty files list), so screenshot paste was silently ignored. extractImageFiles now falls back to items; moved to a testable module with unit tests (addresses Cursor Bugbot).
Wrap the probe serialize() in try/finally so the throwaway Editor is always destroyed even if setContent/getMarkdown throws (addresses Greptile). Adds a test proving PipeSafeTable escapes only interior cell pipes, not structural delimiters.
…ever strand dirty edits

The round-trip-safety verdict now gates editability only at open time — computed once, on the exact
content the editor mounts with, and locked for its lifetime. A dirty document is round-trip-safe by
construction (the editor only emits safe markdown), so the verdict must never flip off mid-edit:
doing so disabled autosave, ⌘S, the toolbar Save and the unmount flush, stranding unsaved edits.
Locking on the opened (reconciled) content also fixes the stale post-stream empty-buffer snapshot,
and lets the redundant MarkdownFileEditor gate (plus its duplicate content fetch) be deleted.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

- code-block: replace hand-rolled copy-with-timeout with shared useCopyToClipboard
- rich-markdown-editor: compute frontmatter split once via lazy ref, drop redundant frontmatterRef
- round-trip-safety: correct stale comments (read-only, not raw editor fallback)
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 89a269e. Configure here.

…der, churn fixes

- image: round-trip linked images/badges via an href attr + custom markdown tokenizer; make
  the image a drag handle so it can be grabbed and reordered
- link-input-rule: convert typed [text](url) to a link on the closing paren (normalized href)
- markdown-paste: render pasted markdown as rich content, guarded against code blocks
- round-trip-safety: behavioral link-count check replaces the static linked-image rejection
- extensions: trim the table serializer's blank lines to stop interior-table whitespace churn
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

…to a paragraph

Notion-style: ProseMirror's default joins or no-ops at a heading boundary, stranding the
heading style. A second Backspace then merges as usual.
… editor

handlePaste/handleDrop ran the workspace image upload without checking editability, so a
read-only doc (canEdit=false or a round-trip-unsafe file) could still trigger an upload.
Guard both on view.editable.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

…line strip

- image: run the linked-image (badge) anchor target through normalizeLinkHref so a
  javascript:/data: href in a file can't execute on click; the markdown still preserves the
  raw target (file content unchanged)
- markdown-fidelity: the table serializer now trims its own surrounding blank lines, so the
  global leading-newline strip in postProcessSerializedMarkdown is redundant — removing it
  stops clobbering content that legitimately begins with whitespace
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f8ac591. Configure here.

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