Skip to main content

agent_engine/extensions/
loader.rs

1//! Async extension loading orchestration.
2//!
3//! The chat UI owns the manager behind an async lock; this module keeps startup
4//! snappy by running discovery/loading in the background and streaming progress
5//! events back to the UI (which can render them as toasts).
6
7use std::sync::Arc;
8
9use tokio::sync::{mpsc, RwLock};
10
11use super::manager::{ExtensionLoadFailure, ExtensionManager};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ExtensionLoaderEvent {
15    Started,
16    Loaded {
17        plugin: String,
18        loaded: usize,
19        failed: usize,
20    },
21    Failed {
22        failure: ExtensionLoadFailure,
23        loaded: usize,
24        failed: usize,
25    },
26    Finished {
27        loaded: Vec<String>,
28        failed: Vec<ExtensionLoadFailure>,
29    },
30}
31
32impl ExtensionLoaderEvent {
33    pub fn progress_counts(&self) -> Option<(usize, usize)> {
34        match self {
35            ExtensionLoaderEvent::Started => Some((0, 0)),
36            ExtensionLoaderEvent::Loaded { loaded, failed, .. } => Some((*loaded, *failed)),
37            ExtensionLoaderEvent::Failed { loaded, failed, .. } => Some((*loaded, *failed)),
38            ExtensionLoaderEvent::Finished { loaded, failed } => Some((loaded.len(), failed.len())),
39        }
40    }
41}
42
43pub fn spawn_discover_and_load(
44    manager: Arc<RwLock<ExtensionManager>>,
45    tx: mpsc::UnboundedSender<ExtensionLoaderEvent>,
46) -> tokio::task::JoinHandle<()> {
47    tokio::spawn(async move {
48        let _ = tx.send(ExtensionLoaderEvent::Started);
49        let (loaded, failed) = manager
50            .write()
51            .await
52            .discover_and_load_with_progress(|event| {
53                let _ = tx.send(event);
54            })
55            .await;
56        let _ = tx.send(ExtensionLoaderEvent::Finished { loaded, failed });
57    })
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn progress_counts_report_finished_totals() {
66        let event = ExtensionLoaderEvent::Finished {
67            loaded: vec!["a".into(), "b".into()],
68            failed: vec![ExtensionLoadFailure {
69                plugin: "bad".into(),
70                manifest_path: None,
71                reason: "oops".into(),
72                hint: "fix it".into(),
73            }],
74        };
75        assert_eq!(event.progress_counts(), Some((2, 1)));
76    }
77}