Skip to content

feat(rich-editor): rich markdown field + @ mentions for skill & deploy modals#5215

Open
waleedlatif1 wants to merge 5 commits into
stagingfrom
rich-md-skills-deploy
Open

feat(rich-editor): rich markdown field + @ mentions for skill & deploy modals#5215
waleedlatif1 wants to merge 5 commits into
stagingfrom
rich-md-skills-deploy

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Add a controlled, file-less RichMarkdownField (sibling of the file-viewer editor) and use it for the skill Content field and the deploy version description — same WYSIWYG markdown experience, with placeholder/typography matched to the chip fields
  • Add an @-mention menu (TipTap suggestion) that inserts portable [label](sim:kind/id) markdown links for files, folders, tables, knowledge bases, workflows, skills, and integrations; wired into both the field and the file viewer via a shared useEditorMentions hook, with the popup lifecycle + menu chrome shared with the / slash command
  • Fix a false "Unsaved changes" on open: a deferred mount-time transaction re-serialized non-canonical markdown (Monaco-era files) and tripped the dirty check — the dirty baseline is now normalized to the editor's canonical form (round-trip-unsafe files untouched)
  • Always show the deployment version number (v3 · name) so a named version keeps a short, never-truncated reference
  • Skill import: drop the standalone paste box (the Create-tab Content editor now auto-destructures a pasted full SKILL.md), and reorder to GitHub → Upload

Type of Change

  • New feature

Testing

  • Tested manually; bun run type-check, biome, and check:api-validation clean; 351 file-viewer/skills/deploy unit tests pass (incl. new round-trip, dirty-on-open, and sim-link regression tests)

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)

@vercel

vercel Bot commented Jun 25, 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 25, 2026 11:51pm

Request Review

@cursor

cursor Bot commented Jun 25, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches shared editor extensions, dirty/save baseline logic, and deployment metadata validation; regressions could affect autosave, mention round-trip, or version description saves, though coverage includes targeted regression tests.

Overview
Introduces a controlled RichMarkdownField (same TipTap stack as the file viewer, without autosave/uploads) for skill Content and deploy version descriptions, including streaming during AI-generated descriptions and chip-field typography.

Adds a full @ mention flow: TipTap suggestion with a distinct plugin key, sim:<kind>/<id> mention nodes/chips aligned with chat portable links, lazy workspace entity fetching via useEditorMentions, and shared suggestion popup/chrome with the / slash menu (including dialog-safe popup mounting).

Fixes false “unsaved changes” on open by normalizeMarkdownContent on the dirty baseline in useEditableFileContent (skipped during agent streams); registers the sim link protocol so mentions round-trip.

Skill modal/import: textarea → rich editor; paste/import can destructure full SKILL.md; import UI reordered (GitHub then upload, paste box removed). Deploy: version list always shows v{n} plus optional name via formatVersionLabel; description API cap raised to 50k chars.

Reviewed by Cursor Bugbot for commit c46ef1b. Configure here.

@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 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a RichMarkdownField (controlled, file-less WYSIWYG markdown) used in the skill Content and deploy version-description modals, wires an @-mention menu (TipTap Suggestion + a shared createSuggestionPopupRenderer) into both the modal field and the file viewer, and fixes the false "unsaved changes" flag that fired when a non-canonical file was first opened. The version list is also updated to always show the numeric v{n} token alongside any custom name.

  • The mention subsystem is cleanly layered: a MentionStore bridges React Query data into the detached ReactRenderer root, useEditorMentions wires it per-editor, and the sim:<kind>/<id> link scheme round-trips natively through the existing markdown parser without a separate serialization path.
  • The normalizeBaseline hook option in useEditableFileContent fixes the dirty-on-open regression with a targeted, opt-in mechanism that is correctly disabled during agent streaming.
  • The shared createSuggestionPopupRenderer eliminates duplicated popup lifecycle code, but the slash command list's hidden-scrollbar CSS classes were not ported to SUGGESTION_SCROLL_CLASS, producing a minor visual regression on that menu.

Confidence Score: 5/5

