varios improvements to /switch, /model, and /tools

This commit is contained in:
leach
2025-08-18 00:45:54 -04:00
parent 54a456581d
commit e1dd961f3f
3 changed files with 322 additions and 177 deletions

View File

@@ -3,9 +3,9 @@ use anyhow::Result;
use crate::config::Config;
use crate::core::{
create_client, get_provider_for_model, provider::get_all_models, provider::get_supported_models,
provider::is_model_supported, ChatClient, Session,
ChatClient, Session,
};
use crate::utils::{Display, InputHandler};
use crate::utils::{Display, InputHandler, SessionAction};
pub struct ChatCLI {
session: Session,
@@ -134,7 +134,7 @@ impl ChatCLI {
return Ok(false);
}
"/model" => {
self.handle_model_command(&parts).await?;
self.model_switcher().await?;
}
"/models" => {
self.list_models();
@@ -145,22 +145,16 @@ impl ChatCLI {
"/new" => {
self.handle_new_session(&parts)?;
}
"/switch" => {
self.handle_switch_session(&parts).await?;
"/switch" | "/sessions" => {
self.session_manager().await?;
}
"/clear" => {
self.session.clear_messages();
self.session.save()?;
self.display.print_command_result("Conversation cleared");
}
"/delete" => {
self.handle_delete_session(&parts).await?;
}
"/tool" => {
self.handle_tool_command(&parts)?;
}
"/reasoning" => {
self.handle_reasoning_command(&parts)?;
"/tools" => {
self.tools_manager().await?;
}
"/effort" => {
self.handle_effort_command(&parts)?;
@@ -182,42 +176,35 @@ impl ChatCLI {
Ok(true)
}
async fn handle_model_command(&mut self, parts: &[&str]) -> Result<()> {
if parts.len() == 1 {
let all_models = get_all_models();
let selection = self.input.select_from_list(
"Select a model:",
&all_models,
Some(&self.session.model),
)?;
if let Some(model) = selection {
self.session.model = model.to_string();
let provider = get_provider_for_model(&self.session.model);
self.display.print_command_result(&format!(
"Model switched to {} ({})",
self.session.model,
provider.as_str()
));
self.client = None; // Force client recreation
async fn model_switcher(&mut self) -> Result<()> {
let all_models = get_all_models();
let selection = self.input.select_from_list(
"Select a model:",
&all_models,
Some(&self.session.model),
)?;
match selection {
Some(model) => {
if model.to_string() == self.session.model {
self.display.print_info("Already using that model");
} else {
self.session.model = model.to_string();
let provider = get_provider_for_model(&self.session.model);
self.display.print_command_result(&format!(
"Model switched to {} ({})",
self.session.model,
provider.as_str()
));
self.client = None; // Force client recreation
self.session.save()?; // Save the model change
}
}
} else if parts.len() == 2 {
let model = parts[1];
if !is_model_supported(model) {
self.display.print_error("Unsupported model. Use /models to see the list of supported models.");
} else {
self.session.model = model.to_string();
let provider = get_provider_for_model(&self.session.model);
self.display.print_command_result(&format!(
"Model switched to {} ({})",
self.session.model,
provider.as_str()
));
self.client = None; // Force client recreation
None => {
self.display.print_info("Model selection cancelled");
}
} else {
self.display.print_error("Usage: /model [model_name]");
}
Ok(())
}
@@ -277,150 +264,188 @@ impl ChatCLI {
Ok(())
}
async fn handle_switch_session(&mut self, parts: &[&str]) -> Result<()> {
if parts.len() == 1 {
async fn session_manager(&mut self) -> Result<()> {
loop {
let sessions = Session::list_sessions()?;
let session_names: Vec<String> = sessions
.into_iter()
.map(|(name, _)| name)
.filter(|name| name != &self.session.name)
.collect();
if let Some(selection) = self.input.select_from_list(
"Switch to session:",
&session_names,
None,
)? {
self.session.save()?;
match Session::load(&selection) {
Ok(session) => {
self.session = session;
self.display.print_command_result(&format!(
"Switched to session '{}' (model={})",
self.session.name, self.session.model
));
self.client = None; // Force client recreation
}
Err(e) => {
self.display.print_error(&format!("Failed to load session: {}", e));
}
}
}
} else if parts.len() == 2 {
let session_name = parts[1];
self.session.save()?;
match Session::load(session_name) {
Ok(session) => {
self.session = session;
self.display.print_command_result(&format!(
"Switched to session '{}' (model={})",
self.session.name, self.session.model
));
self.client = None; // Force client recreation
}
Err(e) => {
self.display.print_error(&format!("Failed to load session: {}", e));
}
}
} else {
self.display.print_error("Usage: /switch [session_name]");
}
Ok(())
}
async fn handle_delete_session(&mut self, parts: &[&str]) -> Result<()> {
let target = if parts.len() == 1 {
let sessions = Session::list_sessions()?;
let session_names: Vec<String> = sessions
.into_iter()
.map(|(name, _)| name)
.filter(|name| name != &self.session.name)
.collect();
self.input.select_from_list("Delete session:", &session_names, None)?
} else if parts.len() == 2 {
Some(parts[1].to_string())
} else {
self.display.print_error("Usage: /delete [session_name]");
return Ok(());
};
if let Some(target) = target {
if target == self.session.name {
self.display.print_error(
"Cannot delete the session you are currently using. Switch to another session first."
);
if session_names.is_empty() {
self.display.print_info("No sessions available");
return Ok(());
}
if self.input.confirm(&format!("Delete session '{}'?", target))? {
match Session::delete_session(&target) {
Ok(()) => {
self.display.print_command_result(&format!("Session '{}' deleted", target));
let action = self.input.session_manager(
"Session Manager:",
&session_names,
Some(&self.session.name),
)?;
match action {
SessionAction::Switch(session_name) => {
if session_name == self.session.name {
self.display.print_info("Already in that session");
return Ok(());
}
Err(e) => {
self.display.print_error(&format!("Failed to delete session: {}", e));
self.session.save()?;
match Session::load(&session_name) {
Ok(session) => {
self.session = session;
self.display.print_command_result(&format!(
"Switched to session '{}' (model={})",
self.session.name, self.session.model
));
self.client = None; // Force client recreation
return Ok(());
}
Err(e) => {
self.display.print_error(&format!("Failed to load session: {}", e));
// Don't return, allow user to try again or cancel
}
}
}
}
}
Ok(())
}
fn handle_tool_command(&mut self, parts: &[&str]) -> Result<()> {
if parts.len() != 3 || parts[1].to_lowercase() != "websearch" || !["on", "off"].contains(&parts[2]) {
self.display.print_error("Usage: /tool websearch on|off");
return Ok(());
}
let enable = parts[2] == "on";
if enable {
let model = self.session.model.clone();
if let Ok(client) = self.get_client() {
if !client.supports_feature_for_model("web_search", &model) {
let provider = get_provider_for_model(&model);
self.display.print_warning(&format!(
"Web search is not supported by {} models",
provider.as_str()
));
SessionAction::Delete(session_name) => {
match Session::delete_session(&session_name) {
Ok(()) => {
self.display.print_command_result(&format!("Session '{}' deleted", session_name));
// If we deleted the current session, we need to handle this specially
if session_name == self.session.name {
// Try to switch to another session or create a default one
let remaining_sessions = Session::list_sessions()?;
let remaining_names: Vec<String> = remaining_sessions
.into_iter()
.map(|(name, _)| name)
.collect();
if remaining_names.is_empty() {
// No sessions left, create a default one
self.session = Session::new("default".to_string(), self.session.model.clone());
self.display.print_command_result("Created new default session");
return Ok(());
} else {
// Switch to the first available session
match Session::load(&remaining_names[0]) {
Ok(session) => {
self.session = session;
self.display.print_command_result(&format!(
"Switched to session '{}' (model={})",
self.session.name, self.session.model
));
self.client = None;
return Ok(());
}
Err(e) => {
self.display.print_error(&format!("Failed to load fallback session: {}", e));
// Create a new default session as fallback
self.session = Session::new("default".to_string(), self.session.model.clone());
self.display.print_command_result("Created new default session");
return Ok(());
}
}
}
}
// Continue to show updated session list if we didn't delete current session
}
Err(e) => {
self.display.print_error(&format!("Failed to delete session: {}", e));
// Continue to allow retry
}
}
}
SessionAction::Cancel => {
return Ok(());
}
}
}
self.session.enable_web_search = enable;
let state = if enable { "enabled" } else { "disabled" };
self.display.print_command_result(&format!("Web search tool {}", state));
Ok(())
}
fn handle_reasoning_command(&mut self, parts: &[&str]) -> Result<()> {
if parts.len() != 2 || !["on", "off"].contains(&parts[1]) {
self.display.print_error("Usage: /reasoning on|off");
return Ok(());
}
let enable = parts[1] == "on";
if enable {
async fn tools_manager(&mut self) -> Result<()> {
loop {
// Show current tool status
self.display.print_info("Tool Management:");
let web_status = if self.session.enable_web_search { "✓ enabled" } else { "✗ disabled" };
let reasoning_status = if self.session.enable_reasoning_summary { "✓ enabled" } else { "✗ disabled" };
println!(" Web Search: {}", web_status);
println!(" Reasoning Summaries: {}", reasoning_status);
println!(" Reasoning Effort: {}", self.session.reasoning_effort);
// Check model compatibility
let model = self.session.model.clone();
if let Ok(client) = self.get_client() {
if !client.supports_feature_for_model("reasoning_summary", &model) {
let provider = get_provider_for_model(&model);
self.display.print_warning(&format!(
"Reasoning summaries are not supported by {} models",
provider.as_str()
));
let provider = get_provider_for_model(&model);
let web_enabled = self.session.enable_web_search;
let reasoning_enabled = self.session.enable_reasoning_summary;
// Show compatibility warnings based on provider
match provider {
crate::core::provider::Provider::Anthropic => {
if web_enabled {
self.display.print_warning("Web search is not supported by Anthropic models");
}
if reasoning_enabled {
self.display.print_warning("Reasoning summaries are not supported by Anthropic models");
}
}
crate::core::provider::Provider::OpenAI => {
// OpenAI models generally support these features
}
}
// Tool management options
let options = vec![
"Toggle Web Search",
"Toggle Reasoning Summaries",
"Set Reasoning Effort",
"Done"
];
let selection = self.input.select_from_list(
"Select an option:",
&options,
None,
)?;
match selection.as_deref() {
Some("Toggle Web Search") => {
self.session.enable_web_search = !self.session.enable_web_search;
let state = if self.session.enable_web_search { "enabled" } else { "disabled" };
self.display.print_command_result(&format!("Web search {}", state));
}
Some("Toggle Reasoning Summaries") => {
self.session.enable_reasoning_summary = !self.session.enable_reasoning_summary;
let state = if self.session.enable_reasoning_summary { "enabled" } else { "disabled" };
self.display.print_command_result(&format!("Reasoning summaries {}", state));
}
Some("Set Reasoning Effort") => {
let effort_options = vec!["low", "medium", "high"];
if let Some(effort) = self.input.select_from_list(
"Select reasoning effort:",
&effort_options,
Some(&self.session.reasoning_effort),
)? {
self.session.reasoning_effort = effort.to_string();
self.display.print_command_result(&format!("Reasoning effort set to {}", effort));
if !self.session.model.starts_with("gpt-5") {
self.display.print_warning("Reasoning effort is only supported by GPT-5 models");
}
}
}
Some("Done") | None => {
break;
}
_ => {}
}
self.session.save()?; // Save changes after each modification
println!(); // Add spacing
}
self.session.enable_reasoning_summary = enable;
let state = if enable { "enabled" } else { "disabled" };
self.display.print_command_result(&format!("Reasoning summaries {}", state));
Ok(())
}