Skip to content

feat(files): inline rich markdown editor#5133

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

feat(files): inline rich markdown editor#5133
waleedlatif1 wants to merge 20 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)

@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 7:22am

Request Review

@cursor

cursor Bot commented Jun 19, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches persisted file content, autosave ordering, and agent streaming reconciliation; mitigated by round-trip gating and broad tests, but regressions could still clobber or lose edits on edge-case markdown or concurrent saves.

Overview
Markdown in the file viewer now opens in a new TipTap/ProseMirror inline editor instead of Monaco’s raw/split/preview flow. The viewer dynamically loads RichMarkdownEditor, exports isMarkdownFile so files and mothership toolbars hide edit/split/preview toggles, and uses the same surface read-only on public shares.

The new editor adds bubble formatting, / slash blocks, Prism-highlighted code blocks (language picker, wrap, copy), resizable/linked images with workspace upload on paste/drop, GFM tables/tasks, frontmatter held out of band, and an isRoundTripSafe open-time gate that keeps lossy docs read-only inside TipTap. Serialization fixes cover pipe-safe tables, linked badges, fence sizing, link normalization, and markdown paste.

useEditableFileContent centralizes fetch, streaming/reconcile state, autosave, and dirty/save refs for both Monaco and the rich editor. save-success no longer overwrites in-flight keystrokes; useAutosave serializes unmount flushes after in-flight saves. Mothership textStreamingContent is fed only while preview status is streaming, so the editor can reconcile to the agent’s server write after the stream ends.

