tmai_core/detectors/
default.rs1use regex::Regex;
2
3use crate::agents::{AgentStatus, AgentType, ApprovalType};
4
5use super::{DetectionConfidence, DetectionContext, DetectionResult, StatusDetector};
6
7pub 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 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 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 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 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}