Skip to content

Commit 280aa46

Browse files
authored
Merge pull request #51 from MyEcoria/dev/pbuchez/input-history
feat(tui): add command history navigation with up/down keys
2 parents b4fb095 + 6ad8740 commit 280aa46

File tree

1 file changed

+85
-4
lines changed

1 file changed

+85
-4
lines changed

shai-cli/src/tui/input.rs

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use shai_core::agent::{AgentController, AgentEvent, PublicAgentState};
1515
use shai_llm::{tool::call_fc_auto::ToolCallFunctionCallingAuto, ToolCallMethod};
1616
use tui_textarea::{Input, TextArea};
1717

18-
use crate::tui::{cmdnav::CommandNav, helper::HelpArea};
18+
use crate::{tui::{cmdnav::CommandNav, helper::HelpArea}};
1919

2020
use super::theme::SHAI_YELLOW;
2121

@@ -33,10 +33,13 @@ pub enum UserAction {
3333
pub 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

6066
impl 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
214230
impl 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

Comments
 (0)