mod claude_code;
mod codex;
pub(crate) mod common;
mod default;
mod gemini;
pub use claude_code::ClaudeCodeDetector;
pub use codex::CodexDetector;
pub use default::DefaultDetector;
pub use gemini::GeminiDetector;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::agents::{AgentStatus, AgentType};
use crate::config::ClaudeSettingsCache;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DetectionConfidence {
High,
Medium,
Low,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectionReason {
pub rule: String,
pub confidence: DetectionConfidence,
pub matched_text: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DetectionResult {
pub status: AgentStatus,
pub reason: DetectionReason,
}
impl DetectionResult {
pub fn new(status: AgentStatus, rule: &str, confidence: DetectionConfidence) -> Self {
Self {
status,
reason: DetectionReason {
rule: rule.to_string(),
confidence,
matched_text: None,
},
}
}
pub fn with_matched_text(mut self, text: &str) -> Self {
let truncated = if text.len() > 200 {
format!("{}...", &text[..text.floor_char_boundary(197)])
} else {
text.to_string()
};
self.reason.matched_text = Some(truncated);
self
}
}
#[derive(Default)]
pub struct DetectionContext<'a> {
pub cwd: Option<&'a str>,
pub settings_cache: Option<&'a ClaudeSettingsCache>,
}
pub trait StatusDetector: Send + Sync {
fn detect_status(&self, title: &str, content: &str) -> AgentStatus;
fn detect_status_with_context(
&self,
title: &str,
content: &str,
_context: &DetectionContext,
) -> AgentStatus {
self.detect_status(title, content)
}
fn agent_type(&self) -> AgentType;
fn detect_context_warning(&self, _content: &str) -> Option<u8> {
None
}
fn detect_status_with_reason(
&self,
title: &str,
content: &str,
context: &DetectionContext,
) -> DetectionResult {
let status = self.detect_status_with_context(title, content, context);
DetectionResult::new(status, "legacy_fallback", DetectionConfidence::Low)
}
fn approval_keys(&self) -> &str {
"Enter"
}
}
static CLAUDE_DETECTOR: Lazy<ClaudeCodeDetector> = Lazy::new(ClaudeCodeDetector::new);
static CODEX_DETECTOR: Lazy<CodexDetector> = Lazy::new(CodexDetector::new);
static GEMINI_DETECTOR: Lazy<GeminiDetector> = Lazy::new(GeminiDetector::new);
static OPENCODE_DETECTOR: Lazy<DefaultDetector> =
Lazy::new(|| DefaultDetector::new(AgentType::OpenCode));
static CUSTOM_DETECTORS: Lazy<Mutex<HashMap<String, &'static dyn StatusDetector>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
pub fn get_detector(agent_type: &AgentType) -> &'static dyn StatusDetector {
match agent_type {
AgentType::ClaudeCode => &*CLAUDE_DETECTOR,
AgentType::CodexCli => &*CODEX_DETECTOR,
AgentType::GeminiCli => &*GEMINI_DETECTOR,
AgentType::OpenCode => &*OPENCODE_DETECTOR,
AgentType::Custom(name) => {
let mut cache = CUSTOM_DETECTORS.lock();
if let Some(&detector) = cache.get(name) {
detector
} else {
let detector: &'static dyn StatusDetector = Box::leak(Box::new(
DefaultDetector::new(AgentType::Custom(name.clone())),
));
cache.insert(name.clone(), detector);
detector
}
}
}
}