Skip to content

feat(files): inline rich markdown editor#5133

Merged
waleedlatif1 merged 48 commits into
stagingfrom
feature/inline-rich-markdown-editor
Jun 20, 2026
Merged

feat(files): inline rich markdown editor#5133
waleedlatif1 merged 48 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 20, 2026 1:25am

Request Review

@cursor

cursor Bot commented Jun 19, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Large new editing surface with autosave, serialization, and round-trip gates—bugs could corrupt markdown or block edits; sidebar cookie migration is lower risk but affects first paint layout.

Overview
Markdown files now open in a new TipTap-based inline WYSIWYG (RichMarkdownEditor) instead of Monaco plus a separate preview split. The viewer routes markdown through this editor for both edit and read-only share; isMarkdownFile hides raw/split/preview toolbar modes. The editor adds bubble and slash menus, Prism-highlighted code blocks with language picker and Mermaid diagram/source toggling, resizable images and linked badges, GFM tables and task lists, frontmatter held out of band, chunked parsing for large files, streaming agent output in the same surface, and a round-trip safety gate that keeps lossy docs (footnotes, raw HTML, oversized files, etc.) read-only.

PreviewPanel drops the old Streamdown markdown stack; markdown preview there is removed in favor of the rich editor path, with Mermaid factored into a shared module and tests for looksLikeMermaid.

Sidebar collapse is driven by a sidebar_collapsed cookie (with one-time migration from legacy localStorage), wired from the workspace layout into WorkspaceChrome / Sidebar, and the inline boot script no longer sets data-sidebar-collapsed on html. Related sidebar CSS is simplified to data-collapsed on the container only.

Breadcrumb path popover closes before navigation, opens only on enter/focus (not pointer move), and uses a short hover close delay so clicks don’t flash the popover mid-route change.

Reviewed by Cursor Bugbot for commit cc2d84c. 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 / Monaco split for .md files with a TipTap/ProseMirror inline WYSIWYG editor — a single editing surface with a bubble menu, / slash-command menu, Prism-highlighted code blocks, resizable images, GFM tables, and task lists. A round-trip safety gate (idempotency probe + static loss-pattern matching) opens files that can't be edited losslessly as read-only rich previews instead of risking silent data corruption. The useAutosave hook was hardened to prevent data loss when a keystroke lands during an in-flight save, and the sidebar collapse state was migrated from localStorage to a cookie so the server can render the correct collapsed structure on the first paint.

  • New rich editor (rich-markdown-editor/): TipTap editor with RAF-coalesced streaming sync, per-stream settle/verdict locking, PipeSafeTable pipe-escaping, linked-image ([![…](…)](…)) round-trip support, and a shared useEditableFileContent engine extracted from text-editor.tsx.
  • use-autosave.ts hardening: inFlightRef chains the unmount-flush after any in-flight PUT to prevent out-of-order overwrites; unmountedRef guards post-unmount state updates; save-success in the reducer no longer rolls content back to the saved snapshot, closing a silent data-loss window for edits that land during a save.
  • Sidebar SSR fix: collapse state moved to a sidebar_collapsed cookie so WorkspaceLayout can seed the initial render correctly, eliminating the narrow-rail flash on first paint.

Confidence Score: 5/5

Safe to merge — the core editing, streaming, and autosave paths are well-constructed and the previous thread issues have all been resolved.

The autosave hardening (inFlightRef chaining, unmountedRef guard, save-success content preservation) closes real data-loss windows and is backed by comprehensive unit tests. The round-trip safety gate is conservative by design. The two findings are narrow edge cases: one affects only explicitly-escaped callout markers (a very rare pattern), and one is a project-style note about a single CSS rule. Neither affects correctness for the vast majority of real-world markdown files.

