@@ -46,7 +46,7 @@ import type { ContentBlock } from './types/chat'
4646import type { SendMessageFn } from './types/contracts/send-message'
4747import type { User } from './utils/auth'
4848import type { FileTreeNode } from '@codebuff/common/util/file'
49- import type { ScrollBoxRenderable } from '@opentui/core'
49+ import type { KeyEvent , ScrollBoxRenderable } from '@opentui/core'
5050import type { UseMutationResult } from '@tanstack/react-query'
5151import type { Dispatch , SetStateAction } from 'react'
5252
@@ -91,9 +91,11 @@ export const Chat = ({
9191
9292 const [ showReconnectionMessage , setShowReconnectionMessage ] = useState ( false )
9393 const reconnectionTimeout = useTimeout ( )
94+ const [ forceFileOnlyMentions , setForceFileOnlyMentions ] = useState ( false )
9495
9596 const { separatorWidth, terminalWidth, terminalHeight } =
9697 useTerminalDimensions ( )
98+ const messageAvailableWidth = separatorWidth
9799
98100 const theme = useTheme ( )
99101 const markdownPalette = useMemo ( ( ) => createMarkdownPalette ( theme ) , [ theme ] )
@@ -408,13 +410,20 @@ export const Chat = ({
408410 agentSuggestionItems,
409411 fileSuggestionItems,
410412 } = useSuggestionEngine ( {
413+ disableAgentSuggestions : forceFileOnlyMentions ,
411414 inputValue,
412415 cursorPosition,
413416 slashCommands : SLASH_COMMANDS ,
414417 localAgents,
415418 fileTree,
416419 } )
417420
421+ useEffect ( ( ) => {
422+ if ( ! mentionContext . active ) {
423+ setForceFileOnlyMentions ( false )
424+ }
425+ } , [ mentionContext . active ] )
426+
418427 // Reset suggestion menu indexes when context changes
419428 useEffect ( ( ) => {
420429 if ( ! slashContext . active ) {
@@ -456,19 +465,69 @@ export const Chat = ({
456465 setAgentSelectedIndex ,
457466 ] )
458467
459- const { handleSuggestionMenuKey } = useSuggestionMenuHandlers ( {
460- slashContext,
461- mentionContext,
462- slashMatches,
463- agentMatches,
464- fileMatches,
465- slashSelectedIndex,
466- agentSelectedIndex,
467- inputValue,
468- setInputValue,
469- setSlashSelectedIndex,
470- setAgentSelectedIndex,
471- } )
468+ const { handleSuggestionMenuKey : handleSuggestionMenuKeyInternal } =
469+ useSuggestionMenuHandlers ( {
470+ slashContext,
471+ mentionContext,
472+ slashMatches,
473+ agentMatches,
474+ fileMatches,
475+ slashSelectedIndex,
476+ agentSelectedIndex,
477+ inputValue,
478+ setInputValue,
479+ setSlashSelectedIndex,
480+ setAgentSelectedIndex,
481+ } )
482+ const openFileMenuWithTab = useCallback ( ( ) => {
483+ const safeCursor = Math . max ( 0 , Math . min ( cursorPosition , inputValue . length ) )
484+
485+ let wordStart = safeCursor
486+ while ( wordStart > 0 && ! / \s / . test ( inputValue [ wordStart - 1 ] ) ) {
487+ wordStart --
488+ }
489+
490+ const before = inputValue . slice ( 0 , wordStart )
491+ const wordAtCursor = inputValue . slice ( wordStart , safeCursor )
492+ const after = inputValue . slice ( safeCursor )
493+ const mentionWord = wordAtCursor . startsWith ( '@' )
494+ ? wordAtCursor
495+ : `@${ wordAtCursor } `
496+
497+ const text = `${ before } ${ mentionWord } ${ after } `
498+ const nextCursor = before . length + mentionWord . length
499+
500+ setInputValue ( {
501+ text,
502+ cursorPosition : nextCursor ,
503+ lastEditDueToNav : false ,
504+ } )
505+ setForceFileOnlyMentions ( true )
506+ } , [ cursorPosition , inputValue , setInputValue ] )
507+
508+ const handleSuggestionMenuKey = useCallback (
509+ ( key : KeyEvent ) : boolean => {
510+ if ( handleSuggestionMenuKeyInternal ( key ) ) {
511+ return true
512+ }
513+
514+ const isPlainTab =
515+ key &&
516+ key . name === 'tab' &&
517+ ! key . shift &&
518+ ! key . ctrl &&
519+ ! key . meta &&
520+ ! key . option
521+
522+ if ( isPlainTab && ! mentionContext . active ) {
523+ openFileMenuWithTab ( )
524+ return true
525+ }
526+
527+ return false
528+ } ,
529+ [ handleSuggestionMenuKeyInternal , mentionContext . active , openFileMenuWithTab ] ,
530+ )
472531
473532 const { saveToHistory, navigateUp, navigateDown } = useInputHistory (
474533 inputValue ,
@@ -557,7 +616,7 @@ export const Chat = ({
557616 onBeforeMessageSend : validateAgents ,
558617 mainAgentTimer,
559618 scrollToLatest,
560- availableWidth : separatorWidth ,
619+ availableWidth : messageAvailableWidth ,
561620 onTimerEvent : ( ) => { } , // No-op for now
562621 setHasReceivedPlanResponse,
563622 lastMessageMode,
@@ -949,7 +1008,7 @@ export const Chat = ({
9491008 streamingAgents = { streamingAgents }
9501009 messageTree = { messageTree }
9511010 messages = { messages }
952- availableWidth = { separatorWidth }
1011+ availableWidth = { messageAvailableWidth }
9531012 setFocusedAgentId = { setFocusedAgentId }
9541013 isWaitingForResponse = { isWaitingForResponse }
9551014 timerStartTime = { timerStartTime }
0 commit comments