Safe to merge — all new surfaces reuse the existing TipTap extension stack and the changes are well-isolated behind feature boundaries.

The mention subsystem, the controlled RichMarkdownField, and the dirty-on-open fix are all well-structured with unit tests covering the key invariants. The only notable change from the extraction refactor is that the slash command popup no longer hides its scrollbar, which is a cosmetic gap rather than a behavioural one.

suggestion-menu-chrome.ts — the shared scroll class is missing the scrollbar-hiding CSS that the original slash command list carried.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/suggestion-popup.ts Extracted shared popup-lifecycle renderer, eliminating duplicate teardown/autoUpdate code from the slash command. The slash command list's scrollbar is now visible (scrollbar-hiding CSS was not ported to SUGGESTION_SCROLL_CLASS).
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-list.tsx New @-mention popup list. Uses useSyncExternalStore to reactively read from MentionStore; the Loading… vs No results heuristic is sound given integrations are always present.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field.tsx New controlled WYSIWYG markdown field for modals. Correct seed-once/streaming/settle lifecycle; canonicalSeed comparison avoids false-dirty on non-canonical initial values.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts Adds normalizeBaseline opt-in to fix false-dirty-on-open; everStreamedRef is intentionally set during render (not an effect) so the baseline stays stable through post-stream reconciliation.
apps/sim/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx Replaces plain textarea with RichMarkdownField; contentSeed remount key correctly handles reset-on-open, import, and SKILL.md paste. The paste-destructure heuristic (starts with ---, name required) is safely conservative.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx Upgraded description to RichMarkdownField; isTooLong guard prevents saves when description exceeds 50 000 chars, consistent with the updated API contract.
apps/sim/lib/api/contracts/deployments.ts Description limit raised from 2 000 to 50 000 chars to accommodate rich markdown. The .trim() check is preserved; no apparent DB column constraint mismatch visible in this diff.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Host as Host Component
    participant UEM as useEditorMentions
    participant UMM as useMarkdownMentions
    participant RQ as React Query hooks
    participant Store as MentionStore
    participant TipTap as TipTap Mention Extension
    participant List as MentionList

    Host->>UEM: mount(editor, workspaceId)
    UEM->>TipTap: "storage.mention.enabled = true"
    UEM->>TipTap: "storage.mention.onOpen = setActive"

    Note over Host: User types @
    TipTap->>TipTap: Suggestion plugin fires
    TipTap->>UEM: onOpen → setActive(true)
    UEM->>UMM: enabled: true
    UMM->>RQ: enable all workspace queries
    RQ-->>UMM: data arrives
    UMM-->>UEM: items[]
    UEM->>Store: store.set(items[])
    Store-->>List: useSyncExternalStore notifies
    List->>List: filter by query, render chips

    Note over Host: User selects item
    List->>TipTap: command(kind, id, label)
    TipTap->>TipTap: insertContent mention node
    TipTap->>Host: onUpdate serialized as sim:kind/id
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 Host as Host Component
    participant UEM as useEditorMentions
    participant UMM as useMarkdownMentions
    participant RQ as React Query hooks
    participant Store as MentionStore
    participant TipTap as TipTap Mention Extension
    participant List as MentionList

    Host->>UEM: mount(editor, workspaceId)
    UEM->>TipTap: "storage.mention.enabled = true"
    UEM->>TipTap: "storage.mention.onOpen = setActive"

    Note over Host: User types @
    TipTap->>TipTap: Suggestion plugin fires
    TipTap->>UEM: onOpen → setActive(true)
    UEM->>UMM: enabled: true
    UMM->>RQ: enable all workspace queries
    RQ-->>UMM: data arrives
    UMM-->>UEM: items[]
    UEM->>Store: store.set(items[])
    Store-->>List: useSyncExternalStore notifies
    List->>List: filter by query, render chips

    Note over Host: User selects item
    List->>TipTap: command(kind, id, label)
    TipTap->>TipTap: insertContent mention node
    TipTap->>Host: onUpdate serialized as sim:kind/id
Loading

