Skip to content

feat: @ mention picker in the rich text editor - #237

Merged
StephenTangCook merged 7 commits into
mainfrom
claude/rich-text-mention-picker
Oct 12, 2026
Merged

StephenTangCook merged 7 commits into
mainfrom
claude/rich-text-mention-picker

Conversation

@StephenTangCook

@StephenTangCook StephenTangCook commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in mentions prop. In the rich text WYSIWYG, typing @ (or the new Mention toolbar button) opens a picker backed by the host's searchUsers, and picking someone inserts a rich text user element ({ type: "user", user_id }). An optional getUserName(userId) labels mentions in the editor, including ones that arrived with the draft or a loaded message.

<BlockKitchen
  mentions={{
    searchUsers: (query, { signal }) => searchMembers(query, signal), // → { id, name, avatarUrl? }[]
    getUserName: (userId) => names.get(userId)
  }}
/>
  • Trigger: @tiptap/suggestion (same 3.30.x as the other TipTap packages, so ProseMirror stays deduped).
    • It is debounced at 200 ms, aborts superseded searches through signal, and ignores the @ inside name@domain.
    • It's gated by the suggestion's allow, so a host turning mentions on or off keeps the same editor, with its content, caret and history.
  • Picker: a nested Radix popover anchored at the caret. Focus stays in the editor (arrow keys, Enter/Tab, Escape), and clicking a row doesn't close a surrounding block-editor popover. It shows the previous results while the next search runs, "Searching…" before the first results, and an empty state on no match or a rejected search.
  • Round-trip: user mentions no longer force the structured editor. A userMention node imports and exports user elements, carrying bold, italic, strike and underline as marks (code is dropped, since Slack rejects it on a user element). Mentions with highlight, client_highlight or unlink still fall back to the structured editor. A mention inside a code block falls back too, through the general code-block check fix(editors): keep links and emoji in rich text code blocks #238 added; this branch carries the identical hunk and has main merged in.
  • Escape: the picker stops its own Escape at Radix's document-capture listener. Otherwise it would also reach the inline rich text editor, whose Escape handler discards the whole edit.
  • Preview hooks fix (fix(preview)): slack-blocks-to-jsx copies hooks into state when <Message> mounts and never reads the prop again. So a host that resolves a mention's name after the first paint kept showing the raw @U… id. This affected mentions already in a draft too, not only picked ones. SlackBlockPreview now hands the renderer one stable object that reads through to the latest previewHooks, without remounting.
  • Emoji-load race fix (fix(editors)): the editor re-seeds from block when the emoji dataset finishes loading. That effect could run with a block older than the user's latest edit (the host hadn't re-rendered with it yet) and revert the edit. That lost an early keystroke, or undid an @ pick and reopened the picker. It now re-seeds on a resolver change only while the user hasn't edited.
  • Hosts that don't pass mentions get no picker. Existing mentions still round-trip and show as @U….

Why

Tightknit's Message Blaster composer (ENG-6346) needs admins to @mention members while composing. Today the only way is hand-typing a raw Slack id in Advanced mode. The search stays host-brokered, like every other I/O concern in the package.

Test plan

  • pnpm typecheck
  • pnpm lint
  • pnpm test: all 671 tests pass (unit, plus the Storybook browser project with the new WithMentions story), run against a pre-provisioned Chromium (BK_CHROMIUM_EXECUTABLE).
  • Spot-checked in pnpm storybook (BlockKitchen › With Mentions) with Playwright-driven Chromium:
    • Typing @gr and clicking a row inserts @Grace Hopper.
    • The toolbar button, then typing, ArrowDown and Enter inserts a mention.
    • Escape closes only the picker, and the inline edit survives.
    • A bare Escape still cancels the inline edit.
    • No page errors.
  • Consumed from a local build in Tightknit Studio's Message Blaster composer: the picker searches members, and the builder preview resolves the picked and seeded mentions to names.
  • Picker stability: before the emoji-load fix, the search-then-pick test failed intermittently (the picker reopened after a pick). After it, the test passed 35 runs in a row. One more failure showed up in an earlier batch of 12 before I added failure capture; its cause wasn't recorded.

