@@ -39,6 +39,7 @@ import { loadLocalAgents } from './utils/local-agent-registry'
3939import { buildMessageTree } from './utils/message-tree-utils'
4040import { createMarkdownPalette } from './utils/theme-system'
4141import { BORDER_CHARS } from './utils/ui-constants'
42+ import { computeInputLayoutMetrics } from './utils/text-layout'
4243
4344import type { SendMessageTimerEvent } from './hooks/use-send-message'
4445import type { ContentBlock } from './types/chat'
@@ -535,6 +536,36 @@ export const Chat = ({
535536 ) : null
536537
537538 const shouldShowQueuePreview = queuedMessages . length > 0
539+ const queuePreviewTitle = useMemo ( ( ) => {
540+ if ( ! shouldShowQueuePreview ) return undefined
541+ const previewWidth = Math . max ( 30 , separatorWidth - 20 )
542+ return formatQueuedPreview ( queuedMessages , previewWidth )
543+ } , [ queuedMessages , separatorWidth , shouldShowQueuePreview ] )
544+ const hasSlashSuggestions = slashContext . active && slashSuggestionItems . length > 0
545+ const hasMentionSuggestions =
546+ ! slashContext . active && mentionContext . active && agentSuggestionItems . length > 0
547+ const hasSuggestionMenu = hasSlashSuggestions || hasMentionSuggestions
548+ const showAgentStatusLine = showAgentDisplayName && loadedAgentsData
549+
550+ const inputLayoutMetrics = useMemo ( ( ) => {
551+ const text = inputValue ?? ''
552+ const layoutContent = text . length > 0 ? text : ' '
553+ const safeCursor = Math . max ( 0 , Math . min ( cursorPosition , layoutContent . length ) )
554+ const cursorProbe =
555+ safeCursor >= layoutContent . length
556+ ? layoutContent
557+ : layoutContent . slice ( 0 , safeCursor )
558+ const cols = Math . max ( 1 , inputWidth - 4 )
559+ return computeInputLayoutMetrics ( {
560+ layoutContent,
561+ cursorProbe,
562+ cols,
563+ maxHeight : 5 ,
564+ } )
565+ } , [ inputValue , cursorPosition , inputWidth ] )
566+ const isMultilineInput = inputLayoutMetrics . heightLines > 1
567+ const shouldCenterInputVertically =
568+ ! hasSuggestionMenu && ! showAgentStatusLine && ! isMultilineInput
538569 const shouldShowStatusLine =
539570 streamStatus !== 'idle' ||
540571 shouldShowQueuePreview ||
@@ -711,46 +742,38 @@ export const Chat = ({
711742 </ box >
712743 </ box >
713744
714- { /* Queue preview line - separate row */ }
715- { shouldShowQueuePreview && (
716- < box
717- style = { {
718- flexDirection : 'row' ,
719- width : '100%' ,
720- justifyContent : 'center' ,
721- } }
722- >
723- < text style = { { wrapMode : 'none' } } >
724- < span fg = { theme . secondary } bg = { theme . inputFocusedBg } >
725- { ` ${ formatQueuedPreview (
726- queuedMessages ,
727- Math . max ( 30 , terminalWidth - 10 ) ,
728- ) } `}
729- </ span >
730- </ text >
731- </ box >
732- ) }
733745 </ box >
734746 ) }
747+
748+ { /* Wrap the input row in a single OpenTUI border so the toggle stays inside the flex layout.
749+ The queue preview is injected via the border title rather than custom text nodes, which
750+ keeps the border coupled to the content height while preserving the inline preview look. */ }
735751 < box
752+ title = { queuePreviewTitle ? ` ${ queuePreviewTitle } ` : undefined }
753+ titleAlignment = "center"
736754 style = { {
737755 width : '100%' ,
738756 borderStyle : 'single' ,
739757 borderColor : theme . secondary ,
758+ focusedBorderColor : theme . foreground ,
740759 customBorderChars : BORDER_CHARS ,
760+ paddingLeft : 1 ,
761+ paddingRight : 1 ,
762+ paddingTop : 0 ,
763+ paddingBottom : 0 ,
764+ flexDirection : 'column' ,
765+ gap : hasSuggestionMenu ? 1 : 0 ,
741766 } }
742767 >
743- { slashContext . active && slashSuggestionItems . length > 0 ? (
768+ { hasSlashSuggestions ? (
744769 < SuggestionMenu
745770 items = { slashSuggestionItems }
746771 selectedIndex = { slashSelectedIndex }
747772 maxVisible = { 10 }
748773 prefix = "/"
749774 />
750775 ) : null }
751- { ! slashContext . active &&
752- mentionContext . active &&
753- agentSuggestionItems . length > 0 ? (
776+ { hasMentionSuggestions ? (
754777 < SuggestionMenu
755778 items = { agentSuggestionItems }
756779 selectedIndex = { agentSelectedIndex }
@@ -760,55 +783,65 @@ export const Chat = ({
760783 ) : null }
761784 < box
762785 style = { {
763- flexDirection : 'row' ,
764- alignItems : 'center' ,
765- width : '100%' ,
786+ flexDirection : 'column' ,
787+ justifyContent : shouldCenterInputVertically
788+ ? 'center'
789+ : 'flex-start' ,
790+ minHeight : shouldCenterInputVertically ? 3 : undefined ,
791+ gap : showAgentStatusLine ? 1 : 0 ,
766792 } }
767793 >
768- < box style = { { flexGrow : 1 , minWidth : 0 } } >
769- < MultilineInput
770- value = { inputValue }
771- onChange = { setInputValue }
772- onSubmit = { handleSubmit }
773- placeholder = "Enter a coding task or / for commands"
774- focused = { inputFocused }
775- maxHeight = { 5 }
776- width = { inputWidth }
777- onKeyIntercept = { handleSuggestionMenuKey }
778- textAttributes = { theme . messageTextAttributes }
779- ref = { inputRef }
780- cursorPosition = { cursorPosition }
781- />
782- </ box >
783794 < box
784795 style = { {
785- flexShrink : 0 ,
786- paddingLeft : 2 ,
796+ flexDirection : 'row' ,
797+ alignItems : shouldCenterInputVertically ? 'center' : 'flex-start' ,
798+ width : '100%' ,
787799 } }
788800 >
789- < AgentModeToggle
790- mode = { agentMode }
791- onToggle = { toggleAgentMode }
792- onSelectMode = { setAgentMode }
793- />
801+ < box style = { { flexGrow : 1 , minWidth : 0 } } >
802+ < MultilineInput
803+ value = { inputValue }
804+ onChange = { setInputValue }
805+ onSubmit = { handleSubmit }
806+ placeholder = "Enter a coding task or / for commands"
807+ focused = { inputFocused }
808+ maxHeight = { 5 }
809+ width = { inputWidth }
810+ onKeyIntercept = { handleSuggestionMenuKey }
811+ textAttributes = { theme . messageTextAttributes }
812+ ref = { inputRef }
813+ cursorPosition = { cursorPosition }
814+ />
815+ </ box >
816+ < box
817+ style = { {
818+ flexShrink : 0 ,
819+ paddingLeft : 2 ,
820+ } }
821+ >
822+ < AgentModeToggle
823+ mode = { agentMode }
824+ onToggle = { toggleAgentMode }
825+ onSelectMode = { setAgentMode }
826+ />
827+ </ box >
794828 </ box >
829+ { /* Agent status line - right-aligned under toggle */ }
830+ { showAgentStatusLine && (
831+ < box
832+ style = { {
833+ flexDirection : 'row' ,
834+ justifyContent : 'flex-end' ,
835+ paddingTop : 0 ,
836+ } }
837+ >
838+ < text >
839+ < span fg = { theme . muted } > Agent: { agentDisplayName } </ span >
840+ </ text >
841+ </ box >
842+ ) }
795843 </ box >
796844 </ box >
797- { /* Agent status line - right-aligned under toggle */ }
798- { showAgentDisplayName && loadedAgentsData && (
799- < box
800- style = { {
801- flexDirection : 'row' ,
802- justifyContent : 'flex-end' ,
803- paddingRight : 1 ,
804- paddingTop : 0 ,
805- } }
806- >
807- < text >
808- < span fg = { theme . muted } > Agent: { agentDisplayName } </ span >
809- </ text >
810- </ box >
811- ) }
812845 </ box >
813846
814847 { /* Login Modal Overlay - show when not authenticated and done checking */ }
0 commit comments