chore: refactor interactive session to reduce line count (#6274)

This commit is contained in:
Bradley Axen
2026-01-05 20:42:38 -08:00
committed by GitHub
parent 41dbddade6
commit d7a7b21487
+189 -141
View File
@@ -105,6 +105,53 @@ pub enum RunMode {
Plan, Plan,
} }
struct HistoryManager {
history_file: PathBuf,
old_history_file: PathBuf,
}
impl HistoryManager {
fn new() -> Self {
Self {
history_file: Paths::state_dir().join("history.txt"),
old_history_file: Paths::config_dir().join("history.txt"),
}
}
fn load(
&self,
editor: &mut rustyline::Editor<GooseCompleter, rustyline::history::DefaultHistory>,
) {
if let Some(parent) = self.history_file.parent() {
if !parent.exists() {
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!("Warning: Failed to create history directory: {}", e);
}
}
}
let history_files = [&self.history_file, &self.old_history_file];
if let Some(file) = history_files.iter().find(|f| f.exists()) {
if let Err(err) = editor.load_history(file) {
eprintln!("Warning: Failed to load command history: {}", err);
}
}
}
fn save(
&self,
editor: &mut rustyline::Editor<GooseCompleter, rustyline::history::DefaultHistory>,
) {
if let Err(err) = editor.save_history(&self.history_file) {
eprintln!("Warning: Failed to save command history: {}", err);
} else if self.old_history_file.exists() {
if let Err(err) = std::fs::remove_file(&self.old_history_file) {
eprintln!("Warning: Failed to remove old history file: {}", err);
}
}
}
}
pub struct CliSession { pub struct CliSession {
agent: Agent, agent: Agent,
messages: Conversation, messages: Conversation,
@@ -380,83 +427,148 @@ impl CliSession {
/// Start an interactive session, optionally with an initial message /// Start an interactive session, optionally with an initial message
pub async fn interactive(&mut self, prompt: Option<String>) -> Result<()> { pub async fn interactive(&mut self, prompt: Option<String>) -> Result<()> {
// Process initial message if provided
if let Some(prompt) = prompt { if let Some(prompt) = prompt {
let msg = Message::user().with_text(&prompt); let msg = Message::user().with_text(&prompt);
self.process_message(msg, CancellationToken::default()) self.process_message(msg, CancellationToken::default())
.await?; .await?;
} }
// Initialize the completion cache
self.update_completion_cache().await?; self.update_completion_cache().await?;
// Create a new editor with our custom completer let mut editor = self.create_editor()?;
let history_manager = HistoryManager::new();
history_manager.load(&mut editor);
output::display_greeting();
loop {
self.display_context_usage().await?;
let input = input::get_input(&mut editor)?;
if matches!(input, InputResult::Exit) {
break;
}
self.handle_input(input, &history_manager, &mut editor)
.await?;
}
println!(
"Closing session. Session ID: {}",
console::style(&self.session_id).cyan()
);
Ok(())
}
fn create_editor(
&self,
) -> Result<rustyline::Editor<GooseCompleter, rustyline::history::DefaultHistory>> {
let builder = let builder =
rustyline::Config::builder().completion_type(rustyline::CompletionType::Circular); rustyline::Config::builder().completion_type(rustyline::CompletionType::Circular);
let builder = if let Some(edit_mode) = self.edit_mode { let builder = match self.edit_mode {
builder.edit_mode(edit_mode) Some(mode) => builder.edit_mode(mode),
} else { None => builder.edit_mode(EditMode::Emacs),
// Default to Emacs mode if no edit mode is set
builder.edit_mode(EditMode::Emacs)
}; };
let config = builder.build(); let config = builder.build();
let mut editor = let mut editor =
rustyline::Editor::<GooseCompleter, rustyline::history::DefaultHistory>::with_config( rustyline::Editor::<GooseCompleter, rustyline::history::DefaultHistory>::with_config(
config, config,
)?; )?;
// Set up the completer with a reference to the completion cache
let completer = GooseCompleter::new(self.completion_cache.clone()); let completer = GooseCompleter::new(self.completion_cache.clone());
editor.set_helper(Some(completer)); editor.set_helper(Some(completer));
Ok(editor)
let history_file = Paths::state_dir().join("history.txt");
let old_history_file = Paths::config_dir().join("history.txt");
if let Some(parent) = history_file.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
} }
let history_files = [&history_file, &old_history_file]; async fn handle_input(
let load_from = history_files.iter().find(|f| f.exists()); &mut self,
input: InputResult,
if let Some(file) = load_from { history: &HistoryManager,
if let Err(err) = editor.load_history(file) { editor: &mut rustyline::Editor<GooseCompleter, rustyline::history::DefaultHistory>,
eprintln!("Warning: Failed to load command history: {}", err); ) -> Result<()> {
} match input {
}
let save_history =
|editor: &mut rustyline::Editor<GooseCompleter, rustyline::history::DefaultHistory>| {
if let Err(err) = editor.save_history(&history_file) {
eprintln!("Warning: Failed to save command history: {}", err);
} else if old_history_file.exists() {
if let Err(err) = std::fs::remove_file(&old_history_file) {
eprintln!("Warning: Failed to remove old history file: {}", err);
}
}
};
output::display_greeting();
loop {
// Display context usage before each prompt
self.display_context_usage().await?;
match input::get_input(&mut editor)? {
InputResult::Message(content) => { InputResult::Message(content) => {
self.handle_message_input(&content, history, editor).await?;
}
InputResult::Exit => unreachable!("Exit is handled in the main loop"),
InputResult::AddExtension(cmd) => {
history.save(editor);
match self.add_extension(cmd.clone()).await {
Ok(_) => output::render_extension_success(&cmd),
Err(e) => output::render_extension_error(&cmd, &e.to_string()),
}
}
InputResult::AddBuiltin(names) => {
history.save(editor);
match self.add_builtin(names.clone()).await {
Ok(_) => output::render_builtin_success(&names),
Err(e) => output::render_builtin_error(&names, &e.to_string()),
}
}
InputResult::ToggleTheme => {
history.save(editor);
self.handle_toggle_theme();
}
InputResult::SelectTheme(theme_name) => {
history.save(editor);
self.handle_select_theme(&theme_name);
}
InputResult::Retry => {}
InputResult::ListPrompts(extension) => {
history.save(editor);
match self.list_prompts(extension).await {
Ok(prompts) => output::render_prompts(&prompts),
Err(e) => output::render_error(&e.to_string()),
}
}
InputResult::GooseMode(mode) => {
history.save(editor);
self.handle_goose_mode(&mode)?;
}
InputResult::Plan(options) => {
self.handle_plan_mode(options).await?;
}
InputResult::EndPlan => {
self.run_mode = RunMode::Normal;
output::render_exit_plan_mode();
}
InputResult::Clear => {
history.save(editor);
self.handle_clear().await?;
}
InputResult::PromptCommand(opts) => {
history.save(editor);
self.handle_prompt_command(opts).await?;
}
InputResult::Recipe(filepath_opt) => {
history.save(editor);
self.handle_recipe(filepath_opt).await;
}
InputResult::Compact => {
history.save(editor);
self.handle_compact().await?;
}
}
Ok(())
}
async fn handle_message_input(
&mut self,
content: &str,
history: &HistoryManager,
editor: &mut rustyline::Editor<GooseCompleter, rustyline::history::DefaultHistory>,
) -> Result<()> {
match self.run_mode { match self.run_mode {
RunMode::Normal => { RunMode::Normal => {
save_history(&mut editor); history.save(editor);
self.push_message(Message::user().with_text(content));
self.push_message(Message::user().with_text(&content));
// Track the current directory and last instruction in projects.json
if let Err(e) = crate::project_tracker::update_project_tracker( if let Err(e) = crate::project_tracker::update_project_tracker(
Some(&content), Some(content),
Some(&self.session_id), Some(&self.session_id),
) { ) {
eprintln!("Warning: Failed to update project tracker with instruction: {}", e); eprintln!(
"Warning: Failed to update project tracker with instruction: {}",
e
);
} }
let _provider = self.agent.provider().await?; let _provider = self.agent.provider().await?;
@@ -467,7 +579,6 @@ impl CliSession {
.await?; .await?;
output::hide_thinking(); output::hide_thinking();
// Display elapsed time
let elapsed = start_time.elapsed(); let elapsed = start_time.elapsed();
let elapsed_str = format_elapsed_time(elapsed); let elapsed_str = format_elapsed_time(elapsed);
println!( println!(
@@ -477,33 +588,16 @@ impl CliSession {
} }
RunMode::Plan => { RunMode::Plan => {
let mut plan_messages = self.messages.clone(); let mut plan_messages = self.messages.clone();
plan_messages.push(Message::user().with_text(&content)); plan_messages.push(Message::user().with_text(content));
let reasoner = get_reasoner().await?; let reasoner = get_reasoner().await?;
self.plan_with_reasoner_model(plan_messages, reasoner) self.plan_with_reasoner_model(plan_messages, reasoner)
.await?; .await?;
} }
} }
Ok(())
} }
input::InputResult::Exit => break,
input::InputResult::AddExtension(cmd) => {
save_history(&mut editor);
match self.add_extension(cmd.clone()).await {
Ok(_) => output::render_extension_success(&cmd),
Err(e) => output::render_extension_error(&cmd, &e.to_string()),
}
}
input::InputResult::AddBuiltin(names) => {
save_history(&mut editor);
match self.add_builtin(names.clone()).await {
Ok(_) => output::render_builtin_success(&names),
Err(e) => output::render_builtin_error(&names, &e.to_string()),
}
}
input::InputResult::ToggleTheme => {
save_history(&mut editor);
fn handle_toggle_theme(&self) {
let current = output::get_theme(); let current = output::get_theme();
let new_theme = match current { let new_theme = match current {
output::Theme::Ansi => { output::Theme::Ansi => {
@@ -520,13 +614,10 @@ impl CliSession {
} }
}; };
output::set_theme(new_theme); output::set_theme(new_theme);
continue;
} }
input::InputResult::SelectTheme(theme_name) => { fn handle_select_theme(&self, theme_name: &str) {
save_history(&mut editor); let new_theme = match theme_name {
let new_theme = match theme_name.as_str() {
"light" => { "light" => {
println!("Switching to Light theme"); println!("Switching to Light theme");
output::Theme::Light output::Theme::Light
@@ -542,20 +633,9 @@ impl CliSession {
_ => output::Theme::Dark, _ => output::Theme::Dark,
}; };
output::set_theme(new_theme); output::set_theme(new_theme);
continue;
} }
input::InputResult::Retry => continue,
input::InputResult::ListPrompts(extension) => {
save_history(&mut editor);
match self.list_prompts(extension).await {
Ok(prompts) => output::render_prompts(&prompts),
Err(e) => output::render_error(&e.to_string()),
}
}
input::InputResult::GooseMode(mode) => {
save_history(&mut editor);
fn handle_goose_mode(&self, mode: &str) -> Result<()> {
let config = Config::global(); let config = Config::global();
let mode = match GooseMode::from_str(&mode.to_lowercase()) { let mode = match GooseMode::from_str(&mode.to_lowercase()) {
Ok(mode) => mode, Ok(mode) => mode,
@@ -564,44 +644,35 @@ impl CliSession {
"Invalid mode '{}'. Mode must be one of: auto, approve, chat, smart_approve", "Invalid mode '{}'. Mode must be one of: auto, approve, chat, smart_approve",
mode mode
)); ));
continue; return Ok(());
} }
}; };
config.set_goose_mode(mode)?; config.set_goose_mode(mode)?;
output::goose_mode_message(&format!("Goose mode set to '{:?}'", mode)); output::goose_mode_message(&format!("Goose mode set to '{:?}'", mode));
continue; Ok(())
} }
input::InputResult::Plan(options) => {
async fn handle_plan_mode(&mut self, options: input::PlanCommandOptions) -> Result<()> {
self.run_mode = RunMode::Plan; self.run_mode = RunMode::Plan;
output::render_enter_plan_mode(); output::render_enter_plan_mode();
let message_text = options.message_text; if options.message_text.is_empty() {
if message_text.is_empty() { return Ok(());
continue;
} }
let mut plan_messages = self.messages.clone(); let mut plan_messages = self.messages.clone();
plan_messages.push(Message::user().with_text(&message_text)); plan_messages.push(Message::user().with_text(&options.message_text));
let reasoner = get_reasoner().await?; let reasoner = get_reasoner().await?;
self.plan_with_reasoner_model(plan_messages, reasoner) self.plan_with_reasoner_model(plan_messages, reasoner).await
.await?;
} }
input::InputResult::EndPlan => {
self.run_mode = RunMode::Normal;
output::render_exit_plan_mode();
continue;
}
input::InputResult::Clear => {
save_history(&mut editor);
if let Err(e) = SessionManager::replace_conversation( async fn handle_clear(&mut self) -> Result<()> {
&self.session_id, if let Err(e) =
&Conversation::default(), SessionManager::replace_conversation(&self.session_id, &Conversation::default()).await
)
.await
{ {
output::render_error(&format!("Failed to clear session: {}", e)); output::render_error(&format!("Failed to clear session: {}", e));
continue; return Ok(());
} }
if let Err(e) = SessionManager::update_session(&self.session_id) if let Err(e) = SessionManager::update_session(&self.session_id)
@@ -612,24 +683,19 @@ impl CliSession {
.await .await
{ {
output::render_error(&format!("Failed to reset token counts: {}", e)); output::render_error(&format!("Failed to reset token counts: {}", e));
continue; return Ok(());
} }
self.messages.clear(); self.messages.clear();
tracing::info!("Chat context cleared by user."); tracing::info!("Chat context cleared by user.");
output::render_message( output::render_message(
&Message::assistant().with_text("Chat context cleared.\n"), &Message::assistant().with_text("Chat context cleared.\n"),
self.debug, self.debug,
); );
Ok(())
}
continue; async fn handle_recipe(&mut self, filepath_opt: Option<String>) {
}
input::InputResult::PromptCommand(opts) => {
save_history(&mut editor);
self.handle_prompt_command(opts).await?;
}
InputResult::Recipe(filepath_opt) => {
println!("{}", console::style("Generating Recipe").green()); println!("{}", console::style("Generating Recipe").green());
output::show_thinking(); output::show_thinking();
@@ -638,17 +704,13 @@ impl CliSession {
match recipe { match recipe {
Ok(recipe) => { Ok(recipe) => {
// Use provided filepath or default
let filepath_str = filepath_opt.as_deref().unwrap_or("recipe.yaml"); let filepath_str = filepath_opt.as_deref().unwrap_or("recipe.yaml");
match self.save_recipe(&recipe, filepath_str) { match self.save_recipe(&recipe, filepath_str) {
Ok(path) => println!( Ok(path) => println!(
"{}", "{}",
console::style(format!("Saved recipe to {}", path.display())) console::style(format!("Saved recipe to {}", path.display())).green()
.green()
), ),
Err(e) => { Err(e) => println!("{}", console::style(e).red()),
println!("{}", console::style(e).red());
}
} }
} }
Err(e) => { Err(e) => {
@@ -659,15 +721,11 @@ impl CliSession {
); );
} }
} }
continue;
} }
InputResult::Compact => {
save_history(&mut editor);
async fn handle_compact(&mut self) -> Result<()> {
let prompt = "Are you sure you want to compact this conversation? This will condense the message history."; let prompt = "Are you sure you want to compact this conversation? This will condense the message history.";
let should_summarize = let should_summarize = match cliclack::confirm(prompt).initial_value(true).interact() {
match cliclack::confirm(prompt).initial_value(true).interact() {
Ok(choice) => choice, Ok(choice) => choice,
Err(e) => { Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted { if e.kind() == std::io::ErrorKind::Interrupted {
@@ -687,16 +745,6 @@ impl CliSession {
} else { } else {
println!("{}", console::style("Compaction cancelled.").yellow()); println!("{}", console::style("Compaction cancelled.").yellow());
} }
continue;
}
}
}
println!(
"Closing session. Session ID: {}",
console::style(&self.session_id).cyan()
);
Ok(()) Ok(())
} }