Skip to content

feat(comments): render @mentions as pills instead of running text - #223

Merged
huhamhire merged 1 commit into
devfrom
feat/mention-pill
Sep 8, 2026
Merged

huhamhire merged 1 commit into
devfrom
feat/mention-pill

Conversation

@huhamhire

Copy link
Copy Markdown
Owner

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 @Override in a snippet would be noise, not signal. That boundary is written into the doc so the next person does not simply grep for ReactMarkdown and add it everywhere.

Three things worth reviewing

  • Parsing lives beside writing. findMentions sits next to formatMention in shared/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.
  • Matching is syntactic, not resolved. There is no authoritative local list of who exists on the remote (a mention may name someone outside this PR''s participants), so anything shaped like a mention is styled. That trade only holds because a false positive costs a tinted background and nothing else — the text is unaltered and nothing becomes clickable. Boundary rules exclude the common false positives (email addresses, scoped package names like @scope/pkg, trailing sentence punctuation); code spans and fences are excluded for free, since only mdast text nodes are rewritten.
  • The class had to be allowlisted for sanitize. Comment bodies pass through rehype-sanitize, which strips class from a span by 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-form className, so a comment cannot borrow arbitrary app styles by writing raw HTML.

Verification

lint / typecheck / test / build pass. 13 new tests in shared cover the matching boundaries: email addresses, scoped packages, trailing punctuation, adjacent mentions, an empty quoted token, exact offsets, and round-tripping both forms formatMention can produce.

The mdast → hast output was also exercised against a real unified pipeline before committing (the custom-node → span behaviour was the one assumption worth proving). That run changed the implementation twice:

  • it showed the Bitbucket quoted form was rendering its quotes inside the pill, which is why the pill now displays the bare name;
  • and it confirmed the sanitize allowlist holds — an injected <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

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>
@huhamhire huhamhire added the enhancement New feature or request label Sep 8, 2026
@huhamhire
huhamhire merged commit b06eb97 into dev Sep 8, 2026
3 checks passed
@huhamhire huhamhire mentioned this pull request Sep 9, 2026
3 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant