@@ -15,7 +15,7 @@ use shai_core::agent::{AgentController, AgentEvent, PublicAgentState};
1515use shai_llm:: { tool:: call_fc_auto:: ToolCallFunctionCallingAuto , ToolCallMethod } ;
1616use tui_textarea:: { Input , TextArea } ;
1717
18- use crate :: tui:: { cmdnav:: CommandNav , helper:: HelpArea } ;
18+ use crate :: { tui:: { cmdnav:: CommandNav , helper:: HelpArea } } ;
1919
2020use super :: theme:: SHAI_YELLOW ;
2121
@@ -33,10 +33,13 @@ pub enum UserAction {
3333pub struct InputArea < ' a > {
3434 agent_running : bool ,
3535
36- // input text
36+ // input text
3737 input : TextArea < ' a > ,
3838 placeholder : String ,
3939
40+ // draft saving for history navigation
41+ current_draft : Option < String > ,
42+
4043 // alert top left
4144 animation_start : Option < Instant > ,
4245 status_message : Option < String > ,
@@ -54,7 +57,10 @@ pub struct InputArea<'a> {
5457
5558 // bottom helper
5659 help : Option < HelpArea > ,
57- cmdnav : CommandNav
60+ cmdnav : CommandNav ,
61+
62+ history : Vec < String > ,
63+ history_index : usize ,
5864}
5965
6066impl Default for InputArea < ' _ > {
@@ -63,6 +69,7 @@ impl Default for InputArea<'_> {
6369 agent_running : false ,
6470 input : TextArea :: default ( ) ,
6571 placeholder : "? for shortcuts" . to_string ( ) ,
72+ current_draft : None ,
6673 animation_start : None ,
6774 status_message : None ,
6875 last_keystroke_time : None ,
@@ -73,7 +80,9 @@ impl Default for InputArea<'_> {
7380 escape_press_time : None ,
7481 method : ToolCallMethod :: FunctionCall ,
7582 help : None ,
76- cmdnav : CommandNav { }
83+ cmdnav : CommandNav { } ,
84+ history : Vec :: new ( ) ,
85+ history_index : 0 ,
7786 }
7887 }
7988}
@@ -82,6 +91,11 @@ impl InputArea<'_> {
8291 pub fn new ( ) -> Self {
8392 Self :: default ( )
8493 }
94+
95+ pub fn set_history ( & mut self , history : Vec < String > ) {
96+ self . history = history;
97+ self . history_index = self . history . len ( ) ;
98+ }
8599}
86100
87101
@@ -175,6 +189,8 @@ impl InputArea<'_> {
175189 let lines = self . input . lines ( ) ;
176190 if !lines[ 0 ] . is_empty ( ) {
177191 let input = lines. join ( "\n " ) ;
192+ self . history . push ( input. clone ( ) ) ;
193+ self . history_index = self . history . len ( ) ;
178194
179195 // Handle app commands vs agent input
180196 self . input = TextArea :: default ( ) ;
@@ -212,6 +228,24 @@ impl InputArea<'_> {
212228
213229/// event related
214230impl InputArea < ' _ > {
231+ fn move_cursor_to_end_of_text ( & mut self ) {
232+ for _ in 0 ..self . input . lines ( ) . len ( ) . saturating_sub ( 1 ) {
233+ self . input . move_cursor ( tui_textarea:: CursorMove :: Down ) ;
234+ }
235+ if let Some ( last_line) = self . input . lines ( ) . last ( ) {
236+ for _ in 0 ..last_line. len ( ) {
237+ self . input . move_cursor ( tui_textarea:: CursorMove :: Forward ) ;
238+ }
239+ }
240+ }
241+
242+ fn load_historic_prompt ( & mut self , index : usize ) {
243+ if let Some ( entry) = self . history . get ( index) {
244+ self . input = TextArea :: new ( entry. lines ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ) ;
245+ self . move_cursor_to_end_of_text ( ) ;
246+ }
247+ }
248+
215249 pub async fn handle_event ( & mut self , key_event : KeyEvent ) -> UserAction {
216250 let now = Instant :: now ( ) ;
217251 self . last_keystroke_time = Some ( now) ;
@@ -291,6 +325,53 @@ impl InputArea<'_> {
291325 self . pending_enter = Some ( now) ;
292326 return UserAction :: Nope ;
293327 }
328+ KeyCode :: Up => {
329+ // Get current cursor position
330+ let ( cursor_row, _) = self . input . cursor ( ) ;
331+ let is_empty = self . input . lines ( ) . iter ( ) . all ( |line| line. is_empty ( ) ) ;
332+
333+ // Navigate history only if:
334+ // 1. Input is empty, OR
335+ // 2. Cursor is at the first line
336+ if !self . history . is_empty ( ) && self . history_index > 0 && ( is_empty || cursor_row == 0 ) {
337+ if self . history_index == self . history . len ( ) && !is_empty {
338+ let current_text = self . input . lines ( ) . join ( "\n " ) ;
339+ self . current_draft = Some ( current_text) ;
340+ }
341+
342+ self . history_index -= 1 ;
343+ self . load_historic_prompt ( self . history_index ) ;
344+ } else if !is_empty && cursor_row > 0 {
345+ self . input . move_cursor ( tui_textarea:: CursorMove :: Up ) ;
346+ }
347+ }
348+ KeyCode :: Down => {
349+ // Get current cursor position
350+ let ( cursor_row, _) = self . input . cursor ( ) ;
351+ let is_empty = self . input . lines ( ) . iter ( ) . all ( |line| line. is_empty ( ) ) ;
352+ let line_count = self . input . lines ( ) . len ( ) ;
353+
354+ // Navigate history only if:
355+ // 1. Cursor is at the last line
356+ if !self . history . is_empty ( ) && ( is_empty || cursor_row == line_count - 1 ) {
357+ if self . history_index < self . history . len ( ) {
358+ self . history_index += 1 ;
359+ if self . history_index < self . history . len ( ) {
360+ self . load_historic_prompt ( self . history_index ) ;
361+ } else {
362+ // Restore draft or create empty input
363+ if let Some ( draft) = self . current_draft . take ( ) {
364+ self . input = TextArea :: new ( draft. lines ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ) ;
365+ self . move_cursor_to_end_of_text ( ) ;
366+ } else {
367+ self . input = TextArea :: default ( ) ;
368+ }
369+ }
370+ }
371+ } else if !is_empty && cursor_row < line_count - 1 {
372+ self . input . move_cursor ( tui_textarea:: CursorMove :: Down ) ;
373+ }
374+ }
294375 _ => {
295376 // Convert to ratatui event format for tui-textarea
296377 self . help = None ;
0 commit comments