Reviews (5): Last reviewed commit: "feat(rich-editor): render mentions as ic..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a controlled RichMarkdownField (TipTap-backed, file-less) and wires it into the skill Content field and the deploy version description editor. It also introduces a full @-mention system (TipTap suggestion extension, reactive MentionStore, useMarkdownMentions aggregator, and useEditorMentions coordinator) that inserts portable sim:<kind>/<id> markdown links. A normalizeMarkdownContent baseline fix prevents false "unsaved changes" on open, and version display is refactored to always show the numeric v3 · name format.

  • RichMarkdownField reuses all existing TipTap extensions/parsers with no file I/O and is dynamically imported (ssr: false) in modals; streaming mode mirrors AI-generated content and settles back to editable when generation ends.
  • The @ menu is lazy-activated (queries disabled until first @ trigger), populates reactively via useSyncExternalStore into a detached ReactRenderer, and shares all popup chrome with the / slash-command menu through a refactored createSuggestionPopupRenderer.
  • normalizeMarkdownContent round-trips markdown through the editor's serializer to produce the canonical form, preventing the deferred mount-time re-serialization from falsely marking files dirty on open.

Confidence Score: 4/5

The PR is safe to merge. The core mechanics — duplicate plugin key prevention, sim: link round-trip, dirty-on-open baseline, streaming mirror — are all covered by new tests. The two findings are both about edge-case consistency rather than broken paths.

Two inconsistencies worth addressing before shipping: folders and skills lack the same { enabled: active } guard that files and knowledgeBases carry, so those queries may fire early if the underlying hooks don't self-gate on empty string. And the new !editValue.trim() guard silently swallows attempts to clear a named version's label with no feedback to the user. Neither blocks the primary feature paths.

use-markdown-mentions.ts (inconsistent enabled gating) and versions.tsx (silent no-op when clearing a version name).

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/use-markdown-mentions.ts New hook aggregating workspace entities for the @ menu; folders and skills queries omit { enabled: active } while files and knowledgeBases explicitly include it.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention.ts New TipTap extension wiring the @ mention popup; uses a distinct plugin key to coexist with slash-command, and correctly gates on whitespace/start-of-block to avoid triggering inside email addresses.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-list.tsx New @ menu list component mirroring slash-command list chrome; uses useSyncExternalStore for reactive updates from the MentionStore.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/suggestion-popup.ts New shared popup renderer for both slash-command and @ mention suggestion popups; properly handles double-teardown (destroy + onExit) via null-checks, safe in all cleanup paths.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field.tsx New controlled WYSIWYG markdown field for modal embed; correctly holds frontmatter out-of-band, handles streaming mirror, and intercepts paste for SKILL.md destructuring.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts Adds normalizeBaseline option to prevent false dirty-on-open for canonically-serialized files; everStreamedRef latch correctly skips normalization for any mount that ever streams.
apps/sim/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal.tsx Replaces plain textarea with RichMarkdownField; contentSeed key remounts editor on programmatic import, and onPasteText handles SKILL.md paste-destructuring.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/version-description-modal.tsx Upgrades description field to RichMarkdownField with 2000-char limit enforced via isTooLong guard; uses isStreaming=isGenerating to mirror AI-generated content into the editor.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx Refactors version display to always show numeric version alongside name (v3 · name); rename now starts empty with placeholder, and empty-name saves are blocked at the guard.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/normalize-content.ts New utility normalizing markdown to canonical editor form to neutralize false dirty-on-open signals; correctly skips round-trip-unsafe content.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/format-version-label.ts Tiny pure helper formatting v3 or v3 · name; used consistently across all version label sites in general.tsx and versions.tsx.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-store.ts Minimal external store bridging React Query data into TipTap's detached ReactRenderer; identity-checks on set to avoid spurious listener notifications.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Host as Host Component
    participant UEM as useEditorMentions
    participant UMM as useMarkdownMentions
    participant Store as MentionStore
    participant Ext as Mention Extension
    participant List as MentionList

    Host->>UEM: mount (workspaceId)
    UEM->>Ext: "storage.enabled = true"
    UEM->>Ext: "storage.onOpen = () => setActive(true)"
    Note over UMM: queries disabled (active=false)

    Host->>Ext: "user types @"
    Ext->>Ext: allow() → true
    Ext->>UEM: onOpen() → setActive(true)
    UEM->>UMM: "enabled=true → React Query fires"
    UMM-->>UEM: items[] (async)
    UEM->>Store: store.set(items)
    Store-->>List: useSyncExternalStore notify
    List-->>Host: menu populated reactively

    Host->>List: user selects item
    List->>Ext: command(item)
    Ext->>Ext: insertContent [label](sim:kind/id)
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 Host as Host Component
    participant UEM as useEditorMentions
    participant UMM as useMarkdownMentions
    participant Store as MentionStore
    participant Ext as Mention Extension
    participant List as MentionList

    Host->>UEM: mount (workspaceId)
    UEM->>Ext: "storage.enabled = true"
    UEM->>Ext: "storage.onOpen = () => setActive(true)"
    Note over UMM: queries disabled (active=false)

    Host->>Ext: "user types @"
    Ext->>Ext: allow() → true
    Ext->>UEM: onOpen() → setActive(true)
    UEM->>UMM: "enabled=true → React Query fires"
    UMM-->>UEM: items[] (async)
    UEM->>Store: store.set(items)
    Store-->>List: useSyncExternalStore notify
    List-->>Host: menu populated reactively

    Host->>List: user selects item
    List->>Ext: command(item)
    Ext->>Ext: insertContent [label](sim:kind/id)
