Skip to content

feat(files): inline rich markdown editor#5133

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

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

Conversation

@waleedlatif1

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

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.
@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 2:02am

Request Review

@cursor

cursor Bot commented Jun 19, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches workspace file persistence and adds a large client editor surface, but lossy markdown is gated to Monaco and autosave race fixes reduce silent edit loss risk.

Overview
Adds a TipTap/ProseMirror inline WYSIWYG path for workspace markdown files, replacing the Monaco raw/split/preview flow when the file is not agent-streaming. FileViewer dynamically loads MarkdownFileEditor, which picks RichMarkdownEditor vs Monaco using a one-time isRoundTripSafe preflight so lossy constructs (footnotes, linked images, table <br>, etc.) never save through the rich pipeline.

The new editor stack includes bubble formatting, / slash blocks, Prism code blocks (language picker, wrap, copy), resizable images with workspace upload on paste/drop, frontmatter held out of band, and markdown serializers tuned for pipes-in-tables and fence sizing. Files and mothership toolbars use isMarkdownFile to drop edit/split/preview toggles while keeping Save.

useEditableFileContent centralizes fetch, stream reconciliation, and autosave for Monaco and rich surfaces. save-success no longer overwrites in-flight edits; useAutosave serializes unmount flushes after any in-flight save. Breadcrumb path navigation closes the location popover with a short reopen latch to avoid a flash during route changes.

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

Comment thread apps/sim/hooks/use-autosave.ts Outdated
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).
@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 introduces a Linear-style inline WYSIWYG markdown editor powered by TipTap/ProseMirror, replacing the previous raw-markdown/preview split for .md files. It includes a round-trip preflight gate that falls back to Monaco for files that can't be edited losslessly, along with a hardened shared autosave engine that closes a race condition where keystrokes landing mid-save could be silently dropped.

  • Rich editor stack: TipTap with GFM tables, task lists, code-block language picker (Prism), resizable images, bubble menu, and / slash command menu. Frontmatter is held out-of-band so TipTap never corrupts it.
  • Safety gate (isRoundTripSafe): Runs two headless serialization passes on first load; any file with linked images, footnotes, HTML comments, raw HTML tags, or other constructs that can't survive the round-trip opens in the raw Monaco editor instead.
  • Autosave race fix (text-editor-state.ts + use-autosave.ts): save-success no longer rolls content back to the saved snapshot, and the unmount flush now chains after any in-flight save rather than racing it, so no edit typed mid-save is ever lost.

Confidence Score: 5/5

Safe to merge — no data-loss, correctness, or security issues found in the changed files.

The autosave race fix is correctly implemented and unit-tested: save-success no longer resets content to the saved snapshot, the unmount flush chains after any in-flight save, and stale status updates after unmount are guarded. The round-trip safety gate is conservative (rejects on any doubt), uses try/finally to clean up headless editor instances, and is backed by 229 round-trip tests. The TipTap integration follows standard patterns, extension module-level sharing is safe, and the shared query cache between MarkdownFileEditor and useEditableFileContent is confirmed to use the same key. No behavioral regressions were found in the refactored TextEditor.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/hooks/use-autosave.ts Hardened with unmountedRef to skip state updates after unmount, inFlightRef to sequence the unmount flush after any in-flight save, and displayTimerRef for proper cleanup. The race fix (no longer rolling content back on save-success) is correct and well-tested.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/round-trip-safety.ts Conservative two-pass safety gate with try/finally editor cleanup, stable-loss pattern detection, and code-stripping to avoid false positives from patterns inside code blocks.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-file-editor.tsx Gate decision is made once via decisionRef and locked for the mount lifetime. Cache key sharing with useWorkspaceFileContent confirmed — both callers produce mode = 'text' so they share the same React Query cache entry.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx Editor mounts cleanly with frontmatter held out-of-band, external content sync uses emitUpdate: false to avoid re-triggering onUpdate, and image insert is safely guarded with position clamping and try/catch.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts Clean extraction of the shared content-loading/autosave engine. The onSave callback correctly captures contentRef.current at call time rather than closing over stale content.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor-state.ts save-success no longer resets content to the saved snapshot, preventing the mid-save keystroke loss bug. The no-op check is updated consistently and a new test verifies the behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[FileViewer: markdown file\nno streaming] --> B{MarkdownFileEditor}
    B --> C[useWorkspaceFileContent\nReact Query cache]
    C -->|data arrives| D{isRoundTripSafe?}
    D -->|file > 128 KB\nor unsafe constructs| E[TextEditor\nMonaco fallback]
    D -->|safe| F[RichMarkdownEditor\nTipTap / ProseMirror]
    C -->|still loading| G[PreviewLoadingFrame]
    C -->|fetch error| E

    F --> H[useEditableFileContent]
    E --> H
    H --> I[useFileContentState\nuseReducer]
    H --> J[useAutosave\ndebounced 2 s]

    I -->|edit action| K[content ← draft]
    J -->|save-success| L[savedContent ← saved snapshot\ncontent unchanged]
    L -->|content ≠ savedContent| J

    J -->|unmount flush| M[await inFlightRef\nthen final PUT]

    F -->|onUpdate| N[serialize markdown\napply frontmatter\nsetDraftContent]
    N --> I
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
    A[FileViewer: markdown file\nno streaming] --> B{MarkdownFileEditor}
    B --> C[useWorkspaceFileContent\nReact Query cache]
    C -->|data arrives| D{isRoundTripSafe?}
    D -->|file > 128 KB\nor unsafe constructs| E[TextEditor\nMonaco fallback]
    D -->|safe| F[RichMarkdownEditor\nTipTap / ProseMirror]
    C -->|still loading| G[PreviewLoadingFrame]
    C -->|fetch error| E

    F --> H[useEditableFileContent]
    E --> H
    H --> I[useFileContentState\nuseReducer]
    H --> J[useAutosave\ndebounced 2 s]

    I -->|edit action| K[content ← draft]
    J -->|save-success| L[savedContent ← saved snapshot\ncontent unchanged]
    L -->|content ≠ savedContent| J

    J -->|unmount flush| M[await inFlightRef\nthen final PUT]

    F -->|onUpdate| N[serialize markdown\napply frontmatter\nsetDraftContent]
    N --> I
Loading

Reviews (3): Last reviewed commit: "chore(files): drop platform references a..." | Re-trigger Greptile

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).
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

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.
scheduleClose fired on the pointer/focus exit that immediately follows a click-to-navigate and was clearing the reopen latch before the route swapped, letting the popover flash back open. The latch is now released by a short timer instead (addresses Cursor Bugbot).
@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.

The mothership preview was routing streaming markdown through the inline editor path: it showed Monaco during streaming (previewMode fell back to 'editor') and lost the streamed content on the TextEditor→MarkdownFileEditor swap (the TextEditor unmounted before it could reconcile + autosave). The inline rich editor is now opt-in via a FileViewer prop that only the files view sets, so the mothership keeps its raw/preview streaming editor and persists as before.
…view

Idle markdown in the chat resource view now renders the single-surface inline editor (no raw/split/preview pencil toggle), matching the files view. While the agent streams, FileViewer forces the rendered preview instead of Monaco, and the streamed file persists via the agent's server write + the existing content-query invalidation on tool completion — so the idle editor refetches the persisted content.
…es streaming

The preview session keeps status='complete' and previewText after streaming ends, so streamingContent stayed defined and the file stuck on the read-only rendered preview. Treat content as streaming only while status==='streaming'; once complete the EmbeddedFile sees no streamingContent and mounts the editable inline editor (which refetches the persisted content). The synthetic streaming-file stays a pure preview.
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