1+ import { RobotIcon } from "@phosphor-icons/react" ;
12import { Avatar , AvatarFallback , InputGroup } from "@posthog/quill" ;
23import type { UserBasic } from "@posthog/shared/domain-types" ;
34import { getUserInitials } from "@posthog/ui/features/auth/userInitials" ;
45import {
6+ type ComposerMentionCandidate ,
57 contentToDoc ,
68 docToContent ,
7- filterMentionCandidates ,
9+ filterComposerMentionCandidates ,
810} from "@posthog/ui/features/canvas/utils/mentionComposer" ;
911import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay" ;
1012import { Text } from "@radix-ui/themes" ;
@@ -23,6 +25,7 @@ interface MentionComposerProps {
2325 onSubmit : ( ) => void ;
2426 /** The taggable pool; typically the org's members. */
2527 members : UserBasic [ ] ;
28+ allowAgentMention ?: boolean ;
2629 onMentionInsert ?: ( member : UserBasic ) => void ;
2730 placeholder ?: string ;
2831 rows ?: number ;
@@ -40,8 +43,8 @@ const EDITOR_CLASS =
4043 "w-full px-2.5 py-2 outline-none break-words [overflow-wrap:break-word] [white-space:pre-wrap] [word-break:break-word]" ;
4144
4245interface SuggestionSession {
43- items : UserBasic [ ] ;
44- command : ( member : UserBasic ) => void ;
46+ items : ComposerMentionCandidate [ ] ;
47+ command : ( candidate : ComposerMentionCandidate ) => void ;
4548}
4649
4750/**
@@ -55,6 +58,7 @@ export function MentionComposer({
5558 onValueChange,
5659 onSubmit,
5760 members,
61+ allowAgentMention = false ,
5862 onMentionInsert,
5963 placeholder,
6064 rows,
@@ -78,6 +82,8 @@ export function MentionComposer({
7882 // latest props and popup state.
7983 const membersRef = useRef ( members ) ;
8084 membersRef . current = members ;
85+ const allowAgentMentionRef = useRef ( allowAgentMention ) ;
86+ allowAgentMentionRef . current = allowAgentMention ;
8187 const onValueChangeRef = useRef ( onValueChange ) ;
8288 onValueChangeRef . current = onValueChange ;
8389 const onSubmitRef = useRef ( onSubmit ) ;
@@ -121,11 +127,21 @@ export function MentionComposer({
121127 char : "@" ,
122128 allowSpaces : true ,
123129 items : ( { query } ) =>
124- filterMentionCandidates ( membersRef . current , query ) ,
125- // The suggestion pipeline carries whole members, not node attrs,
126- // so member data survives into `command`.
130+ filterComposerMentionCandidates (
131+ membersRef . current ,
132+ query ,
133+ allowAgentMentionRef . current ,
134+ ) ,
127135 command : ( { editor : e , range, props } ) => {
128- const member = props as unknown as UserBasic ;
136+ const candidate = props as unknown as ComposerMentionCandidate ;
137+ if ( candidate . kind === "agent" ) {
138+ e . chain ( )
139+ . focus ( )
140+ . insertContentAt ( range , { type : "text" , text : "@agent " } )
141+ . run ( ) ;
142+ return ;
143+ }
144+ const { member } = candidate ;
129145 e . chain ( )
130146 . focus ( )
131147 . insertContentAt ( range , [
@@ -143,17 +159,17 @@ export function MentionComposer({
143159 setDismissed ( false ) ;
144160 setSelectedIndex ( 0 ) ;
145161 setSession ( {
146- items : props . items as unknown as UserBasic [ ] ,
147- command : ( member ) =>
148- props . command ( member as unknown as MentionNodeAttrs ) ,
162+ items : props . items as unknown as ComposerMentionCandidate [ ] ,
163+ command : ( candidate ) =>
164+ props . command ( candidate as unknown as MentionNodeAttrs ) ,
149165 } ) ;
150166 } ,
151167 onUpdate : ( props ) => {
152168 setSelectedIndex ( 0 ) ;
153169 setSession ( {
154- items : props . items as unknown as UserBasic [ ] ,
155- command : ( member ) =>
156- props . command ( member as unknown as MentionNodeAttrs ) ,
170+ items : props . items as unknown as ComposerMentionCandidate [ ] ,
171+ command : ( candidate ) =>
172+ props . command ( candidate as unknown as MentionNodeAttrs ) ,
157173 } ) ;
158174 } ,
159175 onKeyDown : ( { event } ) => {
@@ -172,8 +188,8 @@ export function MentionComposer({
172188 return true ;
173189 }
174190 if ( event . key === "Enter" || event . key === "Tab" ) {
175- const member = items [ highlightedRef . current ] ;
176- if ( member ) sessionRef . current ?. command ( member ) ;
191+ const candidate = items [ highlightedRef . current ] ;
192+ if ( candidate ) sessionRef . current ?. command ( candidate ) ;
177193 return true ;
178194 }
179195 return false ;
@@ -227,37 +243,49 @@ export function MentionComposer({
227243 < div className = "absolute inset-x-0 bottom-full z-50 mb-1 flex flex-col overflow-hidden rounded-md border border-[var(--gray-a6)] bg-[var(--color-panel-solid)] text-[13px] shadow-lg" >
228244 < div
229245 role = "listbox"
230- aria-label = "Mention a teammate"
246+ aria-label = "Mention a teammate or agent "
231247 className = "max-h-56 overflow-y-auto py-1"
232248 >
233- { session . items . map ( ( member , index ) => (
249+ { session . items . map ( ( candidate , index ) => (
234250 < button
235251 type = "button"
236252 role = "option"
237253 aria-selected = { index === highlightedIndex }
238- key = { member . uuid }
254+ key = {
255+ candidate . kind === "agent" ? "agent" : candidate . member . uuid
256+ }
239257 ref = { ( el ) => {
240258 itemRefs . current [ index ] = el ;
241259 } }
242260 // Keep focus in the editor so insertion lands at the caret.
243261 onMouseDown = { ( event ) => event . preventDefault ( ) }
244- onClick = { ( ) => session . command ( member ) }
262+ onClick = { ( ) => session . command ( candidate ) }
245263 onMouseEnter = { ( ) => setSelectedIndex ( index ) }
246264 className = { `flex w-full items-center gap-2 border-none px-2 py-1 text-left ${
247265 index === highlightedIndex ? "bg-[var(--accent-a4)]" : ""
248266 } `}
249267 >
250268 < Avatar size = "xs" className = "shrink-0" >
251- < AvatarFallback > { getUserInitials ( member ) } </ AvatarFallback >
269+ < AvatarFallback >
270+ { candidate . kind === "agent" ? (
271+ < RobotIcon size = { 12 } />
272+ ) : (
273+ getUserInitials ( candidate . member )
274+ ) }
275+ </ AvatarFallback >
252276 </ Avatar >
253277 < Text size = "1" weight = "medium" className = "truncate" >
254- { userDisplayName ( member ) }
278+ { candidate . kind === "agent"
279+ ? "Agent"
280+ : userDisplayName ( candidate . member ) }
255281 </ Text >
256282 < Text
257283 size = "1"
258284 className = "ml-auto shrink-0 truncate text-muted-foreground"
259285 >
260- { member . email }
286+ { candidate . kind === "agent"
287+ ? "Send to agent"
288+ : candidate . member . email }
261289 </ Text >
262290 </ button >
263291 ) ) }
0 commit comments