use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display, EnumIter, EnumString, IntoEnumIterator};
use crate::domain::value::CommandLine;
#[derive(
AsRefStr,
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Display,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
#[serde(rename_all = "lowercase")]
pub enum AgentTool {
#[strum(to_string = "Claude")]
Claude,
#[strum(to_string = "Codex")]
Codex,
#[strum(to_string = "Gemini")]
Gemini,
#[strum(to_string = "Amp")]
Amp,
#[strum(to_string = "OpenCode")]
Opencode,
#[strum(to_string = "Copilot")]
Copilot,
#[strum(to_string = "Kimi")]
Kimi,
#[strum(to_string = "Custom agent", serialize = "custom")]
Custom,
}
impl AgentTool {
pub fn options() -> impl Iterator<Item = Self> {
Self::iter()
}
pub const fn protocol_token(self) -> &'static str {
match self {
Self::Claude => "claude",
Self::Codex => "codex",
Self::Gemini => "gemini",
Self::Amp => "amp",
Self::Opencode => "opencode",
Self::Copilot => "copilot",
Self::Kimi => "kimi",
Self::Custom => "custom",
}
}
pub const fn default_command(self) -> Option<&'static str> {
match self {
Self::Claude => Some("claude"),
Self::Codex => Some("codex"),
Self::Gemini => Some("gemini"),
Self::Amp => Some("amp"),
Self::Opencode => Some("opencode"),
Self::Copilot => Some("copilot"),
Self::Kimi => Some("kimi"),
Self::Custom => None,
}
}
pub fn from_command(command: Option<&CommandLine>) -> Self {
let Some(executable) = command
.and_then(|command| command.as_ref().split_whitespace().next())
.and_then(|executable| executable.rsplit(['/', '\\']).next())
.map(|executable| {
executable
.strip_suffix(".exe")
.or_else(|| executable.strip_suffix(".cmd"))
.or_else(|| executable.strip_suffix(".bat"))
.unwrap_or(executable)
})
else {
return Self::Custom;
};
Self::iter()
.find(|tool| {
tool.default_command().is_some_and(|default_command| {
#[cfg(windows)]
{
executable.eq_ignore_ascii_case(default_command)
}
#[cfg(not(windows))]
{
executable == default_command
}
})
})
.unwrap_or(Self::Custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{agent_session::NativeSessionId, process::AgentProtocol};
#[test]
fn infers_known_executables_from_commands() {
let command = CommandLine::try_new("/usr/local/bin/codex --full-auto").unwrap();
assert_eq!(AgentTool::from_command(Some(&command)), AgentTool::Codex);
}
#[test]
fn infers_known_windows_executables_from_commands() {
let command = CommandLine::try_new(r"C:\Tools\claude.cmd").unwrap();
assert_eq!(AgentTool::from_command(Some(&command)), AgentTool::Claude);
}
#[test]
fn treats_unknown_commands_as_custom() {
let command = CommandLine::try_new("my-agent").unwrap();
assert_eq!(AgentTool::from_command(Some(&command)), AgentTool::Custom);
}
#[test]
fn every_launcher_option_parses() {
for tool in AgentTool::options() {
assert_eq!(tool.to_string().parse::<AgentTool>().unwrap(), tool);
}
}
#[test]
fn every_tool_maps_to_its_launcher_option() {
for tool in AgentTool::options() {
assert_eq!(tool.to_string().parse::<AgentTool>().unwrap(), tool);
}
}
#[test]
fn displays_title_cased_provider_labels() {
assert_eq!(AgentTool::Claude.to_string(), "Claude");
assert_eq!(AgentTool::Codex.to_string(), "Codex");
assert_eq!(AgentTool::Gemini.to_string(), "Gemini");
assert_eq!(AgentTool::Opencode.to_string(), "OpenCode");
}
#[test]
fn exposes_lowercase_protocol_tokens() {
assert_eq!(AgentTool::Claude.protocol_token(), "claude");
assert_eq!(AgentTool::Opencode.protocol_token(), "opencode");
assert_eq!(AgentTool::Custom.protocol_token(), "custom");
}
#[test]
fn custom_protocol_token_parses() {
assert_eq!("custom".parse::<AgentTool>().unwrap(), AgentTool::Custom);
}
#[test]
fn builds_provider_resume_commands() {
let cases = [
(AgentTool::Claude, "claude --resume abc"),
(AgentTool::Codex, "codex resume abc"),
(AgentTool::Gemini, "gemini --resume abc"),
(AgentTool::Amp, "amp threads continue abc"),
(AgentTool::Opencode, "opencode --session abc"),
(AgentTool::Copilot, "copilot --resume abc"),
(AgentTool::Kimi, "kimi --session abc"),
];
let native_id = NativeSessionId::try_new("abc").unwrap();
for (tool, expected) in cases {
let launch = CommandLine::try_new(tool.default_command().unwrap()).unwrap();
assert_eq!(
tool.resume_command(&launch, &native_id).unwrap().as_ref(),
expected
);
}
}
}