@@ -9,19 +9,12 @@ interface UsePromptHistoryProps {
99 setInputValue : ( value : string ) => void
1010}
1111
12- interface CursorPositionState {
13- value : string
14- afterRender ?: "SET_CURSOR_FIRST_LINE" | "SET_CURSOR_LAST_LINE" | "SET_CURSOR_START"
15- }
16-
1712export interface UsePromptHistoryReturn {
1813 historyIndex : number
1914 setHistoryIndex : ( index : number ) => void
2015 tempInput : string
2116 setTempInput : ( input : string ) => void
2217 promptHistory : string [ ]
23- inputValueWithCursor : CursorPositionState
24- setInputValueWithCursor : ( state : CursorPositionState ) => void
2518 handleHistoryNavigation : (
2619 event : React . KeyboardEvent < HTMLTextAreaElement > ,
2720 showContextMenu : boolean ,
@@ -45,49 +38,35 @@ export const usePromptHistory = ({
4538 const [ historyIndex , setHistoryIndex ] = useState ( - 1 )
4639 const [ tempInput , setTempInput ] = useState ( "" )
4740 const [ promptHistory , setPromptHistory ] = useState < string [ ] > ( [ ] )
48- const [ inputValueWithCursor , setInputValueWithCursor ] = useState < CursorPositionState > ( { value : inputValue } )
4941
5042 // Initialize prompt history with hybrid approach: conversation messages if in task, otherwise task history
5143 const filteredPromptHistory = useMemo ( ( ) => {
5244 // First try to get conversation messages (user_feedback from clineMessages)
5345 const conversationPrompts = clineMessages
54- ?. filter ( ( message ) => {
55- // Filter for user_feedback messages that have text content
56- return (
57- message . type === "say" &&
58- message . say === "user_feedback" &&
59- message . text &&
60- message . text . trim ( ) !== ""
61- )
62- } )
46+ ?. filter ( ( message ) => message . type === "say" && message . say === "user_feedback" && message . text ?. trim ( ) )
6347 . map ( ( message ) => message . text ! )
6448
6549 // If we have conversation messages, use those (newest first when navigating up)
66- if ( conversationPrompts && conversationPrompts . length > 0 ) {
67- return conversationPrompts . slice ( - MAX_PROMPT_HISTORY_SIZE ) . reverse ( ) // newest first for conversation messages
50+ if ( conversationPrompts ? .length ) {
51+ return conversationPrompts . slice ( - MAX_PROMPT_HISTORY_SIZE ) . reverse ( )
6852 }
6953
7054 // If we have clineMessages array (meaning we're in an active task), don't fall back to task history
7155 // Only use task history when starting fresh (no active conversation)
72- if ( clineMessages && clineMessages . length > 0 ) {
56+ if ( clineMessages ? .length ) {
7357 return [ ]
7458 }
7559
7660 // Fall back to task history only when starting fresh (no active conversation)
77- if ( ! taskHistory || taskHistory . length === 0 || ! cwd ) {
61+ if ( ! taskHistory ? .length || ! cwd ) {
7862 return [ ]
7963 }
8064
8165 // Extract user prompts from task history for the current workspace only
82- const taskPrompts = taskHistory
83- . filter ( ( item ) => {
84- // Filter by workspace and ensure task is not empty
85- return item . task && item . task . trim ( ) !== "" && ( ! item . workspace || item . workspace === cwd )
86- } )
66+ return taskHistory
67+ . filter ( ( item ) => item . task ?. trim ( ) && ( ! item . workspace || item . workspace === cwd ) )
8768 . map ( ( item ) => item . task )
8869 . slice ( 0 , MAX_PROMPT_HISTORY_SIZE )
89-
90- return taskPrompts
9170 } , [ clineMessages , taskHistory , cwd ] )
9271
9372 // Update prompt history when filtered history changes and reset navigation
@@ -106,76 +85,113 @@ export const usePromptHistory = ({
10685 }
10786 } , [ historyIndex ] )
10887
88+ // Helper to set cursor position after React renders
89+ const setCursorPosition = useCallback (
90+ ( textarea : HTMLTextAreaElement , position : number | "start" | "end" , length ?: number ) => {
91+ setTimeout ( ( ) => {
92+ if ( position === "start" ) {
93+ textarea . setSelectionRange ( 0 , 0 )
94+ } else if ( position === "end" ) {
95+ const len = length ?? textarea . value . length
96+ textarea . setSelectionRange ( len , len )
97+ } else {
98+ textarea . setSelectionRange ( position , position )
99+ }
100+ } , 0 )
101+ } ,
102+ [ ] ,
103+ )
104+
105+ // Helper to navigate to a specific history entry
106+ const navigateToHistory = useCallback (
107+ ( newIndex : number , textarea : HTMLTextAreaElement , cursorPos : "start" | "end" = "start" ) : boolean => {
108+ if ( newIndex < 0 || newIndex >= promptHistory . length ) return false
109+
110+ const historicalPrompt = promptHistory [ newIndex ]
111+ if ( ! historicalPrompt ) return false
112+
113+ setHistoryIndex ( newIndex )
114+ setInputValue ( historicalPrompt )
115+ setCursorPosition ( textarea , cursorPos , historicalPrompt . length )
116+
117+ return true
118+ } ,
119+ [ promptHistory , setInputValue , setCursorPosition ] ,
120+ )
121+
122+ // Helper to return to current input
123+ const returnToCurrentInput = useCallback (
124+ ( textarea : HTMLTextAreaElement , cursorPos : "start" | "end" = "end" ) => {
125+ setHistoryIndex ( - 1 )
126+ setInputValue ( tempInput )
127+ setCursorPosition ( textarea , cursorPos , tempInput . length )
128+ } ,
129+ [ tempInput , setInputValue , setCursorPosition ] ,
130+ )
131+
109132 const handleHistoryNavigation = useCallback (
110133 ( event : React . KeyboardEvent < HTMLTextAreaElement > , showContextMenu : boolean , isComposing : boolean ) : boolean => {
111134 // Handle prompt history navigation
112135 if ( ! showContextMenu && promptHistory . length > 0 && ! isComposing ) {
113136 const textarea = event . currentTarget
114137 const { selectionStart, selectionEnd, value } = textarea
115- const lines = value . substring ( 0 , selectionStart ) . split ( "\n" )
116- const currentLineIndex = lines . length - 1
117- const totalLines = value . split ( "\n" ) . length
118- const isAtFirstLine = currentLineIndex === 0
119- const isAtLastLine = currentLineIndex === totalLines - 1
120138 const hasSelection = selectionStart !== selectionEnd
139+ const isAtBeginning = selectionStart === 0 && selectionEnd === 0
140+ const isAtEnd = selectionStart === value . length && selectionEnd === value . length
121141
122- // Only navigate history if cursor is at first/last line and no text is selected
123- if ( ! hasSelection ) {
124- if ( event . key === "ArrowUp" && isAtFirstLine ) {
125- event . preventDefault ( )
142+ // Check for modifier keys (Alt or Cmd/Ctrl)
143+ const hasModifier = event . altKey || event . metaKey || event . ctrlKey
126144
145+ // Handle explicit history navigation with Alt+Up/Down
146+ if ( hasModifier && ( event . key === "ArrowUp" || event . key === "ArrowDown" ) ) {
147+ event . preventDefault ( )
148+
149+ if ( event . key === "ArrowUp" ) {
127150 // Save current input if starting navigation
128- if ( historyIndex === - 1 && inputValue . trim ( ) !== "" ) {
151+ if ( historyIndex === - 1 ) {
129152 setTempInput ( inputValue )
130153 }
154+ return navigateToHistory ( historyIndex + 1 , textarea , "start" )
155+ } else {
156+ // ArrowDown
157+ if ( historyIndex > 0 ) {
158+ return navigateToHistory ( historyIndex - 1 , textarea , "end" )
159+ } else if ( historyIndex === 0 ) {
160+ returnToCurrentInput ( textarea , "end" )
161+ return true
162+ }
163+ }
164+ }
131165
132- // Navigate to previous prompt
133- const newIndex = historyIndex + 1
134- if ( newIndex < promptHistory . length ) {
135- setHistoryIndex ( newIndex )
136- const historicalPrompt = promptHistory [ newIndex ]
137- if ( historicalPrompt ) {
138- setInputValue ( historicalPrompt )
139- setInputValueWithCursor ( {
140- value : historicalPrompt ,
141- afterRender : "SET_CURSOR_FIRST_LINE" ,
142- } )
143- }
166+ // Handle smart navigation without modifiers
167+ if ( ! hasSelection && ! hasModifier ) {
168+ // Only navigate history with UP if cursor is at the very beginning
169+ if ( event . key === "ArrowUp" && isAtBeginning ) {
170+ event . preventDefault ( )
171+ // Save current input if starting navigation
172+ if ( historyIndex === - 1 ) {
173+ setTempInput ( inputValue )
144174 }
145- return true
175+ return navigateToHistory ( historyIndex + 1 , textarea , "start" )
146176 }
147177
148- if ( event . key === "ArrowDown" && isAtLastLine ) {
178+ // Handle DOWN arrow - only in history navigation mode
179+ if ( event . key === "ArrowDown" && historyIndex >= 0 && ( isAtBeginning || isAtEnd ) ) {
149180 event . preventDefault ( )
150181
151- // Navigate to next prompt
152182 if ( historyIndex > 0 ) {
153- const newIndex = historyIndex - 1
154- setHistoryIndex ( newIndex )
155- const historicalPrompt = promptHistory [ newIndex ]
156- if ( historicalPrompt ) {
157- setInputValue ( historicalPrompt )
158- setInputValueWithCursor ( {
159- value : historicalPrompt ,
160- afterRender : "SET_CURSOR_LAST_LINE" ,
161- } )
162- }
183+ // Keep cursor position consistent with where we started
184+ return navigateToHistory ( historyIndex - 1 , textarea , isAtBeginning ? "start" : "end" )
163185 } else if ( historyIndex === 0 ) {
164- // Return to current input
165- setHistoryIndex ( - 1 )
166- setInputValue ( tempInput )
167- setInputValueWithCursor ( {
168- value : tempInput ,
169- afterRender : "SET_CURSOR_START" ,
170- } )
186+ returnToCurrentInput ( textarea , isAtBeginning ? "start" : "end" )
187+ return true
171188 }
172- return true
173189 }
174190 }
175191 }
176192 return false
177193 } ,
178- [ promptHistory , historyIndex , inputValue , tempInput , setInputValue ] ,
194+ [ promptHistory , historyIndex , inputValue , navigateToHistory , returnToCurrentInput ] ,
179195 )
180196
181197 const resetHistoryNavigation = useCallback ( ( ) => {
@@ -189,8 +205,6 @@ export const usePromptHistory = ({
189205 tempInput,
190206 setTempInput,
191207 promptHistory,
192- inputValueWithCursor,
193- setInputValueWithCursor,
194208 handleHistoryNavigation,
195209 resetHistoryNavigation,
196210 resetOnInputChange,
0 commit comments