Skip to content

Commit 2251090

Browse files
committed
feat: implement persistent CLI history with file storage
- Add Drop trait to InputSource for automatic history saving - Replace DefaultHistory with FileHistory for persistence - Store history in ~/.aws/amazonq/cli_history - Refactor ChatHinter to use rustyline's built-in history search - Remove manual history tracking in favor of rustyline's implementation - Add history loading on startup with error handling - Clean up unused hinter history update methods
1 parent a8a0426 commit 2251090

File tree

2 files changed

+75
-44
lines changed

2 files changed

+75
-44
lines changed

crates/chat-cli/src/cli/chat/input_source.rs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use super::prompt::{
88
};
99
#[cfg(unix)]
1010
use super::skim_integration::SkimCommandSelector;
11+
use crate::cli::chat::prompt::CLI_HISTORY_PATH;
1112
use crate::os::Os;
1213

1314
#[derive(Debug)]
@@ -31,11 +32,33 @@ mod inner {
3132
}
3233
}
3334

35+
impl Drop for InputSource {
36+
fn drop(&mut self) {
37+
self.save_history().unwrap();
38+
}
39+
}
3440
impl InputSource {
3541
pub fn new(os: &Os, sender: PromptQuerySender, receiver: PromptQueryResponseReceiver) -> Result<Self> {
3642
Ok(Self(inner::Inner::Readline(rl(os, sender, receiver)?)))
3743
}
3844

45+
/// Save history to file
46+
pub fn save_history(&mut self) -> Result<()> {
47+
if let inner::Inner::Readline(rl) = &mut self.0 {
48+
let history_path = dirs::home_dir()
49+
.ok_or_else(|| eyre::eyre!("Could not find home directory"))?
50+
.join(CLI_HISTORY_PATH);
51+
52+
// Create directory if it doesn't exist
53+
if let Some(parent) = history_path.parent() {
54+
std::fs::create_dir_all(parent)?;
55+
}
56+
57+
rl.append_history(&history_path)?;
58+
}
59+
Ok(())
60+
}
61+
3962
#[cfg(unix)]
4063
pub fn put_skim_command_selector(
4164
&mut self,
@@ -78,12 +101,9 @@ impl InputSource {
78101
let curr_line = rl.readline(prompt);
79102
match curr_line {
80103
Ok(line) => {
81-
let _ = rl.add_history_entry(line.as_str());
82-
83-
if let Some(helper) = rl.helper_mut() {
84-
helper.update_hinter_history(&line);
104+
if Self::should_append_history(&line) {
105+
let _ = rl.add_history_entry(line.as_str());
85106
}
86-
87107
Ok(Some(line))
88108
},
89109
Err(ReadlineError::Interrupted | ReadlineError::Eof) => Ok(None),
@@ -97,6 +117,18 @@ impl InputSource {
97117
}
98118
}
99119

120+
fn should_append_history(line: &str) -> bool {
121+
let trimmed = line.trim().to_lowercase();
122+
if trimmed.is_empty() {
123+
return false;
124+
}
125+
126+
if matches!(trimmed.as_str(), "y" | "n" | "t") {
127+
return false;
128+
}
129+
true
130+
}
131+
100132
// We're keeping this method for potential future use
101133
#[allow(dead_code)]
102134
pub fn set_buffer(&mut self, content: &str) {

crates/chat-cli/src/cli/chat/prompt.rs

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ use rustyline::highlight::{
1313
Highlighter,
1414
};
1515
use rustyline::hint::Hinter as RustylineHinter;
16-
use rustyline::history::DefaultHistory;
16+
use rustyline::history::{
17+
FileHistory,
18+
SearchDirection,
19+
};
1720
use rustyline::validate::{
1821
ValidationContext,
1922
ValidationResult,
@@ -45,6 +48,8 @@ use super::tool_manager::{
4548
use crate::database::settings::Setting;
4649
use crate::os::Os;
4750

51+
pub const CLI_HISTORY_PATH: &str = ".aws/amazonq/.cli_bash_history";
52+
4853
pub const COMMANDS: &[&str] = &[
4954
"/clear",
5055
"/help",
@@ -262,31 +267,18 @@ impl Completer for ChatCompleter {
262267

263268
/// Custom hinter that provides shadowtext suggestions
264269
pub struct ChatHinter {
265-
/// Command history for providing suggestions based on past commands
266-
history: Vec<String>,
267270
/// Whether history-based hints are enabled
268271
history_hints_enabled: bool,
269272
}
270273

271274
impl ChatHinter {
272275
/// Creates a new ChatHinter instance
273276
pub fn new(history_hints_enabled: bool) -> Self {
274-
Self {
275-
history: Vec::new(),
276-
history_hints_enabled,
277-
}
277+
Self { history_hints_enabled }
278278
}
279279

280-
/// Updates the history with a new command
281-
pub fn update_history(&mut self, command: &str) {
282-
let command = command.trim();
283-
if !command.is_empty() && !command.contains('\n') && !command.contains('\r') {
284-
self.history.push(command.to_string());
285-
}
286-
}
287-
288-
/// Finds the best hint for the current input
289-
fn find_hint(&self, line: &str) -> Option<String> {
280+
/// Finds the best hint for the current input using rustyline's history
281+
fn find_hint(&self, line: &str, ctx: &Context<'_>) -> Option<String> {
290282
// If line is empty, no hint
291283
if line.is_empty() {
292284
return None;
@@ -300,13 +292,20 @@ impl ChatHinter {
300292
.map(|cmd| cmd[line.len()..].to_string());
301293
}
302294

303-
// Try to find a hint from history if history hints are enabled
295+
// Try to find a hint from rustyline's history if history hints are enabled
304296
if self.history_hints_enabled {
305-
return self.history
306-
.iter()
307-
.rev() // Start from most recent
308-
.find(|cmd| cmd.starts_with(line) && cmd.len() > line.len())
309-
.map(|cmd| cmd[line.len()..].to_string());
297+
let history = ctx.history();
298+
let history_len = history.len();
299+
if history_len == 0 {
300+
return None;
301+
}
302+
303+
if let Ok(Some(search_result)) = history.starts_with(line, history_len - 1, SearchDirection::Reverse) {
304+
let entry = search_result.entry.to_string();
305+
if entry.len() > line.len() {
306+
return Some(entry[line.len()..].to_string());
307+
}
308+
}
310309
}
311310

312311
None
@@ -316,13 +315,13 @@ impl ChatHinter {
316315
impl RustylineHinter for ChatHinter {
317316
type Hint = String;
318317

319-
fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option<Self::Hint> {
318+
fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<Self::Hint> {
320319
// Only provide hints when cursor is at the end of the line
321320
if pos < line.len() {
322321
return None;
323322
}
324323

325-
self.find_hint(line)
324+
self.find_hint(line, ctx)
326325
}
327326
}
328327

@@ -362,13 +361,6 @@ pub struct ChatHelper {
362361
validator: MultiLineValidator,
363362
}
364363

365-
impl ChatHelper {
366-
/// Updates the history of the ChatHinter with a new command
367-
pub fn update_hinter_history(&mut self, command: &str) {
368-
self.hinter.update_history(command);
369-
}
370-
}
371-
372364
impl Validator for ChatHelper {
373365
fn validate(&self, os: &mut ValidationContext<'_>) -> rustyline::Result<ValidationResult> {
374366
self.validator.validate(os)
@@ -426,7 +418,7 @@ pub fn rl(
426418
os: &Os,
427419
sender: PromptQuerySender,
428420
receiver: PromptQueryResponseReceiver,
429-
) -> Result<Editor<ChatHelper, DefaultHistory>> {
421+
) -> Result<Editor<ChatHelper, FileHistory>> {
430422
let edit_mode = match os.database.settings.get_string(Setting::ChatEditMode).as_deref() {
431423
Some("vi" | "vim") => EditMode::Vi,
432424
_ => EditMode::Emacs,
@@ -437,7 +429,6 @@ pub fn rl(
437429
.edit_mode(edit_mode)
438430
.build();
439431

440-
// Default to disabled if setting doesn't exist
441432
let history_hints_enabled = os
442433
.database
443434
.settings
@@ -452,6 +443,17 @@ pub fn rl(
452443
let mut rl = Editor::with_config(config)?;
453444
rl.set_helper(Some(h));
454445

446+
// Load history from ~/.aws/amazonq/cli_history
447+
let history_path = dirs::home_dir()
448+
.ok_or_else(|| eyre::eyre!("Could not find home directory"))?
449+
.join(CLI_HISTORY_PATH);
450+
451+
if let Err(e) = rl.load_history(&history_path) {
452+
if !matches!(e, ReadlineError::Io(ref io_err) if io_err.kind() == std::io::ErrorKind::NotFound) {
453+
eprintln!("Warning: Failed to load history: {}", e);
454+
}
455+
}
456+
455457
// Add custom keybinding for Alt+Enter to insert a newline
456458
rl.bind_sequence(
457459
KeyEvent(KeyCode::Enter, Modifiers::ALT),
@@ -487,6 +489,7 @@ pub fn rl(
487489
mod tests {
488490
use crossterm::style::Stylize;
489491
use rustyline::highlight::Highlighter;
492+
use rustyline::history::DefaultHistory;
490493

491494
use super::*;
492495

@@ -694,11 +697,7 @@ mod tests {
694697

695698
#[test]
696699
fn test_chat_hinter_history_hint_disabled() {
697-
let mut hinter = ChatHinter::new(false);
698-
699-
// Add some history
700-
hinter.update_history("Hello, world!");
701-
hinter.update_history("How are you?");
700+
let hinter = ChatHinter::new(false);
702701

703702
// Test hint from history - should be None since history hints are disabled
704703
let line = "How";

0 commit comments

Comments
 (0)