Skip to main content

tmai_core/api/
builder.rs

1//! Builder for constructing a [`TmaiCore`] instance.
2//!
3//! ```ignore
4//! let core = TmaiCoreBuilder::new(settings)
5//!     .with_state(state)
6//!     .with_ipc_server(ipc)
7//!     .with_command_sender(cmd)
8//!     .build();
9//! ```
10
11use std::sync::Arc;
12
13use crate::audit::AuditEventSender;
14use crate::auto_approve::defer::DeferRegistry;
15use crate::command_sender::CommandSender;
16use crate::config::Settings;
17use crate::hooks::registry::{HookRegistry, SessionPaneMap};
18use crate::ipc::server::IpcServer;
19use crate::pty::PtyRegistry;
20use crate::runtime::RuntimeAdapter;
21use crate::state::{AppState, SharedState};
22use crate::transcript::TranscriptRegistry;
23
24use super::core::TmaiCore;
25
26/// Builder for constructing a [`TmaiCore`] Facade instance
27pub struct TmaiCoreBuilder {
28    settings: Arc<Settings>,
29    state: Option<SharedState>,
30    command_sender: Option<Arc<CommandSender>>,
31    ipc_server: Option<Arc<IpcServer>>,
32    audit_tx: Option<AuditEventSender>,
33    hook_registry: Option<HookRegistry>,
34    session_pane_map: Option<SessionPaneMap>,
35    hook_token: Option<String>,
36    pty_registry: Option<Arc<PtyRegistry>>,
37    runtime: Option<Arc<dyn RuntimeAdapter>>,
38    transcript_registry: Option<TranscriptRegistry>,
39}
40
41impl TmaiCoreBuilder {
42    /// Create a new builder with the given settings
43    pub fn new(settings: Settings) -> Self {
44        Self {
45            settings: Arc::new(settings),
46            state: None,
47            command_sender: None,
48            ipc_server: None,
49            audit_tx: None,
50            hook_registry: None,
51            session_pane_map: None,
52            hook_token: None,
53            pty_registry: None,
54            runtime: None,
55            transcript_registry: None,
56        }
57    }
58
59    /// Create a new builder from an already-shared settings
60    pub fn from_shared_settings(settings: Arc<Settings>) -> Self {
61        Self {
62            settings,
63            state: None,
64            command_sender: None,
65            ipc_server: None,
66            audit_tx: None,
67            hook_registry: None,
68            session_pane_map: None,
69            hook_token: None,
70            pty_registry: None,
71            runtime: None,
72            transcript_registry: None,
73        }
74    }
75
76    /// Use an existing shared state instead of creating a new one
77    pub fn with_state(mut self, state: SharedState) -> Self {
78        self.state = Some(state);
79        self
80    }
81
82    /// Set the IPC server for PTY wrapper communication
83    pub fn with_ipc_server(mut self, ipc_server: Arc<IpcServer>) -> Self {
84        self.ipc_server = Some(ipc_server);
85        self
86    }
87
88    /// Set the command sender
89    pub fn with_command_sender(mut self, sender: Arc<CommandSender>) -> Self {
90        self.command_sender = Some(sender);
91        self
92    }
93
94    /// Set the audit event sender for emitting audit events
95    pub fn with_audit_sender(mut self, tx: AuditEventSender) -> Self {
96        self.audit_tx = Some(tx);
97        self
98    }
99
100    /// Set the hook registry for HTTP hook-based agent state
101    pub fn with_hook_registry(mut self, registry: HookRegistry) -> Self {
102        self.hook_registry = Some(registry);
103        self
104    }
105
106    /// Set the session → pane ID mapping for hook event routing
107    pub fn with_session_pane_map(mut self, map: SessionPaneMap) -> Self {
108        self.session_pane_map = Some(map);
109        self
110    }
111
112    /// Set the authentication token for hook endpoints
113    pub fn with_hook_token(mut self, token: String) -> Self {
114        self.hook_token = Some(token);
115        self
116    }
117
118    /// Set the PTY session registry for spawned agents
119    pub fn with_pty_registry(mut self, registry: Arc<PtyRegistry>) -> Self {
120        self.pty_registry = Some(registry);
121        self
122    }
123
124    /// Set the runtime adapter (tmux, standalone, etc.)
125    pub fn with_runtime(mut self, runtime: Arc<dyn RuntimeAdapter>) -> Self {
126        self.runtime = Some(runtime);
127        self
128    }
129
130    /// Set the transcript registry for JSONL conversation log monitoring
131    pub fn with_transcript_registry(mut self, registry: TranscriptRegistry) -> Self {
132        self.transcript_registry = Some(registry);
133        self
134    }
135
136    /// Build the `TmaiCore` instance
137    ///
138    /// If no state was provided, a fresh `AppState::shared()` is created.
139    /// If no hook registry/session_pane_map was provided, empty ones are created.
140    pub fn build(self) -> TmaiCore {
141        let state = self.state.unwrap_or_else(AppState::shared);
142        let hook_registry = self
143            .hook_registry
144            .unwrap_or_else(crate::hooks::new_hook_registry);
145        let session_pane_map = self
146            .session_pane_map
147            .unwrap_or_else(crate::hooks::new_session_pane_map);
148
149        let pty_registry = self
150            .pty_registry
151            .unwrap_or_else(|| Arc::new(PtyRegistry::default()));
152
153        TmaiCore::new(
154            state,
155            self.command_sender,
156            self.settings,
157            self.ipc_server,
158            self.audit_tx,
159            hook_registry,
160            session_pane_map,
161            self.hook_token,
162            pty_registry,
163            self.runtime,
164            self.transcript_registry,
165            DeferRegistry::new(),
166        )
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_builder_defaults() {
176        let core = TmaiCoreBuilder::new(Settings::default()).build();
177
178        assert_eq!(core.settings().poll_interval_ms, 500);
179        assert!(core.ipc_server().is_none());
180        assert!(core.command_sender_ref().is_none());
181    }
182
183    #[test]
184    fn test_builder_with_state() {
185        let state = AppState::shared();
186        let state_clone = state.clone();
187
188        let core = TmaiCoreBuilder::new(Settings::default())
189            .with_state(state)
190            .build();
191
192        #[allow(deprecated)]
193        let raw = core.raw_state();
194        assert!(Arc::ptr_eq(raw, &state_clone));
195    }
196
197    #[test]
198    fn test_builder_from_shared_settings() {
199        let settings = Arc::new(Settings::default());
200        let settings_clone = settings.clone();
201
202        let core = TmaiCoreBuilder::from_shared_settings(settings).build();
203
204        // Settings should be the same Arc
205        assert_eq!(
206            core.settings().poll_interval_ms,
207            settings_clone.poll_interval_ms
208        );
209    }
210}