use crate::console::CliConsole;
use sage_core::commands::{CommandExecutor, CommandRegistry};
use sage_core::error::SageResult;
use sage_core::output::OutputMode;
use std::sync::Arc;
pub enum SlashCommandAction {
Prompt(String),
Handled,
HandledWithOutput(String),
SetOutputMode(OutputMode),
Resume { session_id: Option<String> },
SwitchModel { model: String },
ModelSelect { models: Vec<String> },
Doctor,
Exit,
}
pub async fn process_slash_command(
input: &str,
console: &CliConsole,
working_dir: &std::path::Path,
) -> SageResult<SlashCommandAction> {
if !CommandExecutor::is_command(input) {
return Ok(SlashCommandAction::Prompt(input.to_string()));
}
let mut registry = CommandRegistry::new(working_dir);
registry.register_builtins();
if let Err(e) = registry.discover().await {
console.warn(&format!("Failed to discover commands: {}", e));
}
let cmd_executor = CommandExecutor::new(Arc::new(tokio::sync::RwLock::new(registry)));
match cmd_executor.process(input).await {
Ok(Some(result)) => {
if let Some(interactive_cmd) = result.interactive {
return handle_interactive_command_v2(&interactive_cmd, console).await;
}
if result.is_local {
if let Some(status) = &result.status_message {
console.info(status);
}
if let Some(output) = &result.local_output {
return Ok(SlashCommandAction::HandledWithOutput(output.clone()));
}
return Ok(SlashCommandAction::Handled);
}
if result.show_expansion {
console.info(&format!(
"Command expanded: {}",
&result.expanded_prompt[..result.expanded_prompt.len().min(100)]
));
}
if let Some(status) = &result.status_message {
console.info(status);
}
Ok(SlashCommandAction::Prompt(result.expanded_prompt))
}
Ok(None) => Ok(SlashCommandAction::Prompt(input.to_string())),
Err(e) => Err(e),
}
}
pub async fn handle_interactive_command_v2(
cmd: &sage_core::commands::types::InteractiveCommand,
console: &CliConsole,
) -> SageResult<SlashCommandAction> {
use sage_core::commands::types::InteractiveCommand;
match cmd {
InteractiveCommand::Resume { session_id, .. } => Ok(SlashCommandAction::Resume {
session_id: session_id.clone(),
}),
InteractiveCommand::Title { title } => {
console.warn(&format!(
"Title command not available in non-interactive mode. Title: {}",
title
));
Ok(SlashCommandAction::Handled)
}
InteractiveCommand::Login => {
use crate::commands::interactive::CliOnboarding;
let mut onboarding = CliOnboarding::new();
match onboarding.run_login().await {
Ok(true) => {
console.success("API key updated! Restart sage to use the new key.");
}
Ok(false) => {
console.info("API key not changed.");
}
Err(e) => {
console.error(&format!("Login failed: {}", e));
}
}
Ok(SlashCommandAction::Handled)
}
InteractiveCommand::OutputMode { mode } => {
let output_mode = match mode.as_str() {
"streaming" => OutputMode::Streaming,
"batch" => OutputMode::Batch,
"silent" => OutputMode::Silent,
_ => {
console.warn(&format!("Unknown output mode: {}", mode));
return Ok(SlashCommandAction::Handled);
}
};
Ok(SlashCommandAction::SetOutputMode(output_mode))
}
InteractiveCommand::Clear => {
console.info("Conversation cleared.");
Ok(SlashCommandAction::Handled)
}
InteractiveCommand::Logout => {
console.info("Credentials cleared.");
Ok(SlashCommandAction::Handled)
}
InteractiveCommand::Model { model } => {
Ok(SlashCommandAction::SwitchModel {
model: model.clone(),
})
}
InteractiveCommand::ModelSelect => {
use sage_core::config::{ModelsApiClient, ProviderRegistry, load_config};
let config = match load_config() {
Ok(c) => c,
Err(e) => {
return Ok(SlashCommandAction::HandledWithOutput(format!(
"Failed to load config: {}",
e
)));
}
};
let provider_name = config.get_default_provider();
let mut registry = ProviderRegistry::with_defaults();
let provider_info = registry.get_provider(provider_name).cloned();
let (base_url, api_key) = {
let mut base_url = provider_info
.as_ref()
.map(|p| p.api_base_url.clone())
.unwrap_or_default();
let mut api_key = None;
if let Some(params) = config.model_providers.get(provider_name) {
if let Some(url) = ¶ms.base_url {
base_url = url.clone();
}
api_key = params.api_key.clone();
}
if api_key.is_none() {
let env_var = match provider_name {
"anthropic" => "ANTHROPIC_API_KEY",
"openai" => "OPENAI_API_KEY",
"zai" => "ZAI_API_KEY",
"google" => "GOOGLE_API_KEY",
"glm" | "zhipu" => "GLM_API_KEY",
"moonshot" | "kimi" => "MOONSHOT_API_KEY",
_ => "",
};
if !env_var.is_empty() {
api_key = std::env::var(env_var).ok();
}
}
(base_url, api_key)
};
let client = ModelsApiClient::new();
let models: Vec<String> = match provider_name {
"anthropic" | "glm" | "zhipu" => {
match client
.fetch_anthropic_models(&base_url, api_key.as_deref().unwrap_or(""))
.await
{
Ok(m) => m.into_iter().map(|m| m.id).collect(),
Err(_) => provider_info
.as_ref()
.map(|p| p.models.iter().map(|m| m.id.clone()).collect())
.unwrap_or_default(),
}
}
"openai" | "openrouter" | "zai" | "moonshot" | "kimi" => {
match client
.fetch_openai_models(&base_url, api_key.as_deref().unwrap_or(""))
.await
{
Ok(m) => m.into_iter().map(|m| m.id).collect(),
Err(_) => provider_info
.as_ref()
.map(|p| p.models.iter().map(|m| m.id.clone()).collect())
.unwrap_or_default(),
}
}
"ollama" => match client.fetch_ollama_models(&base_url).await {
Ok(m) => m.into_iter().map(|m| m.id).collect(),
Err(_) => provider_info
.as_ref()
.map(|p| p.models.iter().map(|m| m.id.clone()).collect())
.unwrap_or_default(),
},
_ => provider_info
.as_ref()
.map(|p| p.models.iter().map(|m| m.id.clone()).collect())
.unwrap_or_default(),
};
if models.is_empty() {
return Ok(SlashCommandAction::HandledWithOutput(
"No models available for this provider".to_string(),
));
}
Ok(SlashCommandAction::ModelSelect { models })
}
InteractiveCommand::Doctor => Ok(SlashCommandAction::Doctor),
InteractiveCommand::Exit => {
console.info("Exiting...");
Ok(SlashCommandAction::Exit)
}
}
}