Loading

Reviews (2): Last reviewed commit: "feat(rich-editor): rich markdown field +..." | Re-trigger Greptile

@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

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor 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 b393ea9. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor 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 bb68df9. Configure here.

…y modals

- Add controlled, file-less RichMarkdownField (sibling of the file editor) used for
  skill Content and deploy version descriptions; placeholder/typography match chip fields
- Add @-mention menu (TipTap suggestion) inserting portable [label](sim:kind/id) links;
  wired into the field and the file viewer via a shared useEditorMentions hook
- Extract a shared suggestion-popup renderer + menu chrome (slash + mention)
- Fix false dirty-on-open: normalize the editor's dirty baseline to canonical markdown
- Always show the deployment version number (v3 · name) so named versions keep a short ref
- Skill import: drop the paste box (Create-tab editor auto-destructures a pasted SKILL.md),
  reorder GitHub → Upload
- RichMarkdownField reports the original value when the doc matches its canonical
  form, so a non-canonical input never reads as a false unsaved change (skill +
  version description modals)
- Add sim: mention link navigation (Cmd/Ctrl-click) to the modal field
- versions: keep the v{n} fallback as the rename guard/seed so re-submitting the
  displayed token is a no-op (no redundant "v3 · v3"); document the clear-name no-op
- Clarify the lazy query-gating comment in useMarkdownMentions
Bump the field's remount key in the reset guard so the seed-once rich editor
re-seeds when content is reset from a changed initialValues (same skill id keeps
the React key otherwise stable), keeping the editor and saved value in sync.
- Render @ mentions as an inline chip node (entity icon + label) instead of a
  blue link; still serializes to the portable [label](sim:kind/id) markdown so
  it round-trips and stays agent-readable (shared mentionIcon resolver)
- Cap the mention/slash menu height + width and scroll it, matching the chat menu
- Give the version description editor more height; lift the 2000-char limit to a
  high anti-abuse cap (client + contract) and drop the visible counter
- Mount the slash/@ menu popup inside the host dialog (when present) instead of
  document.body: Radix's scroll-lock blocks wheel events outside the dialog
  subtree, so a body-level popup couldn't scroll in a modal. position:fixed keeps
  it viewport-positioned (the modal centers via flex, no transform) so it isn't clipped
- Fix the invalid max-w arbitrary value (calc needs spaces) that left the menu uncapped
- Match the version-description editor's dynamic-import loading height to the field
  so the modal doesn't grow when the chunk loads
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1 waleedlatif1 force-pushed the rich-md-skills-deploy branch from bb68df9 to c46ef1b Compare June 25, 2026 23:51
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor 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 c46ef1b. Configure here.

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