From b2f93dec1a1a8b223e6d96a243d3ec9002b2bc86 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Mon, 19 Jan 2026 16:10:57 -0500 Subject: [PATCH] [MM-67118] Add Agents to @ mention autocomplete in channel (#34881) * Add support for autocomplete of plugin-created bots * stashing * Add support for including agents in the autocomplete list for @ mentions * Change server response to be users rather than interface * fix --- server/channels/api4/user.go | 6 ++++ server/channels/app/agents.go | 29 +++++++++++++++++++ server/public/model/user_autocomplete.go | 1 + .../at_mention_provider.tsx | 27 +++++++++++++++++ webapp/channels/src/i18n/en.json | 1 + 5 files changed, 64 insertions(+) diff --git a/server/channels/api4/user.go b/server/channels/api4/user.go index 3545c04ce22..68366c07df8 100644 --- a/server/channels/api4/user.go +++ b/server/channels/api4/user.go @@ -1324,6 +1324,12 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { autocomplete.Users = result } + // Fetch agent users for autocomplete + agentUsers, appErr := c.App.GetUsersForAgents(c.AppContext, c.AppContext.Session().UserId) + if appErr == nil && agentUsers != nil { + autocomplete.Agents = agentUsers + } + if err := json.NewEncoder(w).Encode(autocomplete); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) } diff --git a/server/channels/app/agents.go b/server/channels/app/agents.go index fe7dcb28e38..28823235069 100644 --- a/server/channels/app/agents.go +++ b/server/channels/app/agents.go @@ -101,6 +101,35 @@ func (a *App) GetAgents(rctx request.CTX, userID string) ([]agentclient.BridgeAg return agents, nil } +// GetUsersForAgents retrieves the User objects for all available agents +func (a *App) GetUsersForAgents(rctx request.CTX, userID string) ([]*model.User, *model.AppError) { + agents, appErr := a.GetAgents(rctx, userID) + if appErr != nil { + return nil, appErr + } + + if len(agents) == 0 { + return []*model.User{}, nil + } + + users := make([]*model.User, 0, len(agents)) + for _, agent := range agents { + // Agents have a username field that corresponds to the bot user's username + user, err := a.Srv().Store().User().GetByUsername(agent.Username) + if err != nil { + rctx.Logger().Warn("Failed to get user for agent", + mlog.Err(err), + mlog.String("agent_id", agent.ID), + mlog.String("username", agent.Username), + ) + continue + } + users = append(users, user) + } + + return users, nil +} + // GetLLMServices retrieves all available LLM services from the bridge API func (a *App) GetLLMServices(rctx request.CTX, userID string) ([]agentclient.BridgeServiceInfo, *model.AppError) { // Check if the AI plugin is active and supports the bridge API (v1.5.0+) diff --git a/server/public/model/user_autocomplete.go b/server/public/model/user_autocomplete.go index b07131b387e..9c1f8a106f7 100644 --- a/server/public/model/user_autocomplete.go +++ b/server/public/model/user_autocomplete.go @@ -15,4 +15,5 @@ type UserAutocompleteInTeam struct { type UserAutocomplete struct { Users []*User `json:"users"` OutOfChannel []*User `json:"out_of_channel,omitempty"` + Agents []*User `json:"agents,omitempty"` } diff --git a/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_provider.tsx b/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_provider.tsx index c316845e4e2..2234fb5ba5b 100644 --- a/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_provider.tsx +++ b/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_provider.tsx @@ -1,8 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import React from 'react'; import {defineMessage} from 'react-intl'; +import {CreationOutlineIcon} from '@mattermost/compass-icons/components'; import type {Group} from '@mattermost/types/groups'; import type {UserProfile} from '@mattermost/types/users'; @@ -306,6 +308,17 @@ export default class AtMentionProvider extends Provider { // Combine the local and remote members, sorting to mix the results together. const localAndRemoteMembers = localMembers.concat(remoteMembers).sort(orderUsers); + // Get agents - these are already User objects from the backend + // Only show agents if bridge is enabled (indicated by presence of agents data) + let agents: CreatedProfile[] = []; + if (this.data && this.data.agents && Array.isArray(this.data.agents) && this.data.agents.length > 0) { + const agentUsers = this.data.agents as UserProfileWithLastViewAt[]; + agents = agentUsers. + filter((user: UserProfileWithLastViewAt) => this.filterProfile(user)). + map((user: UserProfileWithLastViewAt) => this.createFromProfile(user)). + sort(orderUsers); + } + // handle groups const localGroups = this.localGroups(); @@ -345,6 +358,9 @@ export default class AtMentionProvider extends Provider { if (priorityProfiles.length > 0 || localAndRemoteMembers.length > 0) { items.push(membersGroup([...priorityProfiles, ...localAndRemoteMembers])); } + if (agents.length > 0) { + items.push(agentsGroup(agents)); + } if (localAndRemoteGroups.length > 0) { items.push(groupsGroup(localAndRemoteGroups)); } @@ -453,6 +469,17 @@ export function membersGroup(items: CreatedProfile[]) { }; } +export function agentsGroup(items: CreatedProfile[]) { + return { + key: 'agents', + label: defineMessage({id: 'suggestion.mention.agents', defaultMessage: 'Agents'}), + icon: , + items, + terms: items.map((profile) => '@' + profile.username), + component: AtMentionSuggestion, + }; +} + export function groupsGroup(items: Group[]) { return { key: 'groups', diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 9be13f36824..63cab25f6a1 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -5990,6 +5990,7 @@ "suggestion.emoji": "Emoji", "suggestion.group_channel": "Group channel", "suggestion.group.members": "{member_count} {member_count, plural, one {member} other {members}}", + "suggestion.mention.agents": "Agents", "suggestion.mention.all": "Notifies everyone in this channel", "suggestion.mention.channel": "Notifies everyone in this channel", "suggestion.mention.channels": "My Channels",