11import React , {
2- useCallback ,
32 useEffect ,
43 useRef ,
54 useMemo ,
65 lazy ,
76 Suspense ,
87} from 'react' ;
9- import { Box , Text } from 'ink' ;
8+ import { Box , Text , useCursor } from 'ink' ;
109import { Viewport } from '../../../utils/ui/textBuffer.js' ;
11- import { cpSlice , visualPosToCodePoint } from '../../../utils/core/textUtils.js' ;
1210
1311// Lazy load panel components to reduce initial bundle size
1412const CommandPanel = lazy ( ( ) => import ( '../panels/CommandPanel.js' ) ) ;
@@ -826,26 +824,8 @@ export default function ChatInput({
826824 isPureBashMode ,
827825 ] ) ;
828826
829- // Render cursor based on focus state
830- const renderCursor = useCallback (
831- ( char : string ) => {
832- if ( hasFocus ) {
833- // Focused: solid block cursor (use inverted colors)
834- return (
835- < Text
836- backgroundColor = { theme . colors . menuNormal }
837- color = { theme . colors . background }
838- >
839- { char }
840- </ Text >
841- ) ;
842- } else {
843- // Unfocused: no cursor, just render the character normally
844- return < Text > { char } </ Text > ;
845- }
846- } ,
847- [ hasFocus , theme ] ,
848- ) ;
827+ // Real terminal cursor via useCursor hook
828+ const { setCursorPosition, cursorRef} = useCursor ( ) ;
849829
850830 // Render content with cursor (treat all text including placeholders as plain text)
851831 const INPUT_MAX_LINES = 6 ;
@@ -884,6 +864,16 @@ export default function ChatInput({
884864 endLine = startLine + maxLines ;
885865 }
886866
867+ // Set real terminal cursor position
868+ const hasScrollUp = startLine > 0 ;
869+ const cursorYInContent =
870+ cursorRow - startLine + ( hasScrollUp ? 1 : 0 ) ;
871+ if ( hasFocus ) {
872+ setCursorPosition ( { x : cursorCol , y : cursorYInContent } ) ;
873+ } else {
874+ setCursorPosition ( undefined ) ;
875+ }
876+
887877 const renderedLines : React . ReactNode [ ] = [ ] ;
888878
889879 // Scroll-up indicator
@@ -899,17 +889,9 @@ export default function ChatInput({
899889 const line = visualLines [ i ] || '' ;
900890
901891 if ( i === cursorRow ) {
902- // This line contains the cursor
903- const cursorIndex = visualPosToCodePoint ( line , cursorCol ) ;
904- const beforeCursor = cpSlice ( line , 0 , cursorIndex ) ;
905- const atCursor = cpSlice ( line , cursorIndex , cursorIndex + 1 ) || ' ' ;
906- const afterCursor = cpSlice ( line , cursorIndex + 1 ) ;
907-
908892 renderedLines . push (
909893 < Box key = { i } flexDirection = "row" >
910- < Text > { beforeCursor } </ Text >
911- { renderCursor ( atCursor ) }
912- < Text > { afterCursor } </ Text >
894+ < Text > { line || ' ' } </ Text >
913895 { commandArgsHint && i === visualLines . length - 1 ? (
914896 < Text color = { theme . colors . menuSecondary } dimColor >
915897 { commandArgsHint }
@@ -918,7 +900,6 @@ export default function ChatInput({
918900 </ Box > ,
919901 ) ;
920902 } else {
921- // No cursor in this line
922903 renderedLines . push ( < Text key = { i } > { line || ' ' } </ Text > ) ;
923904 }
924905 }
@@ -937,13 +918,17 @@ export default function ChatInput({
937918
938919 return < Box flexDirection = "column" > { renderedLines } </ Box > ;
939920 } else {
921+ // Empty input: cursor at start
922+ if ( hasFocus ) {
923+ setCursorPosition ( { x : 0 , y : 0 } ) ;
924+ } else {
925+ setCursorPosition ( undefined ) ;
926+ }
927+
940928 return (
941- < >
942- { renderCursor ( ' ' ) }
943- < Text color = { theme . colors . menuSecondary } dimColor >
944- { disabled ? t . chatScreen . waitingForResponse : placeholder }
945- </ Text >
946- </ >
929+ < Text color = { theme . colors . menuSecondary } dimColor >
930+ { disabled ? t . chatScreen . waitingForResponse : placeholder }
931+ </ Text >
947932 ) ;
948933 }
949934 } ;
@@ -997,7 +982,9 @@ export default function ChatInput({
997982 ? '⤢'
998983 : '❯' } { ' ' }
999984 </ Text >
1000- < Box flexGrow = { 1 } > { renderContent ( ) } </ Box >
985+ < Box ref = { cursorRef } flexGrow = { 1 } >
986+ { renderContent ( ) }
987+ </ Box >
1001988 </ Box >
1002989 < Box flexDirection = "row" >
1003990 < Text
0 commit comments