feat: Add @mention tagging in ticket comments (#138)#201
Conversation
Add autocomplete dropdown for mentioning team members in comment replies. When users type @ in the comment textarea, a dropdown appears showing available users filtered by the typed query. Mentions are stored as plain text (@DisplayName) for Azure DevOps compatibility and highlighted with visual styling when displayed in comments.
There was a problem hiding this comment.
Pull request overview
Adds @mention support in ticket comment replies by introducing an autocomplete input and rendering-time highlighting of mention text.
Changes:
- Added mention parsing/highlighting utilities (
highlightMentions,extractMentions). - Replaced the reply textarea with a new
MentionInputcomponent that fetches users and provides an @-triggered suggestion dropdown. - Added global CSS styling for highlighted mentions and applied highlighting when rendering comment HTML.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
src/lib/mentions.ts |
New utilities to highlight and extract @... mentions from content. |
src/components/tickets/TicketDetail.tsx |
Uses MentionInput for replies and applies highlightMentions when rendering comment HTML. |
src/components/common/MentionInput.tsx |
New client component implementing @mention autocomplete UI and insertion behavior. |
src/app/globals.css |
Adds .mention styling for highlighted mentions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Create the mention text - use displayName for readability | ||
| const mentionText = `@${user.displayName} `; | ||
|
|
||
| // Replace the @query with the mention | ||
| const beforeMention = value.slice(0, mentionStartIndex); | ||
| const afterMention = value.slice(mentionStartIndex + 1 + mentionQuery.length); | ||
| const newValue = beforeMention + mentionText + afterMention; |
There was a problem hiding this comment.
mentionText inserts @${user.displayName} (with a trailing space) and allows spaces within the display name, but the current parsing/highlighting logic relies on regexes that can’t reliably determine where the mention ends once the user continues typing. To avoid over-highlighting/incorrect extraction, consider storing mentions in an unambiguous plain-text format (e.g. bracketed @[Display Name] or @DisplayName without spaces) or ensuring the renderer matches mentions by lookup against the known user list.
There was a problem hiding this comment.
Acknowledged — this is a known MVP tradeoff. The mention text is always inserted via autocomplete (not manually typed), and the trailing space acts as a soft delimiter. Changing to a bracketed format like \ would solve the ambiguity but adds complexity for a v1 feature. We'll revisit if this causes issues in practice.
src/lib/mentions.ts
Outdated
| // This regex handles display names like "John Doe" or "Jane.Smith" | ||
| const mentionRegex = /(^|[\s>])(@[\w][\w\s.\-']*[\w]|@[\w])/g; | ||
|
|
||
| return html.replace(mentionRegex, (match, prefix, mention) => { |
There was a problem hiding this comment.
The first parameter of the replace callback (match) is unused. With the repo’s ESLint setup, this is likely to be flagged by no-unused-vars. Consider removing it or renaming it to _match (or similar) to satisfy the linter.
| return html.replace(mentionRegex, (match, prefix, mention) => { | |
| return html.replace(mentionRegex, (_match, prefix, mention) => { |
| if (!showSuggestions || suggestions.length === 0) return; | ||
|
|
||
| switch (e.key) { | ||
| case 'ArrowDown': | ||
| e.preventDefault(); | ||
| setSelectedIndex((prev) => (prev + 1) % suggestions.length); | ||
| break; | ||
| case 'ArrowUp': | ||
| e.preventDefault(); | ||
| setSelectedIndex((prev) => (prev - 1 + suggestions.length) % suggestions.length); |
There was a problem hiding this comment.
handleKeyDown returns early when suggestions.length === 0, which prevents the Escape key from closing the suggestions dropdown while results are still loading or when the query yields no matches. Consider handling Escape (and possibly other close behaviors) whenever showSuggestions is true, independent of suggestions.length.
| if (!showSuggestions || suggestions.length === 0) return; | |
| switch (e.key) { | |
| case 'ArrowDown': | |
| e.preventDefault(); | |
| setSelectedIndex((prev) => (prev + 1) % suggestions.length); | |
| break; | |
| case 'ArrowUp': | |
| e.preventDefault(); | |
| setSelectedIndex((prev) => (prev - 1 + suggestions.length) % suggestions.length); | |
| if (!showSuggestions) return; | |
| switch (e.key) { | |
| case 'ArrowDown': | |
| if (suggestions.length > 0) { | |
| e.preventDefault(); | |
| setSelectedIndex((prev) => (prev + 1) % suggestions.length); | |
| } | |
| break; | |
| case 'ArrowUp': | |
| if (suggestions.length > 0) { | |
| e.preventDefault(); | |
| setSelectedIndex((prev) => (prev - 1 + suggestions.length) % suggestions.length); | |
| } |
There was a problem hiding this comment.
Fixed in a7fbfce — restructured the early return to only check showSuggestions, so Escape now works when the dropdown shows "Loading..." or "No users found". Arrow/Enter/Tab are individually guarded by suggestions.length > 0.
| const response = await fetch('/api/devops/users'); | ||
| if (response.ok) { | ||
| const data = await response.json(); | ||
| setAllUsers(data.users || []); | ||
| setHasLoadedUsers(true); |
There was a problem hiding this comment.
The mention suggestions are fetched from /api/devops/users, which returns a combined list that includes customers/ticket requesters in addition to internal team members. This doesn't match the PR description (“mentioning team members”). Consider switching to an endpoint that is already filtered to internal users (e.g. /api/devops/team) or filtering the fetched list to internal/team users before showing suggestions.
There was a problem hiding this comment.
Intentional — in a support ticketing context, users may want to mention customers/requesters (e.g. @John Smith requested this), not just internal team members. The broader /api/devops/users list is by design.
src/lib/mentions.ts
Outdated
| export function extractMentions(text: string): string[] { | ||
| if (!text) return []; | ||
|
|
||
| const mentionRegex = /@([\w][\w\s.\-']*[\w]|[\w])/g; |
There was a problem hiding this comment.
extractMentions will also match the domain part of email addresses (e.g. john@contoso.com -> contoso.com) because the regex doesn’t require a start/whitespace boundary before @. If this is meant to extract only explicit @mentions, update the regex to require an appropriate boundary (start-of-string or whitespace) before @ (or use a negative lookbehind for non-whitespace).
| const mentionRegex = /@([\w][\w\s.\-']*[\w]|[\w])/g; | |
| const mentionRegex = /(?<!\S)@([\w][\w\s.\-']*[\w]|[\w])/g; |
There was a problem hiding this comment.
Fixed in a7fbfce — added (?<!\S) negative lookbehind so email addresses like john@contoso.com are no longer matched.
| // - Start with @ preceded by whitespace or start of string | ||
| // - Followed by a name (letters, numbers, spaces, dots, hyphens) | ||
| // - Name continues until we hit certain delimiters or end | ||
| // This regex handles display names like "John Doe" or "Jane.Smith" | ||
| const mentionRegex = /(^|[\s>])(@[\w][\w\s.\-']*[\w]|@[\w])/g; | ||
|
|
There was a problem hiding this comment.
The mention regex allows arbitrary whitespace (\s) inside the mention token, so it will greedily consume normal text after a mention (e.g. "Hi @John Doe thanks" would match "@John Doe thanks"). Since MentionInput inserts a trailing space after the mention, this will commonly over-highlight/over-extract. Consider changing the mention syntax or regex so mentions have an unambiguous terminator (e.g., punctuation/end, a bracketed format, or matching against a known user list).
There was a problem hiding this comment.
Same root issue as the mention format comment above — the regex allows \ inside mentions because Azure DevOps display names contain spaces (e.g. "John Doe"). The \ regex does have a boundary check \ before \ which mitigates the worst cases. Fixing properly requires changing the mention format (e.g. ), which we'll consider in a future iteration.
|
Can you respond to GitHub Copilot please @akash2017sky |
- Rename unused `match` param to `_match` in highlightMentions (lint) - Add negative lookbehind to extractMentions regex to avoid matching email domains - Restructure handleKeyDown so Escape works when suggestions are empty/loading - Remove redundant showSuggestions checks in Enter/Tab cases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Merge conflict on this one @EdiWeeks (recent merge) |
Combine @mention tagging imports (HEAD) with attachment imports (main). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I resolved conflict, @BenGWeeks please merge |


Add autocomplete dropdown for mentioning team members in comment replies.
When users type @ in the comment textarea, a dropdown appears showing
available users filtered by the typed query. Mentions are stored as plain
text (@DisplayName) for Azure DevOps compatibility and highlighted with
visual styling when displayed in comments.