New tests:

  • test/rich-text-tiptap-mentions.test.ts: lossy detection (styles, code block), round-trip with styles, and export dropping name and code.
  • test/rich-text-mention-picker.test.tsx:
    • Search, then click or keyboard to pick; the toolbar trigger.
    • Escape doesn't reach the host's onKeyDown; this test fails without the fix.
    • Failed search, the email @, getUserName labels, and the no-config path.
    • Turning mentions on after mount keeps the same editor.
    • BlockKitchen passes the prop down to the editor.
  • test/slack-block-preview-hooks.test.tsx: a preview re-rendered with new hooks uses them, and dropping them falls back to the default. Both fail without the fix.
  • test/rich-text-editor-emoji-load.test.tsx: an edit made before the host re-renders survives the emoji dataset landing (this fails without the fix), and an untouched editor still picks up emoji glyphs.

Notes for reviewer


🤖 Generated with Claude Code

https://claude.ai/code/session_01MTy6g5wxP9DqjWJQshGz51

Adds an opt-in `mentions` prop: typing `@` (or the toolbar's Mention
button) in the rich text WYSIWYG opens a picker backed by the host's
`searchUsers`, and picking someone inserts a rich text `user` element.
`getUserName` labels mentions in the editor by id.

User mentions now round-trip through the WYSIWYG (with bold, italic,
strike and underline) instead of forcing the structured editor. The
picker's Escape is kept from reaching the inline editor, which would
otherwise discard the whole edit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTy6g5wxP9DqjWJQshGz51
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Cloudflare preview

✅ Deployed c8eb5e1 · Branch preview · Commit preview · Build logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
block-kitchen c8eb5e1 Commit Preview URL

Branch Preview URL
Oct 12 2026, 02:42 PM

slack-blocks-to-jsx copies `hooks` into state when `<Message>` mounts and
never reads the prop again, so a host that resolves a mention's name after
the first paint kept showing the raw `@U…` id. Hand the renderer one stable
object that reads through to the latest hooks instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTy6g5wxP9DqjWJQshGz51

Copy link
Copy Markdown
Contributor Author

Audit (pnpm) fails here, but this PR didn't cause it. The same job has been red on main since at least the 0.10.18 release run. Both advisories come through dependency paths this PR doesn't touch:

  • prosemirror-view <1.42.3 (GHSA-c8x8-7fp4-3x9w, paste XSS), via @tiptap/extension-link > @tiptap/pm.
  • braces <=3.0.3 (GHSA-vfj7-8cjw-p6xm), via @tailwindcss/cli > @parcel/watcher > micromatch. This one has no patched release, and the path is build-time only.

No fix exists in an open PR yet. Proposed patch for package.json → pnpm. The override matches the floor tightknit-app already pins for ENG-6316. Ignoring braces is a policy call, so I've left it out of this PR:

"overrides": {
  "esbuild": ">=0.28.1",
  "undici": "^7.29.0",
  "prosemirror-view": "^1.42.3"
},
"auditConfig": {
  "ignoreGhsas": ["GHSA-vfj7-8cjw-p6xm"]
}

Generated by Claude Code

Generalize the mention-in-code-block check to every non-text inline and
styled text run, matching #238, so the two
changes merge cleanly in either order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTy6g5wxP9DqjWJQshGz51
The rich text editor re-seeds from `block` when the emoji resolver changes.
That effect can run with a `block` that predates the user's latest edit (the
host hasn't re-rendered with it yet), and re-seeding then reverted the edit:
an early keystroke, or an `@` pick. Re-seed on a resolver change only while
the user hasn't edited.

The `@` trigger is now gated by the suggestion's `allow` instead of
rebuilding the editor when the host turns mentions on or off, so toggling
keeps the editor's content, caret and history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTy6g5wxP9DqjWJQshGz51
Clears GHSA-c8x8-7fp4-3x9w (high; prosemirror-view paste XSS) by pinning
prosemirror-view ^1.42.3, and pins prosemirror-model ^1.25.12 with it so
the bundle keeps a single ProseMirror model. Ignores GHSA-vfj7-8cjw-p6xm
(braces), which has no patched release and only reaches the build
toolchain through @tailwindcss/cli.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTy6g5wxP9DqjWJQshGz51
@StephenTangCook
StephenTangCook merged commit bbe9974 into main Oct 12, 2026
13 checks passed
@StephenTangCook
StephenTangCook deleted the claude/rich-text-mention-picker branch October 12, 2026 14:59
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.

2 participants