@@ -58,6 +58,51 @@ import type { ChatContext } from '@/stores/panel'
5858
5959export type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'
6060
61+ function getCaretAnchor (
62+ textarea : HTMLTextAreaElement ,
63+ caretPos : number
64+ ) : { left : number ; top : number } {
65+ const textareaRect = textarea . getBoundingClientRect ( )
66+ const style = window . getComputedStyle ( textarea )
67+
68+ const mirror = document . createElement ( 'div' )
69+ mirror . style . position = 'absolute'
70+ mirror . style . top = '0'
71+ mirror . style . left = '0'
72+ mirror . style . visibility = 'hidden'
73+ mirror . style . whiteSpace = 'pre-wrap'
74+ mirror . style . overflowWrap = 'break-word'
75+ mirror . style . font = style . font
76+ mirror . style . padding = style . padding
77+ mirror . style . border = style . border
78+ mirror . style . width = style . width
79+ mirror . style . lineHeight = style . lineHeight
80+ mirror . style . boxSizing = style . boxSizing
81+ mirror . style . letterSpacing = style . letterSpacing
82+ mirror . style . textTransform = style . textTransform
83+ mirror . style . textIndent = style . textIndent
84+ mirror . style . textAlign = style . textAlign
85+ mirror . textContent = textarea . value . substring ( 0 , caretPos )
86+
87+ const marker = document . createElement ( 'span' )
88+ marker . style . display = 'inline-block'
89+ marker . style . width = '0px'
90+ marker . style . padding = '0'
91+ marker . style . border = '0'
92+ marker . style . verticalAlign = 'text-top'
93+ mirror . appendChild ( marker )
94+
95+ document . body . appendChild ( mirror )
96+ const markerRect = marker . getBoundingClientRect ( )
97+ const mirrorRect = mirror . getBoundingClientRect ( )
98+ document . body . removeChild ( mirror )
99+
100+ return {
101+ left : textareaRect . left + ( markerRect . left - mirrorRect . left ) - textarea . scrollLeft ,
102+ top : textareaRect . top + ( markerRect . top - mirrorRect . top ) - textarea . scrollTop ,
103+ }
104+ }
105+
61106interface UserInputProps {
62107 defaultValue ?: string
63108 draftScopeKey ?: string
@@ -304,7 +349,6 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
304349 const pendingCursorRef = useRef < number | null > ( null )
305350 const mentionRangeRef = useRef < { start : number ; end : number } | null > ( null )
306351 const [ mentionQuery , setMentionQuery ] = useState < string | null > ( null )
307- const containerRef = useRef < HTMLDivElement > ( null )
308352
309353 useImperativeHandle (
310354 ref ,
@@ -664,7 +708,7 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
664708 getActiveMentionAtRef . current = mentionMenu . getActiveMentionQueryAtPosition
665709
666710 const syncMentionState = useCallback (
667- ( _textarea : HTMLTextAreaElement , text : string , caret : number ) => {
711+ ( textarea : HTMLTextAreaElement , text : string , caret : number ) => {
668712 const active = getActiveMentionAtRef . current ( caret , text )
669713 // Treat any whitespace inside the query as a closer — typing a space
670714 // after `@foo` should leave the raw `@foo` text and dismiss the menu.
@@ -682,10 +726,8 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
682726 mentionRangeRef . current = { start : active . start , end : active . end }
683727 setMentionQuery ( active . query )
684728 if ( ! wasActive ) {
685- // Anchor above the whole input box (not at the caret) so the menu can never
686- // overlap the user's typing.
687- const rect = containerRef . current ?. getBoundingClientRect ( )
688- const anchor = rect ? { left : rect . left , top : rect . top } : { left : 0 , top : 0 }
729+ // Anchor at the caret so the menu floats above the user's cursor.
730+ const anchor = getCaretAnchor ( textarea , active . start )
689731 plusMenuRef . current ?. open ( anchor , { mention : true } )
690732 }
691733 } ,
@@ -834,7 +876,6 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
834876
835877 return (
836878 < div
837- ref = { containerRef }
838879 onClick = { handleContainerClick }
839880 className = { cn (
840881 'relative z-10 mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-2.5 py-2 dark:bg-[var(--surface-4)]' ,
0 commit comments