markdown-fidelity.ts — the callout-escape restoration in postProcessSerializedMarkdown has a subtle interaction with the round-trip safety gate that allows a backslash-escape to be silently stripped on first edit for intentionally-escaped callout markers.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx New TipTap/ProseMirror WYSIWYG editor; RAF-coalesced streaming sync, per-stream settle/verdict locking, image paste/drop upload, anchor navigation, Cmd+S shortcut. Streaming→editable handoff and ref-based change isolation look solid.
apps/sim/hooks/use-autosave.ts Hardened autosave: unmountedRef guards post-unmount state updates, inFlightRef chains the unmount-flush after any in-flight PUT (preventing out-of-order overwrites), displayTimerRef properly cleared on unmount. All edge cases well-handled.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts New shared engine for content loading, reconciliation, and autosave; extracted from text-editor.tsx and adapted for the rich editor. Clean separation of concerns.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/round-trip-safety.ts Two-phase safety gate: static STABLE_LOSS_PATTERNS + idempotency probe. Code-stripping before pattern matching prevents false positives. Minor gap: explicitly-escaped callout markers pass the gate but lose their backslash on first edit.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/extensions.ts Extension factory separating content-only (headless) and full-UI (node-views) sets. PipeSafeTable pipes-in-cells fix is correct; InlineCode excludes-fix allows bold/italic to coexist with inline code.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-fidelity.ts Frontmatter split/apply, link normalization, and serializer post-processing. normalizeLinkHref correctly blocks dangerous schemes. Minor issue: callout-escape restoration can silently strip intentional [!WORD] escapes on first edit.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx MarkdownImage node with linked-image tokenizer (badge support), sized images serialize to HTML img tags, ResizableImageView with AbortController-based drag-to-resize. safeHref sanitization on render path is correct.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor-state.ts Removed StreamingMode/resolveStreamingEditorContent; fixed save-success to not roll content back to the saved snapshot, preventing silent data loss when an edit lands during a save.
apps/sim/stores/sidebar/store.ts Sidebar collapse migrated from localStorage to cookie (server-readable); skipHydration + manual rehydrate in useLayoutEffect; merge override prevents legacy persisted isCollapsed from overriding cookie-seeded value.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-parse.ts Chunked O(n) markdown parser using a singleton headless Editor; splitMarkdownBlocks correctly fences code blocks and merges continuation groups. NON_CHUNKABLE guard ensures reference-style links and block HTML parse whole.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as FileViewer
    participant RME as RichMarkdownEditor
    participant Engine as useEditableFileContent
    participant State as textEditorContentReducer
    participant AS as useAutosave
    participant API as Server API

    UI->>RME: mount (file, canEdit)
    RME->>Engine: useEditableFileContent(file)
    Engine->>API: useWorkspaceFileContent()
    API-->>Engine: fetchedContent
    Engine->>State: sync-external (fetchedContent)
    State-->>Engine: "phase=ready, content"

    RME->>RME: isRoundTripSafe(content) → verdict
    RME->>RME: lockSettled (frontmatter + verdict)
    RME->>RME: "useEditor (initialContent = parseMarkdownToDoc)"

    Note over RME: Agent streaming path
    Engine->>State: sync-external (streamingContent)
    State-->>Engine: "phase=streaming, isStreamInteractionLocked=true"
    RME->>RME: RAF-coalesced setContent (read-only)
    Note over RME: stream settles
    Engine->>State: "sync-external (streamingContent=undefined)"
    State-->>Engine: "phase=reconciling → ready"
    RME->>RME: lockSettled on settled content, editor.setEditable(true)

    Note over RME: User edit path
    RME->>RME: onUpdate → getMarkdown() + postProcess
    RME->>Engine: setDraftContent(markdown)
    Engine->>State: edit action
    State-->>Engine: "content updated, isDirty=true"
    Engine->>AS: useAutosave (content, savedContent)
    AS->>AS: debounce 1500ms
    AS->>API: onSave() → PUT fileContent
    API-->>AS: success
    AS->>Engine: markSavedContent(content)
    Engine->>State: save-success (savedContent only, content preserved)

    Note over RME: Unmount
    AS->>AS: await inFlightRef, flush if still dirty
    AS->>API: onSave() final flush
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"}}}%%
sequenceDiagram
    participant UI as FileViewer
    participant RME as RichMarkdownEditor
    participant Engine as useEditableFileContent
    participant State as textEditorContentReducer
    participant AS as useAutosave
    participant API as Server API

    UI->>RME: mount (file, canEdit)
    RME->>Engine: useEditableFileContent(file)
    Engine->>API: useWorkspaceFileContent()
    API-->>Engine: fetchedContent
    Engine->>State: sync-external (fetchedContent)
    State-->>Engine: "phase=ready, content"

    RME->>RME: isRoundTripSafe(content) → verdict
    RME->>RME: lockSettled (frontmatter + verdict)
    RME->>RME: "useEditor (initialContent = parseMarkdownToDoc)"

    Note over RME: Agent streaming path
    Engine->>State: sync-external (streamingContent)
    State-->>Engine: "phase=streaming, isStreamInteractionLocked=true"
    RME->>RME: RAF-coalesced setContent (read-only)
    Note over RME: stream settles
    Engine->>State: "sync-external (streamingContent=undefined)"
    State-->>Engine: "phase=reconciling → ready"
    RME->>RME: lockSettled on settled content, editor.setEditable(true)

    Note over RME: User edit path
    RME->>RME: onUpdate → getMarkdown() + postProcess
    RME->>Engine: setDraftContent(markdown)
    Engine->>State: edit action
    State-->>Engine: "content updated, isDirty=true"
    Engine->>AS: useAutosave (content, savedContent)
    AS->>AS: debounce 1500ms
    AS->>API: onSave() → PUT fileContent
    API-->>AS: success
    AS->>Engine: markSavedContent(content)
    Engine->>State: save-success (savedContent only, content preserved)

    Note over RME: Unmount
    AS->>AS: await inFlightRef, flush if still dirty
    AS->>API: onSave() final flush
