1+ import type { MultilineTextEditorHandle } from "./multiline-editor" ;
12import type { ReviewDecision } from "../../utils/agent/review.js" ;
23import type { HistoryEntry } from "../../utils/storage/command-history.js" ;
34import type {
45 ResponseInputItem ,
56 ResponseItem ,
67} from "openai/resources/responses/responses.mjs" ;
78
9+ import MultilineTextEditor from "./multiline-editor" ;
810import { TerminalChatCommandReview } from "./terminal-chat-command-review.js" ;
9- import { log , isLoggingEnabled } from "../../utils/agent/log.js" ;
11+ import { log } from "../../utils/agent/log.js" ;
1012import { loadConfig } from "../../utils/config.js" ;
1113import { createInputItem } from "../../utils/input-utils.js" ;
1214import { setSessionId } from "../../utils/session.js" ;
@@ -16,10 +18,15 @@ import {
1618 addToHistory ,
1719} from "../../utils/storage/command-history.js" ;
1820import { clearTerminal , onExit } from "../../utils/terminal.js" ;
19- import TextInput from "../vendor/ink-text-input.js" ;
2021import { Box , Text , useApp , useInput , useStdin } from "ink" ;
2122import { fileURLToPath } from "node:url" ;
22- import React , { useCallback , useState , Fragment , useEffect } from "react" ;
23+ import React , {
24+ useCallback ,
25+ useState ,
26+ Fragment ,
27+ useEffect ,
28+ useRef ,
29+ } from "react" ;
2330import { useInterval } from "use-interval" ;
2431
2532const suggestions = [
@@ -83,6 +90,12 @@ export default function TerminalChatInput({
8390 const [ historyIndex , setHistoryIndex ] = useState < number | null > ( null ) ;
8491 const [ draftInput , setDraftInput ] = useState < string > ( "" ) ;
8592 const [ skipNextSubmit , setSkipNextSubmit ] = useState < boolean > ( false ) ;
93+ // Multiline text editor key to force remount after submission
94+ const [ editorKey , setEditorKey ] = useState ( 0 ) ;
95+ // Imperative handle from the multiline editor so we can query caret position
96+ const editorRef = useRef < MultilineTextEditorHandle | null > ( null ) ;
97+ // Track the caret row across keystrokes
98+ const prevCursorRow = useRef < number | null > ( null ) ;
8699
87100 // Load command history on component mount
88101 useEffect ( ( ) => {
@@ -168,6 +181,9 @@ export default function TerminalChatInput({
168181 case "/approval" :
169182 openApprovalOverlay ( ) ;
170183 break ;
184+ case "/diff" :
185+ openDiffOverlay ( ) ;
186+ break ;
171187 case "/bug" :
172188 onSubmit ( cmd ) ;
173189 break ;
@@ -181,9 +197,15 @@ export default function TerminalChatInput({
181197 }
182198 if ( ! confirmationPrompt && ! loading ) {
183199 if ( _key . upArrow ) {
184- if ( history . length > 0 ) {
200+ // Only recall history when the caret was *already* on the very first
201+ // row *before* this key-press.
202+ const cursorRow = editorRef . current ?. getRow ?.( ) ?? 0 ;
203+ const wasAtFirstRow = ( prevCursorRow . current ?? cursorRow ) === 0 ;
204+
205+ if ( history . length > 0 && cursorRow === 0 && wasAtFirstRow ) {
185206 if ( historyIndex == null ) {
186- setDraftInput ( input ) ;
207+ const currentDraft = editorRef . current ?. getText ?.( ) ?? input ;
208+ setDraftInput ( currentDraft ) ;
187209 }
188210
189211 let newIndex : number ;
@@ -194,27 +216,37 @@ export default function TerminalChatInput({
194216 }
195217 setHistoryIndex ( newIndex ) ;
196218 setInput ( history [ newIndex ] ?. command ?? "" ) ;
219+ // Re-mount the editor so it picks up the new initialText
220+ setEditorKey ( ( k ) => k + 1 ) ;
221+ return ; // we handled the key
197222 }
198- return ;
223+ // Otherwise let the event propagate so the editor moves the caret
199224 }
200225
201226 if ( _key . downArrow ) {
202- if ( historyIndex == null ) {
203- return ;
204- }
205-
206- const newIndex = historyIndex + 1 ;
207- if ( newIndex >= history . length ) {
208- setHistoryIndex ( null ) ;
209- setInput ( draftInput ) ;
210- } else {
211- setHistoryIndex ( newIndex ) ;
212- setInput ( history [ newIndex ] ?. command ?? "" ) ;
227+ // Only move forward in history when we're already *in* history mode
228+ // AND the caret sits on the last line of the buffer
229+ if ( historyIndex != null && editorRef . current ?. isCursorAtLastRow ( ) ) {
230+ const newIndex = historyIndex + 1 ;
231+ if ( newIndex >= history . length ) {
232+ setHistoryIndex ( null ) ;
233+ setInput ( draftInput ) ;
234+ setEditorKey ( ( k ) => k + 1 ) ;
235+ } else {
236+ setHistoryIndex ( newIndex ) ;
237+ setInput ( history [ newIndex ] ?. command ?? "" ) ;
238+ setEditorKey ( ( k ) => k + 1 ) ;
239+ }
240+ return ; // handled
213241 }
214- return ;
242+ // Otherwise let it propagate
215243 }
216244 }
217245
246+ // Update the cached cursor position *after* we've potentially handled
247+ // the key so that the next event has the correct "previous" reference.
248+ prevCursorRow . current = editorRef . current ?. getRow ?.( ) ?? null ;
249+
218250 if ( input . trim ( ) === "" && isNew ) {
219251 if ( _key . tab ) {
220252 setSelectedSuggestion (
@@ -313,15 +345,27 @@ export default function TerminalChatInput({
313345
314346 // Emit a system message to confirm the clear action. We *append*
315347 // it so Ink's <Static> treats it as new output and actually renders it.
316- setItems ( ( prev ) => [
317- ...prev ,
318- {
319- id : `clear-${ Date . now ( ) } ` ,
320- type : "message" ,
321- role : "system" ,
322- content : [ { type : "input_text" , text : "Context cleared" } ] ,
323- } ,
324- ] ) ;
348+ setItems ( ( prev ) => {
349+ const filteredOldItems = prev . filter ( ( item ) => {
350+ if (
351+ item . type === "message" &&
352+ ( item . role === "user" || item . role === "assistant" )
353+ ) {
354+ return false ;
355+ }
356+ return true ;
357+ } ) ;
358+
359+ return [
360+ ...filteredOldItems ,
361+ {
362+ id : `clear-${ Date . now ( ) } ` ,
363+ type : "message" ,
364+ role : "system" ,
365+ content : [ { type : "input_text" , text : "Terminal cleared" } ] ,
366+ } ,
367+ ] ;
368+ } ) ;
325369
326370 return ;
327371 } else if ( inputValue === "/clearhistory" ) {
@@ -534,25 +578,27 @@ export default function TerminalChatInput({
534578 thinkingSeconds = { thinkingSeconds }
535579 />
536580 ) : (
537- < Box paddingX = { 1 } >
538- < TextInput
539- focus = { active }
540- placeholder = {
541- selectedSuggestion
542- ? `"${ suggestions [ selectedSuggestion - 1 ] } "`
543- : "send a message" +
544- ( isNew ? " or press tab to select a suggestion" : "" )
545- }
546- showCursor
547- value = { input }
548- onChange = { ( value ) => {
549- setDraftInput ( value ) ;
581+ < Box >
582+ < MultilineTextEditor
583+ ref = { editorRef }
584+ onChange = { ( txt : string ) => {
585+ setDraftInput ( txt ) ;
550586 if ( historyIndex != null ) {
551587 setHistoryIndex ( null ) ;
552588 }
553- setInput ( value ) ;
589+ setInput ( txt ) ;
590+ } }
591+ key = { editorKey }
592+ initialText = { input }
593+ height = { 6 }
594+ focus = { active }
595+ onSubmit = { ( txt ) => {
596+ onSubmit ( txt ) ;
597+ setEditorKey ( ( k ) => k + 1 ) ;
598+ setInput ( "" ) ;
599+ setHistoryIndex ( null ) ;
600+ setDraftInput ( "" ) ;
554601 } }
555- onSubmit = { onSubmit }
556602 />
557603 </ Box >
558604 ) }
@@ -597,7 +643,7 @@ export default function TerminalChatInput({
597643 ) : (
598644 < >
599645 send q or ctrl+c to exit | send "/clear" to reset | send "/help"
600- for commands | press enter to send
646+ for commands | press enter to send | shift+enter for new line
601647 { contextLeftPercent > 25 && (
602648 < >
603649 { " — " }
@@ -692,11 +738,9 @@ function TerminalChatInputThinking({
692738 const str = Buffer . isBuffer ( data ) ? data . toString ( "utf8" ) : data ;
693739 if ( str === "\x1b\x1b" ) {
694740 // Treat as the first Escape press – prompt the user for confirmation.
695- if ( isLoggingEnabled ( ) ) {
696- log (
697- "raw stdin: received collapsed ESC ESC – starting confirmation timer" ,
698- ) ;
699- }
741+ log (
742+ "raw stdin: received collapsed ESC ESC – starting confirmation timer" ,
743+ ) ;
700744 setAwaitingConfirm ( true ) ;
701745 setTimeout ( ( ) => setAwaitingConfirm ( false ) , 1500 ) ;
702746 }
@@ -721,15 +765,11 @@ function TerminalChatInputThinking({
721765 }
722766
723767 if ( awaitingConfirm ) {
724- if ( isLoggingEnabled ( ) ) {
725- log ( "useInput: second ESC detected – triggering onInterrupt()" ) ;
726- }
768+ log ( "useInput: second ESC detected – triggering onInterrupt()" ) ;
727769 onInterrupt ( ) ;
728770 setAwaitingConfirm ( false ) ;
729771 } else {
730- if ( isLoggingEnabled ( ) ) {
731- log ( "useInput: first ESC detected – waiting for confirmation" ) ;
732- }
772+ log ( "useInput: first ESC detected – waiting for confirmation" ) ;
733773 setAwaitingConfirm ( true ) ;
734774 setTimeout ( ( ) => setAwaitingConfirm ( false ) , 1500 ) ;
735775 }
0 commit comments