feat(files): inline rich markdown editor#5133
Conversation
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 latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview The new editor stack includes bubble formatting,
Reviewed by Cursor Bugbot for commit 2ca63c2. Configure here. |
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).
|
@greptile review |
|
@cursor review |
Greptile SummaryThis PR introduces a Linear-style inline WYSIWYG markdown editor powered by TipTap/ProseMirror, replacing the previous raw-markdown/preview split for
Confidence Score: 5/5Safe to merge — no data-loss, correctness, or security issues found in the changed files. The autosave race fix is correctly implemented and unit-tested: No files require special attention. Important Files Changed
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
%%{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
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).
|
@cursor review |
|
@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).
|
@cursor review |
|
@greptile review |
There was a problem hiding this comment.
✅ 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.
… the markdown gate
…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.
Summary
/slash menu, code-block language picker with Prism syntax highlighting + line-wrap, resizable images (sized images serialize to HTML<img>), GFM tables, task lists<img>/entity/heading-hardbreak/table-<br>data-loss paths are all closed and gatedType of Change
Testing
Checklist