Skip to content

feat: Add @mention tagging in ticket comments (#138)#201

Merged
BenGWeeks merged 3 commits intomainfrom
claude/fix-issue-138-cC4Yu
Feb 10, 2026
Merged

feat: Add @mention tagging in ticket comments (#138)#201
BenGWeeks merged 3 commits intomainfrom
claude/fix-issue-138-cC4Yu

Conversation

@BenGWeeks
Copy link
Contributor

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.

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.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 MentionInput component 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.

Comment on lines +195 to +201
// 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;
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

// 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) => {
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
return html.replace(mentionRegex, (match, prefix, mention) => {
return html.replace(mentionRegex, (_match, prefix, mention) => {

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a7fbfce — renamed to _match.

Comment on lines 158 to 167
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);
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +52 to +56
const response = await fetch('/api/devops/users');
if (response.ok) {
const data = await response.json();
setAllUsers(data.users || []);
setHasLoadedUsers(true);
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

export function extractMentions(text: string): string[] {
if (!text) return [];

const mentionRegex = /@([\w][\w\s.\-']*[\w]|[\w])/g;
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
const mentionRegex = /@([\w][\w\s.\-']*[\w]|[\w])/g;
const mentionRegex = /(?<!\S)@([\w][\w\s.\-']*[\w]|[\w])/g;

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a7fbfce — added (?<!\S) negative lookbehind so email addresses like john@contoso.com are no longer matched.

Comment on lines +15 to +20
// - 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;

Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@BenGWeeks
Copy link
Contributor Author

Can you respond to GitHub Copilot please @akash2017sky

@BenGWeeks BenGWeeks marked this pull request as draft February 9, 2026 09:52
- 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>
@EdiWeeks
Copy link
Collaborator

Tagging works as expected
image

@BenGWeeks
Copy link
Contributor Author

Merge conflict on this one @EdiWeeks (recent merge)

@BenGWeeks
Copy link
Contributor Author

BenGWeeks commented Feb 10, 2026

Tagging works as expected

Assume you've confirmed it appears in DevOps as expected as well.

Yes:
image

Combine @mention tagging imports (HEAD) with attachment imports (main).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@EdiWeeks EdiWeeks marked this pull request as ready for review February 10, 2026 16:50
@EdiWeeks
Copy link
Collaborator

I resolved conflict, @BenGWeeks please merge

@BenGWeeks BenGWeeks merged commit 601c3cd into main Feb 10, 2026
4 checks passed
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.

4 participants