Loading

Reviews (23): Last reviewed commit: "fix(sidebar): reconcile migrated-legacy ..." | 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

cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptileai review

…, image escaping)

Final-audit follow-ups:
- splitMarkdownBlocks normalizes CRLF/CR first — a closing fence ending in \r
  no longer fails to match, which had collapsed Windows-authored files with
  fenced code into one block and defeated the linear chunker (perf regression).
- normalizeLinkHref rejects file://, blob:, and other non-network schemes
  (script/data schemes already rejected); network scheme:// (http/ftp/…) and
  bare host:port still pass.
- Image markdown serialization escapes alt/title delimiters and angle-brackets
  a src with spaces/parens, so they round-trip losslessly; linked-image anchors
  open in a new tab (target=_blank).
- Markdown paste routes through the chunker so a large pasted blob can't freeze
  the main thread.
Export and unit-test changeTouchesCodeBlock: prose-only edits map decorations
(false), edits inside a code block or a setNodeMarkup language change re-tokenize
(true) — the perf-correctness path that keeps highlighting off the keystroke path.
…n the SPA

- normalizeLinkHref no longer prefixes `./`/`../` relative paths into `https://./…`
  (they round-trip and resolve correctly).
- Following a same-origin in-app link (e.g. /workspace/…) routes through the
  Next router (same tab) instead of always opening a new tab; modifier-click and
  external URLs still open a new tab.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptileai review

…itor

The linked-image anchor's native navigation was firing on a plain click in edit
mode (where handleClick intentionally returns false for caret placement). Prevent
the anchor's default so the editor's handleClick — gated on editable/modifier,
matching text links via openOnClick:false — is the sole navigator.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptileai review

Comment thread apps/sim/app/layout.tsx Outdated
A substring search for 'sidebar_collapsed=1' also matched 'sidebar_collapsed=10',
desyncing the pre-paint sidebar rail and client store from the strict server read.
Parse the cookie value and compare it to '1' exactly, in both the pre-paint inline
script and readCollapsedCookie. Added a store test.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptileai review

Comment thread apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx Outdated
A user whose collapse lived only in localStorage has no sidebar_collapsed cookie
at SSR (initialCollapsed=false), but the pre-paint script migrates them to a
cookie. The store's persist.rehydrate() is async (flips _hasHydrated after paint),
so the first paint showed expanded labels in the collapsed 51px rail. Reconcile to
the cookie synchronously in a useLayoutEffect (first render still matches the
server, so no hydration mismatch) — no narrow-rail flash.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptileai 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 cc2d84c. Configure here.

@waleedlatif1 waleedlatif1 merged commit ecbe191 into staging Jun 20, 2026
16 checks passed
@waleedlatif1 waleedlatif1 deleted the feature/inline-rich-markdown-editor branch June 20, 2026 01:32
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