tmai_core/detectors/
mod.rs1mod claude_code;
2mod codex;
3mod default;
4mod gemini;
5
6pub use claude_code::ClaudeCodeDetector;
7pub use codex::CodexDetector;
8pub use default::DefaultDetector;
9pub use gemini::GeminiDetector;
10
11use once_cell::sync::Lazy;
12use parking_lot::Mutex;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16use crate::agents::{AgentStatus, AgentType};
17use crate::config::ClaudeSettingsCache;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub enum DetectionConfidence {
22 High,
24 Medium,
26 Low,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DetectionReason {
33 pub rule: String,
35 pub confidence: DetectionConfidence,
37 pub matched_text: Option<String>,
39}
40
41#[derive(Debug, Clone)]
43pub struct DetectionResult {
44 pub status: AgentStatus,
46 pub reason: DetectionReason,
48}
49
50impl DetectionResult {
51 pub fn new(status: AgentStatus, rule: &str, confidence: DetectionConfidence) -> Self {
53 Self {
54 status,
55 reason: DetectionReason {
56 rule: rule.to_string(),
57 confidence,
58 matched_text: None,
59 },
60 }
61 }
62
63 pub fn with_matched_text(mut self, text: &str) -> Self {
65 let truncated = if text.len() > 200 {
67 format!("{}...", &text[..text.floor_char_boundary(197)])
68 } else {
69 text.to_string()
70 };
71 self.reason.matched_text = Some(truncated);
72 self
73 }
74}
75
76#[derive(Default)]
78pub struct DetectionContext<'a> {
79 pub cwd: Option<&'a str>,
81 pub settings_cache: Option<&'a ClaudeSettingsCache>,
83}
84
85pub trait StatusDetector: Send + Sync {
87 fn detect_status(&self, title: &str, content: &str) -> AgentStatus;
89
90 fn detect_status_with_context(
95 &self,
96 title: &str,
97 content: &str,
98 _context: &DetectionContext,
99 ) -> AgentStatus {
100 self.detect_status(title, content)
101 }
102
103 fn agent_type(&self) -> AgentType;
105
106 fn detect_context_warning(&self, _content: &str) -> Option<u8> {
109 None
110 }
111
112 fn detect_status_with_reason(
116 &self,
117 title: &str,
118 content: &str,
119 context: &DetectionContext,
120 ) -> DetectionResult {
121 let status = self.detect_status_with_context(title, content, context);
122 DetectionResult::new(status, "legacy_fallback", DetectionConfidence::Low)
123 }
124
125 fn approval_keys(&self) -> &str {
127 "Enter"
128 }
129}
130
131static CLAUDE_DETECTOR: Lazy<ClaudeCodeDetector> = Lazy::new(ClaudeCodeDetector::new);
133static CODEX_DETECTOR: Lazy<CodexDetector> = Lazy::new(CodexDetector::new);
134static GEMINI_DETECTOR: Lazy<GeminiDetector> = Lazy::new(GeminiDetector::new);
135static OPENCODE_DETECTOR: Lazy<DefaultDetector> =
136 Lazy::new(|| DefaultDetector::new(AgentType::OpenCode));
137
138static CUSTOM_DETECTORS: Lazy<Mutex<HashMap<String, &'static dyn StatusDetector>>> =
140 Lazy::new(|| Mutex::new(HashMap::new()));
141
142pub fn get_detector(agent_type: &AgentType) -> &'static dyn StatusDetector {
145 match agent_type {
146 AgentType::ClaudeCode => &*CLAUDE_DETECTOR,
147 AgentType::CodexCli => &*CODEX_DETECTOR,
148 AgentType::GeminiCli => &*GEMINI_DETECTOR,
149 AgentType::OpenCode => &*OPENCODE_DETECTOR,
150 AgentType::Custom(name) => {
151 let mut cache = CUSTOM_DETECTORS.lock();
153 if let Some(&detector) = cache.get(name) {
154 detector
155 } else {
156 let detector: &'static dyn StatusDetector = Box::leak(Box::new(
158 DefaultDetector::new(AgentType::Custom(name.clone())),
159 ));
160 cache.insert(name.clone(), detector);
161 detector
162 }
163 }
164 }
165}