feat(files): inline rich markdown editor#5133
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Shared editing pipeline: load/fetch/stream reconcile, autosave, dirty state, and Surrounding UX: file and mothership toolbars hide split/preview toggles for markdown; agent streaming passes 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. |
|
@greptile review |
|
@cursor review |
Greptile SummaryThis PR replaces the raw-markdown/preview split for
Confidence Score: 5/5This 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; Important Files Changed
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
%%{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
Reviews (9): Last reviewed commit: "fix(file-viewer): sanitize linked-image ..." | Re-trigger Greptile |
|
@cursor review |
|
@greptile review |
|
@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.
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.
|
@cursor review |
|
@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)
|
@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 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
|
@cursor review |
|
@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.
|
@cursor review |
|
@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
|
@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 f8ac591. Configure here.
… add more code languages - rich-markdown-editor: the TipTap editor is now the only markdown surface. Agent output streams into it read-only (synced per chunk, autoscrolled), then the same instance hands off to an editable editor on settle — no separate streamdown preview, so no stream→edit flash. The round-trip verdict + frontmatter lock when the content settles. - code-block/code-highlight/detect-language: register Go, Rust, Java, C, C++, C#, Ruby, PHP grammars and add detectors, so those blocks highlight and the picker offers them. - css: style h5/h6 in the prose stylesheet.
The collapsed sidebar swaps entire subtrees (collapsed flyout vs expanded lists), but isCollapsed only resolved after the first paint via auto rehydration, so a collapsed reload rendered the expanded tree into the 51px rail and then reflowed — the misplaced/flashing content on refresh. Adopt zustand's documented SSR pattern: skipHydration on the persist config (first render keeps the default false, matching SSR HTML) and flush persist.rehydrate() from a useLayoutEffect so the correct structure commits in the same pre-paint frame. Removes the old race where onRehydrateStorage lifted the data-sidebar-collapsed mask before React committed the rail.
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