Skip to main content

tmai_core/detectors/
default.rs

1use regex::Regex;
2
3use crate::agents::{AgentStatus, AgentType, ApprovalType};
4
5use super::{DetectionConfidence, DetectionContext, DetectionResult, StatusDetector};
6
7/// Default detector for unknown or custom agents
8pub struct DefaultDetector {
9    agent_type: AgentType,
10    approval_pattern: Regex,
11    error_pattern: Regex,
12}
13
14impl DefaultDetector {
15    pub fn new(agent_type: AgentType) -> Self {
16        Self {
17            agent_type,
18            approval_pattern: Regex::new(
19                r"(?i)\[y/n\]|\[Y/n\]|Yes\s*/\s*No|approve|confirm|allow|proceed\?",
20            )
21            .unwrap(),
22            error_pattern: Regex::new(r"(?i)(?:^|\n)\s*(?:Error|ERROR|error:|✗|❌)").unwrap(),
23        }
24    }
25
26    fn detect_approval(&self, content: &str) -> Option<(ApprovalType, String)> {
27        let lines: Vec<&str> = content.lines().collect();
28        let check_start = lines.len().saturating_sub(15);
29        let recent = lines[check_start..].join("\n");
30
31        if self.approval_pattern.is_match(&recent) {
32            Some((
33                ApprovalType::Other("Pending approval".to_string()),
34                String::new(),
35            ))
36        } else {
37            None
38        }
39    }
40
41    fn detect_error(&self, content: &str) -> Option<String> {
42        let lines: Vec<&str> = content.lines().collect();
43        let check_start = lines.len().saturating_sub(10);
44        let recent = lines[check_start..].join("\n");
45
46        if self.error_pattern.is_match(&recent) {
47            for line in lines.iter().rev().take(10) {
48                if line.to_lowercase().contains("error") {
49                    return Some(line.trim().to_string());
50                }
51            }
52            return Some("Error detected".to_string());
53        }
54        None
55    }
56}
57
58impl StatusDetector for DefaultDetector {
59    fn detect_status(&self, title: &str, content: &str) -> AgentStatus {
60        self.detect_status_with_reason(title, content, &DetectionContext::default())
61            .status
62    }
63
64    fn detect_status_with_reason(
65        &self,
66        title: &str,
67        content: &str,
68        _context: &DetectionContext,
69    ) -> DetectionResult {
70        // Check for approval requests
71        if let Some((approval_type, details)) = self.detect_approval(content) {
72            return DetectionResult::new(
73                AgentStatus::AwaitingApproval {
74                    approval_type,
75                    details,
76                },
77                "default_approval_pattern",
78                DetectionConfidence::High,
79            );
80        }
81
82        // Check for errors
83        if let Some(message) = self.detect_error(content) {
84            return DetectionResult::new(
85                AgentStatus::Error {
86                    message: message.clone(),
87                },
88                "default_error_pattern",
89                DetectionConfidence::High,
90            )
91            .with_matched_text(&message);
92        }
93
94        // Title-based heuristics
95        let title_lower = title.to_lowercase();
96        if title_lower.contains("idle")
97            || title_lower.contains("ready")
98            || title_lower.contains("waiting")
99        {
100            return DetectionResult::new(
101                AgentStatus::Idle,
102                "default_title_idle",
103                DetectionConfidence::Medium,
104            )
105            .with_matched_text(title);
106        }
107
108        if title_lower.contains("working")
109            || title_lower.contains("processing")
110            || title_lower.contains("running")
111        {
112            return DetectionResult::new(
113                AgentStatus::Processing {
114                    activity: title.to_string(),
115                },
116                "default_title_processing",
117                DetectionConfidence::Medium,
118            )
119            .with_matched_text(title);
120        }
121
122        // Content-based heuristics
123        let lines: Vec<&str> = content.lines().collect();
124        if let Some(last) = lines.last() {
125            let trimmed = last.trim();
126            if trimmed.ends_with('>')
127                || trimmed.ends_with('$')
128                || trimmed.ends_with('#')
129                || trimmed.ends_with('❯')
130                || trimmed.ends_with(':')
131                || trimmed.is_empty()
132            {
133                return DetectionResult::new(
134                    AgentStatus::Idle,
135                    "default_prompt_ending",
136                    DetectionConfidence::Medium,
137                );
138            }
139        }
140
141        DetectionResult::new(
142            AgentStatus::Unknown,
143            "default_fallback_unknown",
144            DetectionConfidence::Low,
145        )
146    }
147
148    fn agent_type(&self) -> AgentType {
149        self.agent_type.clone()
150    }
151
152    fn approval_keys(&self) -> &str {
153        "Enter"
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_default_detector() {
163        let detector = DefaultDetector::new(AgentType::OpenCode);
164        assert_eq!(detector.agent_type(), AgentType::OpenCode);
165    }
166
167    #[test]
168    fn test_approval_detection() {
169        let detector = DefaultDetector::new(AgentType::OpenCode);
170        let content = "Do you want to proceed? [y/n]";
171        let status = detector.detect_status("OpenCode", content);
172        assert!(matches!(status, AgentStatus::AwaitingApproval { .. }));
173    }
174
175    #[test]
176    fn test_idle_from_prompt() {
177        let detector = DefaultDetector::new(AgentType::OpenCode);
178        let status = detector.detect_status("OpenCode", "Ready\n> ");
179        assert!(matches!(status, AgentStatus::Idle));
180    }
181}