Smaller UX: breadcrumb path popover closes before navigate (no pointer-move re-open), Share icon → Send, files loading background tweak. Adds @tiptap/*, @floating-ui/dom, and extensive unit tests for round-trip and editor behavior.

Reviewed by Cursor Bugbot for commit b7d87c8. 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 Linear-style inline WYSIWYG editor (TipTap/ProseMirror), adding a bubble-menu toolbar, / slash menu, code-block language picker with Prism highlighting, resizable images, GFM tables, and task lists. It also extracts a shared useEditableFileContent hook so the same fetch/reconcile/autosave engine is used by both the rich and Monaco surfaces, hardens the autosave unmount flush to serialize in-flight saves before firing the final write, and fixes a stale-snapshot bug in the save-success reducer case.

  • Rich editor routing: all markdown files are dispatched through RichMarkdownEditor; a per-open round-trip safety probe gates editability — files with footnotes, HTML entities, HTML comments, or raw HTML open read-only rather than falling back to Monaco.
  • Autosave hardening: the unmount flush now chains after any in-flight save (inFlightRef) to prevent out-of-order server writes, and a new displayTimerRef is cleared on unmount to avoid post-unmount setState calls.
  • Content reconciliation fix: save-success no longer rolls content back to the saved snapshot, so keystrokes that land while a save is in-flight are never silently discarded.

Confidence Score: 4/5

Safe to merge with the round-trip fallback gap addressed; no data-loss or corruption path exists, but users with complex markdown lose edit access.

The autosave hardening, content-reconciliation fix, and round-trip safety gate are all well-designed and correctly implemented. The one concrete regression is in file-viewer.tsx: the PR routes all markdown through RichMarkdownEditor, but files that fail the round-trip probe (footnotes, HTML entities, HTML comments, raw HTML blocks) open read-only with no fallback to Monaco. Users who previously edited those files in Monaco now have a read-only surface and no alternative editing path from the UI.

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx — the dispatch logic that routes all markdown to RichMarkdownEditor needs to additionally route round-trip-unsafe files to TextEditor (Monaco) when canEdit is true.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx Routes all markdown to RichMarkdownEditor; round-trip-unsafe files are silently opened read-only instead of falling back to Monaco, regressing editability of complex markdown.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx New inline WYSIWYG editor using TipTap/ProseMirror. Architecture is clean — streaming-locked state shows PreviewPanel, settled content mounts LoadedRichMarkdownEditor keyed by file id. Round-trip check gates editability but doesn't route to Monaco on failure.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts Clean extraction of text-editor lifecycle (fetch, reconcile, autosave, callbacks) into a shared hook. Snapshot-at-save-start pattern correctly avoids stale-closure saves; streaming status propagation looks correct.
apps/sim/hooks/use-autosave.ts Autosave hardened: unmount flush chains after in-flight save (no concurrent PUTs), displayTimerRef cleared on unmount, unmountedRef reset on remount to survive React strict-mode double-invoke.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/round-trip-safety.ts Idempotency gate correctly covers stable-loss patterns, linked-image count drift, and two-pass stabilization. Editor instances are always destroyed via try/finally. 128 KB probe limit guards main-thread blocking.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/extensions.ts PipeSafeTable escapes only cell-interior pipes; InlineCode allows coexistence with bold/italic. Content vs. editor extension split is clean.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-fidelity.ts Frontmatter split/apply is byte-exact; normalizeLinkHref blocks dangerous schemes; callout-marker post-processing is idempotent.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx Bubble menu with link editor; captured range is clamped before re-selection to avoid out-of-range throw; selectionUpdate listener cleans up via editor.off.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor-state.ts save-success no longer rolls content back to the saved snapshot; savedContent-only advancement prevents silently dropping keystrokes that land mid-save.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    FV[FileViewer] -->|isMarkdownFile?| MD{Markdown?}
    MD -->|No| TE[TextEditor / Monaco]
    MD -->|Yes| RME[RichMarkdownEditor]
    RME --> UEFC[useEditableFileContent]
    UEFC --> UA[useAutosave]
    UEFC --> Fetch[useWorkspaceFileContent]
    RME --> Loading{isContentLoading?}
    Loading -->|Yes| LoadFrame[PreviewLoadingFrame]
    Loading -->|No| Streaming{isStreamInteractionLocked?}
    Streaming -->|Yes| PP[PreviewPanel read-only]
    Streaming -->|No| LRME[LoadedRichMarkdownEditor]
    LRME --> RTS{isRoundTripSafe?}
    RTS -->|Yes + canEdit| Editable[TipTap editor — editable]
    RTS -->|No or !canEdit| ReadOnly[TipTap editor — read-only no Monaco fallback]
    Editable --> BM[EditorBubbleMenu]
    Editable --> SC[SlashCommand]
    Editable --> CB[CodeBlockWithLanguage]
    Editable --> RI[ResizableImage]
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?| MD{Markdown?}
    MD -->|No| TE[TextEditor / Monaco]
    MD -->|Yes| RME[RichMarkdownEditor]
    RME --> UEFC[useEditableFileContent]
    UEFC --> UA[useAutosave]
    UEFC --> Fetch[useWorkspaceFileContent]
    RME --> Loading{isContentLoading?}
    Loading -->|Yes| LoadFrame[PreviewLoadingFrame]
    Loading -->|No| Streaming{isStreamInteractionLocked?}
    Streaming -->|Yes| PP[PreviewPanel read-only]
    Streaming -->|No| LRME[LoadedRichMarkdownEditor]
    LRME --> RTS{isRoundTripSafe?}
    RTS -->|Yes + canEdit| Editable[TipTap editor — editable]
    RTS -->|No or !canEdit| ReadOnly[TipTap editor — read-only no Monaco fallback]
    Editable --> BM[EditorBubbleMenu]
    Editable --> SC[SlashCommand]
    Editable --> CB[CodeBlockWithLanguage]
    Editable --> RI[ResizableImage]
Loading

Reviews (7): Last reviewed commit: "feat(file-viewer): linked images, typed-..." | 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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

… a locked stale snapshot

The gate locked isRoundTripSafe on the first post-stream snapshot, which is often the empty create_file buffer before the agent's server write lands — wrongly leaving an unsafe document editable. Derive the verdict from the current content (memoized on the bytes) so canEdit tracks the real payload.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

…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

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b7d87c8. Configure here.

event.preventDefault()
const dropPos = view.posAtCoords({ left: event.clientX, top: event.clientY })?.pos
void insertImagesRef.current(images, dropPos ?? view.state.selection.from)
return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Paste uploads when editor read-only

Medium Severity

The custom handlePaste and handleDrop handlers intercept image clipboard/drop events and call insertImagesRef (workspace upload) without checking whether the editor is editable. When canEdit is false or the round-trip gate sets editable: false, ProseMirror may still deliver paste/drop to these handlers, so uploads can run even though the document is read-only and the insert may not belong in the file.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b7d87c8. Configure here.

Comment on lines 141 to +153
return <CsvTablePreview key={file.id} file={file} workspaceId={workspaceId} />
}

if (isMarkdownFile(file)) {
return (
<RichMarkdownEditor
key={file.id}
file={file}
workspaceId={workspaceId}
canEdit={canEdit}
autoFocus={autoFocus}
onDirtyChange={onDirtyChange}
onSaveStatusChange={onSaveStatusChange}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Round-trip-unsafe files are read-only in the rich editor, not falling back to Monaco

The PR description states "falls back to the raw Monaco editor for any file that can't be edited losslessly," but the code routes all markdown files through RichMarkdownEditor. When isRoundTripSafe returns false (files with footnotes, HTML entities, HTML comments, raw HTML blocks, etc.), LoadedRichMarkdownEditor sets isEditable = false — opening a read-only rich-editor view instead of the previously working Monaco editor.

This regresses editability for any user whose markdown contains constructs matched by STABLE_LOSS_PATTERNS. Before this PR they had a fully editable Monaco surface; now they get a read-only rich editor with no edit path. A check of the round-trip verdict (or the isMarkdownFile decision) should dispatch to TextEditor (Monaco) for the unsafe case.

…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.
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