feat(comments): render @mentions as pills instead of running text - #223
Merged
Merged
Conversation
A mention is a reference to a person, but as plain text it disappears into the sentence -- exactly when the reader is scanning a thread for whether it concerns them. A remark plugin now renders each mention as a pill, wherever author-written prose that can name someone is rendered: the activity page, the inline comment zone, both draft surfaces and the PR description. Drafts are included because a draft is a comment about to be posted, and the same body must not read differently before and after publishing. Agent-facing markdown (chat, finding cards, rule previews) is not: nothing there addresses a person. Parsing lives beside writing. findMentions sits next to formatMention in shared/mention.ts, so the syntax -- notably Bitbucket's quoted @"first.last", required for a username containing a dot -- is defined once rather than once per direction. The pill accordingly shows @first.last: the quotes are platform syntax, not part of anyone's name, and this is the one place the rendered text intentionally differs from the source. Matching is syntactic, not resolved: there is no authoritative local list of who exists on the remote, so anything shaped like a mention is styled. That trade holds only because a false positive costs a tinted background and nothing else -- the text is unaltered and nothing becomes clickable. Boundary rules still exclude the common ones (email addresses, scoped package names, trailing punctuation), and code spans and fences are excluded for free, since only mdast text nodes are rewritten. The pill class had to be allowlisted for rehype-sanitize, which strips class from a span by default. It is allowed as a value-restricted attribute rather than as free-form className, so a comment cannot borrow arbitrary app styles by writing raw HTML. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A mention is a reference to a person, but as plain text it disappears into the sentence — exactly when the reader is scanning a thread for whether it concerns them. Each mention now renders as a pill.
Where it applies
Wherever author-written prose that can name someone is rendered: the activity page (
CommentItem), the inline diff comment zone (InlineCommentZone), both draft surfaces (DraftZone,DraftsPanel), and the PR description (PrInfoView).Drafts are included because a draft is a comment about to be posted, and the same body must not read differently before and after publishing — the comment-surface consistency rule in 04-comment-interactions.md. The PR description is included because it is the same kind of authored text, and a mention that is a pill in one place and plain text in another reads as a bug.
Agent-facing markdown (chat, finding cards, rule previews) is deliberately not included: nothing there addresses a person, so styling
@Overridein a snippet would be noise, not signal. That boundary is written into the doc so the next person does not simply grep forReactMarkdownand add it everywhere.Three things worth reviewing
findMentionssits next toformatMentioninshared/mention.ts, so the syntax — notably Bitbucket''s quoted@"first.last", which the server requires for a username containing a dot — is defined once instead of once per direction. The pill accordingly displays@first.last: the quotes are platform syntax rather than part of anyone''s name, and this is the one place the rendered text intentionally differs from the source.@scope/pkg, trailing sentence punctuation); code spans and fences are excluded for free, since only mdasttextnodes are rewritten.rehype-sanitize, which stripsclassfrom aspanby default — without this the pill silently does not render at all. It is allowed as a value-restricted attribute ([''className'', ''comment-mention'']) rather than as free-formclassName, so a comment cannot borrow arbitrary app styles by writing raw HTML.Verification
lint/typecheck/test/buildpass. 13 new tests insharedcover the matching boundaries: email addresses, scoped packages, trailing punctuation, adjacent mentions, an empty quoted token, exact offsets, and round-tripping both formsformatMentioncan produce.The mdast → hast output was also exercised against a real unified pipeline before committing (the custom-node →
spanbehaviour was the one assumption worth proving). That run changed the implementation twice:<span class="evil">in a body comes out with its class stripped, while the mention class survives.Not verified in the running app; the rendering was checked at the pipeline level rather than visually.
🤖 Generated with Claude Code