1use super::core::TmaiCore;
7use super::types::{AgentDefinitionInfo, AgentSnapshot, ApiError, TeamSummary, TeamTaskInfo};
8
9impl TmaiCore {
10 pub fn list_agents(&self) -> Vec<AgentSnapshot> {
16 let state = self.state().read();
17 let defs = &state.agent_definitions;
18 state
19 .agent_order
20 .iter()
21 .filter_map(|id| state.agents.get(id))
22 .map(|a| {
23 let mut snap = AgentSnapshot::from_agent(a);
24 snap.agent_definition = Self::match_agent_definition(a, defs);
25 snap
26 })
27 .collect()
28 }
29
30 pub fn get_agent(&self, target: &str) -> Result<AgentSnapshot, ApiError> {
32 let state = self.state().read();
33 let defs = &state.agent_definitions;
34 state
35 .agents
36 .get(target)
37 .map(|a| {
38 let mut snap = AgentSnapshot::from_agent(a);
39 snap.agent_definition = Self::match_agent_definition(a, defs);
40 snap
41 })
42 .ok_or_else(|| ApiError::AgentNotFound {
43 target: target.to_string(),
44 })
45 }
46
47 pub fn selected_agent(&self) -> Result<AgentSnapshot, ApiError> {
49 let state = self.state().read();
50 let defs = &state.agent_definitions;
51 state
52 .selected_agent()
53 .map(|agent| {
54 let mut snapshot = AgentSnapshot::from_agent(agent);
55 snapshot.agent_definition = Self::match_agent_definition(agent, defs);
56 snapshot
57 })
58 .ok_or(ApiError::NoSelection)
59 }
60
61 pub fn attention_count(&self) -> usize {
63 let state = self.state().read();
64 state.attention_count()
65 }
66
67 pub fn agent_count(&self) -> usize {
69 let state = self.state().read();
70 state.agents.len()
71 }
72
73 pub fn agents_needing_attention(&self) -> Vec<AgentSnapshot> {
75 let state = self.state().read();
76 state
77 .agent_order
78 .iter()
79 .filter_map(|id| state.agents.get(id))
80 .filter(|a| a.status.needs_attention())
81 .map(AgentSnapshot::from_agent)
82 .collect()
83 }
84
85 pub fn get_preview(&self, target: &str) -> Result<String, ApiError> {
91 let state = self.state().read();
92 state
93 .agents
94 .get(target)
95 .map(|a| a.last_content_ansi.clone())
96 .ok_or_else(|| ApiError::AgentNotFound {
97 target: target.to_string(),
98 })
99 }
100
101 pub fn get_content(&self, target: &str) -> Result<String, ApiError> {
103 let state = self.state().read();
104 state
105 .agents
106 .get(target)
107 .map(|a| a.last_content.clone())
108 .ok_or_else(|| ApiError::AgentNotFound {
109 target: target.to_string(),
110 })
111 }
112
113 pub fn get_transcript(
122 &self,
123 target: &str,
124 ) -> Result<Vec<crate::transcript::TranscriptRecord>, ApiError> {
125 let pane_id = {
127 let state = self.state().read();
128 let agent = state
129 .agents
130 .get(target)
131 .ok_or_else(|| ApiError::AgentNotFound {
132 target: target.to_string(),
133 })?;
134 state
136 .target_to_pane_id
137 .get(&agent.id)
138 .cloned()
139 .unwrap_or_else(|| agent.id.clone())
140 };
141
142 let registry = match self.transcript_registry() {
144 Some(reg) => reg,
145 None => return Ok(Vec::new()),
146 };
147
148 let reg = registry.read();
149 Ok(reg
150 .get(&pane_id)
151 .map(|state| state.recent_records.clone())
152 .unwrap_or_default())
153 }
154
155 pub fn list_teams(&self) -> Vec<TeamSummary> {
161 let state = self.state().read();
162 let mut teams: Vec<TeamSummary> = state
163 .teams
164 .values()
165 .map(TeamSummary::from_snapshot)
166 .collect();
167 teams.sort_by(|a, b| a.name.cmp(&b.name));
168 teams
169 }
170
171 pub fn get_team(&self, name: &str) -> Result<TeamSummary, ApiError> {
173 let state = self.state().read();
174 state
175 .teams
176 .get(name)
177 .map(TeamSummary::from_snapshot)
178 .ok_or_else(|| ApiError::TeamNotFound {
179 name: name.to_string(),
180 })
181 }
182
183 pub fn get_team_tasks(&self, name: &str) -> Result<Vec<TeamTaskInfo>, ApiError> {
185 let state = self.state().read();
186 state
187 .teams
188 .get(name)
189 .map(|ts| ts.tasks.iter().map(TeamTaskInfo::from_task).collect())
190 .ok_or_else(|| ApiError::TeamNotFound {
191 name: name.to_string(),
192 })
193 }
194
195 pub fn config_audit(&self) -> crate::security::ScanResult {
204 let dirs: Vec<std::path::PathBuf> = {
206 let state = self.state().read();
207 state
208 .agents
209 .values()
210 .map(|a| std::path::PathBuf::from(&a.cwd))
211 .collect()
212 };
213
214 let result = crate::security::ConfigAuditScanner::scan(&dirs);
216
217 {
219 let mut state = self.state().write();
220 state.config_audit = Some(result.clone());
221 }
222
223 result
224 }
225
226 pub fn last_config_audit(&self) -> Option<crate::security::ScanResult> {
228 let state = self.state().read();
229 state.config_audit.clone()
230 }
231
232 fn match_agent_definition(
238 agent: &crate::agents::MonitoredAgent,
239 defs: &[crate::teams::AgentDefinition],
240 ) -> Option<AgentDefinitionInfo> {
241 if defs.is_empty() {
242 return None;
243 }
244 if let Some(ref team_info) = agent.team_info {
245 if let Some(ref agent_type) = team_info.agent_type {
247 if let Some(def) = defs.iter().find(|d| d.name == *agent_type) {
248 return Some(AgentDefinitionInfo::from_definition(def));
249 }
250 }
251 if let Some(def) = defs.iter().find(|d| d.name == team_info.member_name) {
253 return Some(AgentDefinitionInfo::from_definition(def));
254 }
255 }
256 None
257 }
258
259 pub fn is_running(&self) -> bool {
261 let state = self.state().read();
262 state.running
263 }
264
265 pub fn last_poll(&self) -> Option<chrono::DateTime<chrono::Utc>> {
267 let state = self.state().read();
268 state.last_poll
269 }
270
271 pub fn known_directories(&self) -> Vec<String> {
273 let state = self.state().read();
274 state.get_known_directories()
275 }
276
277 pub fn list_projects(&self) -> Vec<String> {
283 let state = self.state().read();
284 state.registered_projects.clone()
285 }
286
287 pub fn add_project(&self, path: &str) -> Result<(), ApiError> {
289 let canonical = std::path::Path::new(path);
290 if !canonical.is_absolute() {
291 return Err(ApiError::InvalidInput {
292 message: "Project path must be absolute".to_string(),
293 });
294 }
295 if !canonical.is_dir() {
296 return Err(ApiError::InvalidInput {
297 message: format!("Directory does not exist: {}", path),
298 });
299 }
300 let canonical_str = canonical.to_string_lossy().to_string();
301
302 let mut state = self.state().write();
303 if state.registered_projects.contains(&canonical_str) {
304 return Ok(()); }
306 state.registered_projects.push(canonical_str);
307 let projects = state.registered_projects.clone();
308 drop(state);
309
310 crate::config::Settings::save_projects(&projects);
311 Ok(())
312 }
313
314 pub fn remove_project(&self, path: &str) -> Result<(), ApiError> {
316 let mut state = self.state().write();
317 let before = state.registered_projects.len();
318 state.registered_projects.retain(|p| p != path);
319 if state.registered_projects.len() == before {
320 return Err(ApiError::InvalidInput {
321 message: format!("Project not found: {}", path),
322 });
323 }
324 let projects = state.registered_projects.clone();
325 drop(state);
326
327 crate::config::Settings::save_projects(&projects);
328 Ok(())
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::agents::{AgentStatus, AgentType, MonitoredAgent};
336 use crate::api::builder::TmaiCoreBuilder;
337 use crate::config::Settings;
338 use crate::state::AppState;
339
340 fn make_core_with_agents(agents: Vec<MonitoredAgent>) -> TmaiCore {
341 let state = AppState::shared();
342 {
343 let mut s = state.write();
344 s.update_agents(agents);
345 }
346 TmaiCoreBuilder::new(Settings::default())
347 .with_state(state)
348 .build()
349 }
350
351 fn test_agent(id: &str, status: AgentStatus) -> MonitoredAgent {
352 let mut agent = MonitoredAgent::new(
353 id.to_string(),
354 AgentType::ClaudeCode,
355 "Title".to_string(),
356 "/home/user".to_string(),
357 100,
358 "main".to_string(),
359 "win".to_string(),
360 0,
361 0,
362 );
363 agent.status = status;
364 agent
365 }
366
367 #[test]
368 fn test_list_agents_empty() {
369 let core = TmaiCoreBuilder::new(Settings::default()).build();
370 assert!(core.list_agents().is_empty());
371 }
372
373 #[test]
374 fn test_list_agents() {
375 let core = make_core_with_agents(vec![
376 test_agent("main:0.0", AgentStatus::Idle),
377 test_agent(
378 "main:0.1",
379 AgentStatus::Processing {
380 activity: "Bash".to_string(),
381 },
382 ),
383 ]);
384
385 let agents = core.list_agents();
386 assert_eq!(agents.len(), 2);
387 }
388
389 #[test]
390 fn test_get_agent_found() {
391 let core = make_core_with_agents(vec![test_agent("main:0.0", AgentStatus::Idle)]);
392
393 let result = core.get_agent("main:0.0");
394 assert!(result.is_ok());
395 assert_eq!(result.unwrap().id, "main:0.0");
396 }
397
398 #[test]
399 fn test_get_agent_not_found() {
400 let core = TmaiCoreBuilder::new(Settings::default()).build();
401 let result = core.get_agent("nonexistent");
402 assert!(matches!(result, Err(ApiError::AgentNotFound { .. })));
403 }
404
405 #[test]
406 fn test_attention_count() {
407 let core = make_core_with_agents(vec![
408 test_agent("main:0.0", AgentStatus::Idle),
409 test_agent(
410 "main:0.1",
411 AgentStatus::AwaitingApproval {
412 approval_type: crate::agents::ApprovalType::ShellCommand,
413 details: "rm -rf".to_string(),
414 },
415 ),
416 test_agent(
417 "main:0.2",
418 AgentStatus::Error {
419 message: "oops".to_string(),
420 },
421 ),
422 ]);
423
424 assert_eq!(core.attention_count(), 2);
425 assert_eq!(core.agent_count(), 3);
426 }
427
428 #[test]
429 fn test_agents_needing_attention() {
430 let core = make_core_with_agents(vec![
431 test_agent("main:0.0", AgentStatus::Idle),
432 test_agent(
433 "main:0.1",
434 AgentStatus::AwaitingApproval {
435 approval_type: crate::agents::ApprovalType::FileEdit,
436 details: String::new(),
437 },
438 ),
439 ]);
440
441 let attention = core.agents_needing_attention();
442 assert_eq!(attention.len(), 1);
443 assert_eq!(attention[0].id, "main:0.1");
444 }
445
446 #[test]
447 fn test_get_preview() {
448 let mut agent = test_agent("main:0.0", AgentStatus::Idle);
449 agent.last_content_ansi = "\x1b[32mHello\x1b[0m".to_string();
450 agent.last_content = "Hello".to_string();
451
452 let core = make_core_with_agents(vec![agent]);
453
454 let preview = core.get_preview("main:0.0").unwrap();
455 assert!(preview.contains("Hello"));
456
457 let content = core.get_content("main:0.0").unwrap();
458 assert_eq!(content, "Hello");
459 }
460
461 #[test]
462 fn test_list_teams_empty() {
463 let core = TmaiCoreBuilder::new(Settings::default()).build();
464 assert!(core.list_teams().is_empty());
465 }
466
467 #[test]
468 fn test_is_running() {
469 let core = TmaiCoreBuilder::new(Settings::default()).build();
470 assert!(core.is_running());
471 }
472
473 #[test]
474 fn test_get_transcript_no_registry() {
475 let core = make_core_with_agents(vec![test_agent("main:0.0", AgentStatus::Idle)]);
477 let records = core.get_transcript("main:0.0").unwrap();
478 assert!(records.is_empty());
479 }
480
481 #[test]
482 fn test_get_transcript_agent_not_found() {
483 let core = TmaiCoreBuilder::new(Settings::default()).build();
484 let result = core.get_transcript("nonexistent");
485 assert!(matches!(result, Err(ApiError::AgentNotFound { .. })));
486 }
487
488 #[test]
489 fn test_get_transcript_with_registry() {
490 use crate::transcript::types::TranscriptRecord;
491 use crate::transcript::watcher::new_transcript_registry;
492
493 let registry = new_transcript_registry();
494 {
496 let mut reg = registry.write();
497 let mut state = crate::transcript::TranscriptState::new(
498 "/tmp/test.jsonl".to_string(),
499 "sess1".to_string(),
500 "main:0.0".to_string(),
501 );
502 state.push_records(vec![
503 TranscriptRecord::User {
504 text: "Hello".to_string(),
505 },
506 TranscriptRecord::AssistantText {
507 text: "Hi there".to_string(),
508 },
509 ]);
510 reg.insert("main:0.0".to_string(), state);
511 }
512
513 let app_state = AppState::shared();
514 {
515 let mut s = app_state.write();
516 s.update_agents(vec![test_agent("main:0.0", AgentStatus::Idle)]);
517 }
518
519 let core = TmaiCoreBuilder::new(Settings::default())
520 .with_state(app_state)
521 .with_transcript_registry(registry)
522 .build();
523
524 let records = core.get_transcript("main:0.0").unwrap();
525 assert_eq!(records.len(), 2);
526 }
527}