use crate::error::Result;
use crate::repl::Repl;
pub mod builtin;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandResult {
Continue,
Exit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
Exit,
Clear,
Resume,
EffortPicker,
EffortSet(crate::api::ReasoningEffort),
SafeMode,
NormalMode,
Compact,
ModelPicker,
ModelSet(String),
}
impl Command {
pub fn from_str(s: &str) -> Option<Self> {
let lower = s.to_lowercase();
match lower.as_str() {
"/exit" | "/quit" | "/q" => Some(Command::Exit),
"/clear" => Some(Command::Clear),
"/resume" => Some(Command::Resume),
"/effort" => Some(Command::EffortPicker),
"/safe" => Some(Command::SafeMode),
"/normal" => Some(Command::NormalMode),
"/compact" => Some(Command::Compact),
"/model" => Some(Command::ModelPicker),
_ => {
if let Some(arg) = lower.strip_prefix("/effort ") {
let trimmed = arg.trim();
if trimmed.is_empty() {
Some(Command::EffortPicker)
} else {
crate::api::ReasoningEffort::parse(trimmed).map(Command::EffortSet)
}
} else if let Some(arg) = lower.strip_prefix("/model ") {
let trimmed = arg.trim();
if trimmed.is_empty() {
Some(Command::ModelPicker)
} else {
Some(Command::ModelSet(trimmed.to_string()))
}
} else {
None
}
}
}
}
pub fn execute(&self, repl: &mut Repl) -> Result<CommandResult> {
match self {
Command::Exit => builtin::exit_command(repl),
Command::Clear => builtin::clear_command(repl),
Command::Resume => builtin::resume_command(repl),
Command::EffortPicker => builtin::effort_picker_command(repl),
Command::EffortSet(effort) => builtin::effort_set_command(repl, *effort),
Command::SafeMode => builtin::safe_mode_command(repl),
Command::NormalMode => builtin::normal_mode_command(repl),
Command::Compact => builtin::compact_command(repl),
Command::ModelPicker => builtin::model_picker_command(repl),
Command::ModelSet(name) => builtin::model_set_command(repl, name),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CommandEntry {
pub name: &'static str,
pub description: &'static str,
}
pub static COMMAND_CATALOG: &[CommandEntry] = &[
CommandEntry {
name: "/compact",
description: "summarize the conversation to free up context",
},
CommandEntry {
name: "/clear",
description: "clear the conversation and start fresh",
},
CommandEntry {
name: "/model",
description: "switch the active model (opens a picker)",
},
CommandEntry {
name: "/effort",
description: "switch the reasoning effort (opens a picker)",
},
CommandEntry {
name: "/resume",
description: "resume a previously saved session",
},
CommandEntry {
name: "/safe",
description: "enter safe mode (only read-only tools are allowed)",
},
CommandEntry {
name: "/normal",
description: "leave safe mode and resume normal mode",
},
CommandEntry {
name: "/exit",
description: "save the session and quit",
},
CommandEntry {
name: "/quit",
description: "alias of /exit",
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_slash_model_opens_picker() {
assert_eq!(Command::from_str("/model"), Some(Command::ModelPicker));
}
#[test]
fn slash_model_with_trailing_space_opens_picker() {
assert_eq!(Command::from_str("/model "), Some(Command::ModelPicker));
}
#[test]
fn slash_model_with_name_parses_to_model_set() {
match Command::from_str(&format!("/model {}", crate::api::model_info::CLAUDE_OPUS)) {
Some(Command::ModelSet(name)) => assert_eq!(name, crate::api::model_info::CLAUDE_OPUS),
other => panic!("expected ModelSet, got {other:?}"),
}
}
#[test]
fn slash_model_lowercases_input_before_parsing() {
match Command::from_str(&format!(
"/model {}",
crate::api::model_info::CLAUDE_OPUS.to_uppercase()
)) {
Some(Command::ModelSet(name)) => assert_eq!(name, crate::api::model_info::CLAUDE_OPUS),
other => panic!("expected ModelSet, got {other:?}"),
}
}
#[test]
fn slash_model_accepts_unknown_arg_for_handler_to_reject() {
match Command::from_str("/model totally-made-up") {
Some(Command::ModelSet(name)) => assert_eq!(name, "totally-made-up"),
other => panic!("expected ModelSet, got {other:?}"),
}
}
#[test]
fn bare_slash_effort_opens_picker() {
assert_eq!(Command::from_str("/effort"), Some(Command::EffortPicker));
}
#[test]
fn slash_effort_with_trailing_space_opens_picker() {
assert_eq!(Command::from_str("/effort "), Some(Command::EffortPicker));
}
#[test]
fn slash_effort_with_level_parses_to_effort_set() {
match Command::from_str("/effort high") {
Some(Command::EffortSet(e)) => assert_eq!(e, crate::api::ReasoningEffort::High),
other => panic!("expected EffortSet, got {other:?}"),
}
}
#[test]
fn slash_effort_with_unknown_level_returns_none() {
assert!(Command::from_str("/effort turbo").is_none());
}
#[test]
fn every_catalog_entry_parses() {
for entry in COMMAND_CATALOG {
assert!(
Command::from_str(entry.name).is_some(),
"command `{}` is in the catalog but does not parse",
entry.name
);
}
}
}