Skip to main content

tmai_core/api/
queries.rs

1//! Read-only query methods on [`TmaiCore`].
2//!
3//! Every method acquires a read lock internally, converts to owned snapshots,
4//! and releases the lock before returning. Callers never hold a lock.
5
6use super::core::TmaiCore;
7use super::types::{AgentSnapshot, ApiError, TeamSummary, TeamTaskInfo};
8
9impl TmaiCore {
10    // =========================================================
11    // Agent queries
12    // =========================================================
13
14    /// List all monitored agents as owned snapshots, in current display order.
15    pub fn list_agents(&self) -> Vec<AgentSnapshot> {
16        let state = self.state().read();
17        state
18            .agent_order
19            .iter()
20            .filter_map(|id| state.agents.get(id))
21            .map(AgentSnapshot::from_agent)
22            .collect()
23    }
24
25    /// Get a single agent snapshot by target ID.
26    pub fn get_agent(&self, target: &str) -> Result<AgentSnapshot, ApiError> {
27        let state = self.state().read();
28        state
29            .agents
30            .get(target)
31            .map(AgentSnapshot::from_agent)
32            .ok_or_else(|| ApiError::AgentNotFound {
33                target: target.to_string(),
34            })
35    }
36
37    /// Get the currently selected agent snapshot.
38    pub fn selected_agent(&self) -> Result<AgentSnapshot, ApiError> {
39        let state = self.state().read();
40        state
41            .selected_agent()
42            .map(AgentSnapshot::from_agent)
43            .ok_or(ApiError::NoSelection)
44    }
45
46    /// Get the number of agents that need user attention.
47    pub fn attention_count(&self) -> usize {
48        let state = self.state().read();
49        state.attention_count()
50    }
51
52    /// Get the total number of monitored agents.
53    pub fn agent_count(&self) -> usize {
54        let state = self.state().read();
55        state.agents.len()
56    }
57
58    /// List agents that need attention (awaiting approval or error).
59    pub fn agents_needing_attention(&self) -> Vec<AgentSnapshot> {
60        let state = self.state().read();
61        state
62            .agent_order
63            .iter()
64            .filter_map(|id| state.agents.get(id))
65            .filter(|a| a.status.needs_attention())
66            .map(AgentSnapshot::from_agent)
67            .collect()
68    }
69
70    // =========================================================
71    // Preview
72    // =========================================================
73
74    /// Get the ANSI preview content for an agent.
75    pub fn get_preview(&self, target: &str) -> Result<String, ApiError> {
76        let state = self.state().read();
77        state
78            .agents
79            .get(target)
80            .map(|a| a.last_content_ansi.clone())
81            .ok_or_else(|| ApiError::AgentNotFound {
82                target: target.to_string(),
83            })
84    }
85
86    /// Get the plain-text content for an agent.
87    pub fn get_content(&self, target: &str) -> Result<String, ApiError> {
88        let state = self.state().read();
89        state
90            .agents
91            .get(target)
92            .map(|a| a.last_content.clone())
93            .ok_or_else(|| ApiError::AgentNotFound {
94                target: target.to_string(),
95            })
96    }
97
98    // =========================================================
99    // Team queries
100    // =========================================================
101
102    /// List all known teams as owned summaries.
103    pub fn list_teams(&self) -> Vec<TeamSummary> {
104        let state = self.state().read();
105        let mut teams: Vec<TeamSummary> = state
106            .teams
107            .values()
108            .map(TeamSummary::from_snapshot)
109            .collect();
110        teams.sort_by(|a, b| a.name.cmp(&b.name));
111        teams
112    }
113
114    /// Get a single team summary by name.
115    pub fn get_team(&self, name: &str) -> Result<TeamSummary, ApiError> {
116        let state = self.state().read();
117        state
118            .teams
119            .get(name)
120            .map(TeamSummary::from_snapshot)
121            .ok_or_else(|| ApiError::TeamNotFound {
122                name: name.to_string(),
123            })
124    }
125
126    /// Get tasks for a team.
127    pub fn get_team_tasks(&self, name: &str) -> Result<Vec<TeamTaskInfo>, ApiError> {
128        let state = self.state().read();
129        state
130            .teams
131            .get(name)
132            .map(|ts| ts.tasks.iter().map(TeamTaskInfo::from_task).collect())
133            .ok_or_else(|| ApiError::TeamNotFound {
134                name: name.to_string(),
135            })
136    }
137
138    // =========================================================
139    // Miscellaneous queries
140    // =========================================================
141
142    /// Check if the application is still running.
143    pub fn is_running(&self) -> bool {
144        let state = self.state().read();
145        state.running
146    }
147
148    /// Get the last poll timestamp.
149    pub fn last_poll(&self) -> Option<chrono::DateTime<chrono::Utc>> {
150        let state = self.state().read();
151        state.last_poll
152    }
153
154    /// Get known working directories from current agents.
155    pub fn known_directories(&self) -> Vec<String> {
156        let state = self.state().read();
157        state.get_known_directories()
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::agents::{AgentStatus, AgentType, MonitoredAgent};
165    use crate::api::builder::TmaiCoreBuilder;
166    use crate::config::Settings;
167    use crate::state::AppState;
168
169    fn make_core_with_agents(agents: Vec<MonitoredAgent>) -> TmaiCore {
170        let state = AppState::shared();
171        {
172            let mut s = state.write();
173            s.update_agents(agents);
174        }
175        TmaiCoreBuilder::new(Settings::default())
176            .with_state(state)
177            .build()
178    }
179
180    fn test_agent(id: &str, status: AgentStatus) -> MonitoredAgent {
181        let mut agent = MonitoredAgent::new(
182            id.to_string(),
183            AgentType::ClaudeCode,
184            "Title".to_string(),
185            "/home/user".to_string(),
186            100,
187            "main".to_string(),
188            "win".to_string(),
189            0,
190            0,
191        );
192        agent.status = status;
193        agent
194    }
195
196    #[test]
197    fn test_list_agents_empty() {
198        let core = TmaiCoreBuilder::new(Settings::default()).build();
199        assert!(core.list_agents().is_empty());
200    }
201
202    #[test]
203    fn test_list_agents() {
204        let core = make_core_with_agents(vec![
205            test_agent("main:0.0", AgentStatus::Idle),
206            test_agent(
207                "main:0.1",
208                AgentStatus::Processing {
209                    activity: "Bash".to_string(),
210                },
211            ),
212        ]);
213
214        let agents = core.list_agents();
215        assert_eq!(agents.len(), 2);
216    }
217
218    #[test]
219    fn test_get_agent_found() {
220        let core = make_core_with_agents(vec![test_agent("main:0.0", AgentStatus::Idle)]);
221
222        let result = core.get_agent("main:0.0");
223        assert!(result.is_ok());
224        assert_eq!(result.unwrap().id, "main:0.0");
225    }
226
227    #[test]
228    fn test_get_agent_not_found() {
229        let core = TmaiCoreBuilder::new(Settings::default()).build();
230        let result = core.get_agent("nonexistent");
231        assert!(matches!(result, Err(ApiError::AgentNotFound { .. })));
232    }
233
234    #[test]
235    fn test_attention_count() {
236        let core = make_core_with_agents(vec![
237            test_agent("main:0.0", AgentStatus::Idle),
238            test_agent(
239                "main:0.1",
240                AgentStatus::AwaitingApproval {
241                    approval_type: crate::agents::ApprovalType::ShellCommand,
242                    details: "rm -rf".to_string(),
243                },
244            ),
245            test_agent(
246                "main:0.2",
247                AgentStatus::Error {
248                    message: "oops".to_string(),
249                },
250            ),
251        ]);
252
253        assert_eq!(core.attention_count(), 2);
254        assert_eq!(core.agent_count(), 3);
255    }
256
257    #[test]
258    fn test_agents_needing_attention() {
259        let core = make_core_with_agents(vec![
260            test_agent("main:0.0", AgentStatus::Idle),
261            test_agent(
262                "main:0.1",
263                AgentStatus::AwaitingApproval {
264                    approval_type: crate::agents::ApprovalType::FileEdit,
265                    details: String::new(),
266                },
267            ),
268        ]);
269
270        let attention = core.agents_needing_attention();
271        assert_eq!(attention.len(), 1);
272        assert_eq!(attention[0].id, "main:0.1");
273    }
274
275    #[test]
276    fn test_get_preview() {
277        let mut agent = test_agent("main:0.0", AgentStatus::Idle);
278        agent.last_content_ansi = "\x1b[32mHello\x1b[0m".to_string();
279        agent.last_content = "Hello".to_string();
280
281        let core = make_core_with_agents(vec![agent]);
282
283        let preview = core.get_preview("main:0.0").unwrap();
284        assert!(preview.contains("Hello"));
285
286        let content = core.get_content("main:0.0").unwrap();
287        assert_eq!(content, "Hello");
288    }
289
290    #[test]
291    fn test_list_teams_empty() {
292        let core = TmaiCoreBuilder::new(Settings::default()).build();
293        assert!(core.list_teams().is_empty());
294    }
295
296    #[test]
297    fn test_is_running() {
298        let core = TmaiCoreBuilder::new(Settings::default()).build();
299        assert!(core.is_running());
300    }
301}