@@ -560,59 +560,62 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
560560 const newCursorPosition = e . target . selectionStart
561561 setCursorPosition ( newCursorPosition )
562562
563- const showMenu = shouldShowContextMenu ( newValue , newCursorPosition )
564- setShowContextMenu ( showMenu )
565-
566- if ( showMenu ) {
567- if ( newValue . startsWith ( "/" ) && ! newValue . includes ( " " ) ) {
568- // Handle slash command - request fresh commands
569- const query = newValue
570- setSearchQuery ( query )
571- // Set to first selectable item (skip section headers)
572- setSelectedMenuIndex ( 1 ) // Section header is at 0, first command is at 1
573- // Request commands fresh each time slash menu is shown
574- vscode . postMessage ( { type : "requestCommands" } )
575- } else {
576- // Existing @ mention handling.
577- const lastAtIndex = newValue . lastIndexOf ( "@" , newCursorPosition - 1 )
578- const query = newValue . slice ( lastAtIndex + 1 , newCursorPosition )
579- setSearchQuery ( query )
563+ // Defer context menu logic to avoid blocking input
564+ requestAnimationFrame ( ( ) => {
565+ const showMenu = shouldShowContextMenu ( newValue , newCursorPosition )
566+ setShowContextMenu ( showMenu )
567+
568+ if ( showMenu ) {
569+ if ( newValue . startsWith ( "/" ) && ! newValue . includes ( " " ) ) {
570+ // Handle slash command - request fresh commands
571+ const query = newValue
572+ setSearchQuery ( query )
573+ // Set to first selectable item (skip section headers)
574+ setSelectedMenuIndex ( 1 ) // Section header is at 0, first command is at 1
575+ // Request commands fresh each time slash menu is shown
576+ vscode . postMessage ( { type : "requestCommands" } )
577+ } else {
578+ // Existing @ mention handling.
579+ const lastAtIndex = newValue . lastIndexOf ( "@" , newCursorPosition - 1 )
580+ const query = newValue . slice ( lastAtIndex + 1 , newCursorPosition )
581+ setSearchQuery ( query )
580582
581- // Send file search request if query is not empty.
582- if ( query . length > 0 ) {
583- setSelectedMenuIndex ( 0 )
583+ // Send file search request if query is not empty.
584+ if ( query . length > 0 ) {
585+ setSelectedMenuIndex ( 0 )
584586
585- // Don't clear results until we have new ones. This
586- // prevents flickering.
587+ // Don't clear results until we have new ones. This
588+ // prevents flickering.
587589
588- // Clear any existing timeout.
589- if ( searchTimeoutRef . current ) {
590- clearTimeout ( searchTimeoutRef . current )
591- }
590+ // Clear any existing timeout.
591+ if ( searchTimeoutRef . current ) {
592+ clearTimeout ( searchTimeoutRef . current )
593+ }
592594
593- // Set a timeout to debounce the search requests.
594- searchTimeoutRef . current = setTimeout ( ( ) => {
595- // Generate a request ID for this search.
596- const reqId = Math . random ( ) . toString ( 36 ) . substring ( 2 , 9 )
597- setSearchRequestId ( reqId )
598- setSearchLoading ( true )
599-
600- // Send message to extension to search files.
601- vscode . postMessage ( {
602- type : "searchFiles" ,
603- query : unescapeSpaces ( query ) ,
604- requestId : reqId ,
605- } )
606- } , 200 ) // 200ms debounce.
607- } else {
608- setSelectedMenuIndex ( 3 ) // Set to "File" option by default.
595+ // Set a timeout to debounce the search requests.
596+ searchTimeoutRef . current = setTimeout ( ( ) => {
597+ // Generate a request ID for this search.
598+ const reqId = Math . random ( ) . toString ( 36 ) . substring ( 2 , 9 )
599+ setSearchRequestId ( reqId )
600+ setSearchLoading ( true )
601+
602+ // Send message to extension to search files.
603+ vscode . postMessage ( {
604+ type : "searchFiles" ,
605+ query : unescapeSpaces ( query ) ,
606+ requestId : reqId ,
607+ } )
608+ } , 200 ) // 200ms debounce.
609+ } else {
610+ setSelectedMenuIndex ( 3 ) // Set to "File" option by default.
611+ }
609612 }
613+ } else {
614+ setSearchQuery ( "" )
615+ setSelectedMenuIndex ( - 1 )
616+ setFileSearchResults ( [ ] ) // Clear file search results.
610617 }
611- } else {
612- setSearchQuery ( "" )
613- setSelectedMenuIndex ( - 1 )
614- setFileSearchResults ( [ ] ) // Clear file search results.
615- }
618+ } )
616619 } ,
617620 [ setInputValue , setSearchRequestId , setFileSearchResults , setSearchLoading , resetOnInputChange ] ,
618621 )
@@ -714,45 +717,56 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
714717 setIsMouseDownOnMenu ( true )
715718 } , [ ] )
716719
720+ // Debounce highlight updates for better performance
721+ const updateHighlightsDebounced = useRef < NodeJS . Timeout | null > ( null )
722+
717723 const updateHighlights = useCallback ( ( ) => {
718- if ( ! textAreaRef . current || ! highlightLayerRef . current ) return
724+ // Clear existing timeout
725+ if ( updateHighlightsDebounced . current ) {
726+ clearTimeout ( updateHighlightsDebounced . current )
727+ }
719728
720- const text = textAreaRef . current . value
729+ // Debounce the highlight update
730+ updateHighlightsDebounced . current = setTimeout ( ( ) => {
731+ if ( ! textAreaRef . current || ! highlightLayerRef . current ) return
721732
722- // Helper function to check if a command is valid
723- const isValidCommand = ( commandName : string ) : boolean => {
724- return commands ?. some ( ( cmd ) => cmd . name === commandName ) || false
725- }
733+ const text = textAreaRef . current . value
726734
727- // Process the text to highlight mentions and valid commands
728- let processedText = text
729- . replace ( / \n $ / , "\n\n" )
730- . replace ( / [ < > & ] / g, ( c ) => ( { "<" : "<" , ">" : ">" , "&" : "&" } ) [ c ] || c )
731- . replace ( mentionRegexGlobal , '<mark class="mention-context-textarea-highlight">$&</mark>' )
732-
733- // Custom replacement for commands - only highlight valid ones
734- processedText = processedText . replace ( commandRegexGlobal , ( match , commandName ) => {
735- // Only highlight if the command exists in the valid commands list
736- if ( isValidCommand ( commandName ) ) {
737- // Check if the match starts with a space
738- const startsWithSpace = match . startsWith ( " " )
739- const commandPart = `/${ commandName } `
740-
741- if ( startsWithSpace ) {
742- // Keep the space but only highlight the command part
743- return ` <mark class="mention-context-textarea-highlight">${ commandPart } </mark>`
744- } else {
745- // Highlight the entire command (starts at beginning of line)
746- return `<mark class="mention-context-textarea-highlight">${ commandPart } </mark>`
747- }
735+ // Helper function to check if a command is valid
736+ const isValidCommand = ( commandName : string ) : boolean => {
737+ return commands ?. some ( ( cmd ) => cmd . name === commandName ) || false
748738 }
749- return match // Return unhighlighted if command is not valid
750- } )
751739
752- highlightLayerRef . current . innerHTML = processedText
740+ // Process the text to highlight mentions and valid commands
741+ let processedText = text
742+ . replace ( / \n $ / , "\n\n" )
743+ . replace ( / [ < > & ] / g, ( c ) => ( { "<" : "<" , ">" : ">" , "&" : "&" } ) [ c ] || c )
744+ . replace ( mentionRegexGlobal , '<mark class="mention-context-textarea-highlight">$&</mark>' )
745+
746+ // Custom replacement for commands - only highlight valid ones
747+ processedText = processedText . replace ( commandRegexGlobal , ( match , commandName ) => {
748+ // Only highlight if the command exists in the valid commands list
749+ if ( isValidCommand ( commandName ) ) {
750+ // Check if the match starts with a space
751+ const startsWithSpace = match . startsWith ( " " )
752+ const commandPart = `/${ commandName } `
753+
754+ if ( startsWithSpace ) {
755+ // Keep the space but only highlight the command part
756+ return ` <mark class="mention-context-textarea-highlight">${ commandPart } </mark>`
757+ } else {
758+ // Highlight the entire command (starts at beginning of line)
759+ return `<mark class="mention-context-textarea-highlight">${ commandPart } </mark>`
760+ }
761+ }
762+ return match // Return unhighlighted if command is not valid
763+ } )
764+
765+ highlightLayerRef . current . innerHTML = processedText
753766
754- highlightLayerRef . current . scrollTop = textAreaRef . current . scrollTop
755- highlightLayerRef . current . scrollLeft = textAreaRef . current . scrollLeft
767+ highlightLayerRef . current . scrollTop = textAreaRef . current . scrollTop
768+ highlightLayerRef . current . scrollLeft = textAreaRef . current . scrollLeft
769+ } , 50 ) // 50ms debounce for highlight updates
756770 } , [ commands ] )
757771
758772 useLayoutEffect ( ( ) => {
@@ -1023,7 +1037,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
10231037 value = { inputValue }
10241038 onChange = { ( e ) => {
10251039 handleInputChange ( e )
1026- updateHighlights ( )
1040+ // Defer highlight update to not block typing
1041+ requestAnimationFrame ( ( ) => updateHighlights ( ) )
10271042 } }
10281043 onFocus = { ( ) => setIsFocused ( true ) }
10291044 onKeyDown = { ( e ) => {
@@ -1081,7 +1096,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
10811096 "scrollbar-none" ,
10821097 "scrollbar-hide" ,
10831098 ) }
1084- onScroll = { ( ) => updateHighlights ( ) }
1099+ onScroll = { ( ) => requestAnimationFrame ( ( ) => updateHighlights ( ) ) }
10851100 />
10861101
10871102 < div className = "absolute bottom-2 right-1 z-30 flex flex-col items-center gap-0" >
0 commit comments