ricecoder_tui/
session_integration.rs

1//! Integration between ricecoder-sessions and TUI session widgets
2//!
3//! This module bridges the core session management (ricecoder-sessions) with
4//! the TUI session display widgets, ensuring session status is properly displayed
5//! and session switching is routed correctly.
6
7use crate::sessions::{Session as TuiSession, SessionStatus as TuiSessionStatus, SessionWidget};
8use ricecoder_sessions::{Session, SessionManager, SessionStatus as CoreSessionStatus};
9
10/// Integrates core session management with TUI display
11pub struct SessionIntegration {
12    /// Core session manager (single source of truth)
13    pub manager: SessionManager,
14    /// TUI session widget for display
15    pub widget: SessionWidget,
16}
17
18impl SessionIntegration {
19    /// Create a new session integration with a session limit
20    pub fn new(session_limit: usize) -> Self {
21        Self {
22            manager: SessionManager::new(session_limit),
23            widget: SessionWidget::new(),
24        }
25    }
26
27    /// Sync core sessions to TUI widget for display
28    /// This should be called whenever sessions change to keep the UI in sync
29    pub fn sync_to_widget(&mut self) {
30        // Get all sessions from the core manager
31        let sessions = self.manager.list_sessions();
32
33        // Clear the widget and rebuild it
34        self.widget.clear();
35
36        for session in sessions {
37            let tui_session = self.convert_to_tui_session(&session);
38            self.widget.add_session(tui_session);
39        }
40
41        // Set the selected index to match the active session
42        if let Some(active_id) = self.manager.active_session_id() {
43            if let Some(index) = self.widget.find_session_index(active_id) {
44                self.widget.switch_to_session(index);
45            }
46        }
47    }
48
49    /// Convert a core Session to a TUI Session for display
50    fn convert_to_tui_session(&self, session: &Session) -> TuiSession {
51        let tui_status = match session.status {
52            CoreSessionStatus::Active => TuiSessionStatus::Active,
53            CoreSessionStatus::Paused => TuiSessionStatus::Idle,
54            CoreSessionStatus::Archived => TuiSessionStatus::Idle,
55        };
56
57        let mut tui_session = TuiSession::new(session.id.clone(), session.name.clone());
58        tui_session.set_status(tui_status);
59        tui_session.message_count = session.history.len();
60        tui_session.last_activity = session.updated_at.timestamp().max(0) as u64;
61
62        tui_session
63    }
64
65    /// Handle session switching from the TUI widget
66    /// Updates the core manager to maintain consistency
67    pub fn handle_session_switch(&mut self, session_id: &str) -> Result<(), String> {
68        // Switch in the core manager
69        self.manager
70            .switch_session(session_id)
71            .map_err(|e| e.to_string())?;
72
73        // Sync the widget to reflect the change
74        self.sync_to_widget();
75
76        Ok(())
77    }
78
79    /// Create a new session and add it to the manager
80    pub fn create_session(
81        &mut self,
82        name: String,
83        context: ricecoder_sessions::SessionContext,
84    ) -> Result<String, String> {
85        // Create in the core manager
86        let session = self
87            .manager
88            .create_session(name.clone(), context.clone())
89            .map_err(|e| e.to_string())?;
90
91        let session_id = session.id.clone();
92
93        // Sync the widget to reflect the change
94        self.sync_to_widget();
95
96        Ok(session_id)
97    }
98
99    /// Delete a session from the manager
100    pub fn delete_session(&mut self, session_id: &str) -> Result<(), String> {
101        // Delete from the core manager
102        self.manager
103            .delete_session(session_id)
104            .map_err(|e| e.to_string())?;
105
106        // Sync the widget to reflect the change
107        self.sync_to_widget();
108
109        Ok(())
110    }
111
112    /// Get the currently active session ID
113    pub fn active_session_id(&self) -> Option<&str> {
114        self.manager.active_session_id()
115    }
116
117    /// Get the number of sessions
118    pub fn session_count(&self) -> usize {
119        self.manager.session_count()
120    }
121
122    /// Check if the session limit is reached
123    pub fn is_limit_reached(&self) -> bool {
124        self.manager.is_limit_reached()
125    }
126
127    /// Get all sessions
128    pub fn list_sessions(&self) -> Vec<Session> {
129        self.manager.list_sessions()
130    }
131
132    /// Get a specific session
133    pub fn get_session(&self, session_id: &str) -> Result<Session, String> {
134        self.manager
135            .get_session(session_id)
136            .map_err(|e| e.to_string())
137    }
138
139    /// Get the active session
140    pub fn get_active_session(&self) -> Result<Session, String> {
141        self.manager.get_active_session().map_err(|e| e.to_string())
142    }
143
144    /// Add a message to the active session
145    pub fn add_message_to_active(&mut self, message_content: &str) -> Result<String, String> {
146        // Get the active session
147        let mut session = self
148            .manager
149            .get_active_session()
150            .map_err(|e| e.to_string())?;
151
152        let session_id = session.id.clone();
153
154        // Add the message to the session
155        let message = ricecoder_sessions::Message::new(
156            ricecoder_sessions::MessageRole::User,
157            message_content.to_string(),
158        );
159        session.history.push(message);
160        session.updated_at = chrono::Utc::now();
161
162        // Update the session in the manager
163        self.manager
164            .update_session(session)
165            .map_err(|e| e.to_string())?;
166
167        Ok(session_id)
168    }
169
170    /// Add a message to a specific session
171    pub fn add_message_to_session(
172        &mut self,
173        session_id: &str,
174        message_content: &str,
175    ) -> Result<String, String> {
176        // Get the session
177        let mut session = self
178            .manager
179            .get_session(session_id)
180            .map_err(|e| e.to_string())?;
181
182        // Add the message to the session
183        let message = ricecoder_sessions::Message::new(
184            ricecoder_sessions::MessageRole::User,
185            message_content.to_string(),
186        );
187        session.history.push(message);
188        session.updated_at = chrono::Utc::now();
189
190        // Update the session in the manager
191        self.manager
192            .update_session(session)
193            .map_err(|e| e.to_string())?;
194
195        Ok(session_id.to_string())
196    }
197
198    /// Get the TUI widget reference
199    pub fn widget(&self) -> &SessionWidget {
200        &self.widget
201    }
202
203    /// Get mutable TUI widget reference
204    pub fn widget_mut(&mut self) -> &mut SessionWidget {
205        &mut self.widget
206    }
207}
208
209impl Default for SessionIntegration {
210    fn default() -> Self {
211        Self::new(10) // Default to 10 concurrent sessions
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use ricecoder_sessions::SessionMode;
219
220    fn create_test_context() -> ricecoder_sessions::SessionContext {
221        ricecoder_sessions::SessionContext::new(
222            "openai".to_string(),
223            "gpt-4".to_string(),
224            SessionMode::Chat,
225        )
226    }
227
228    #[test]
229    fn test_create_session_integration() {
230        let mut integration = SessionIntegration::new(5);
231        let context = create_test_context();
232
233        let session_id = integration
234            .create_session("Test Session".to_string(), context)
235            .unwrap();
236
237        assert_eq!(integration.session_count(), 1);
238        assert_eq!(integration.active_session_id(), Some(session_id.as_str()));
239        assert_eq!(integration.widget.session_count(), 1);
240    }
241
242    #[test]
243    fn test_sync_to_widget() {
244        let mut integration = SessionIntegration::new(5);
245        let context = create_test_context();
246
247        integration
248            .create_session("Session 1".to_string(), context.clone())
249            .unwrap();
250        integration
251            .create_session("Session 2".to_string(), context)
252            .unwrap();
253
254        assert_eq!(integration.widget.session_count(), 2);
255
256        let widget_sessions = integration.widget.session_names();
257        assert!(widget_sessions.contains(&"Session 1"));
258        assert!(widget_sessions.contains(&"Session 2"));
259    }
260
261    #[test]
262    fn test_session_switch_integration() {
263        let mut integration = SessionIntegration::new(5);
264        let context = create_test_context();
265
266        let session1_id = integration
267            .create_session("Session 1".to_string(), context.clone())
268            .unwrap();
269        let session2_id = integration
270            .create_session("Session 2".to_string(), context)
271            .unwrap();
272
273        // Switch to session 1
274        integration.handle_session_switch(&session1_id).unwrap();
275
276        assert_eq!(integration.active_session_id(), Some(session1_id.as_str()));
277        assert_eq!(
278            integration.widget.current_session().unwrap().id,
279            session1_id
280        );
281
282        // Switch to session 2
283        integration.handle_session_switch(&session2_id).unwrap();
284
285        assert_eq!(integration.active_session_id(), Some(session2_id.as_str()));
286        assert_eq!(
287            integration.widget.current_session().unwrap().id,
288            session2_id
289        );
290    }
291
292    #[test]
293    fn test_delete_session_integration() {
294        let mut integration = SessionIntegration::new(5);
295        let context = create_test_context();
296
297        let session_id = integration
298            .create_session("Test Session".to_string(), context)
299            .unwrap();
300
301        assert_eq!(integration.session_count(), 1);
302
303        integration.delete_session(&session_id).unwrap();
304
305        assert_eq!(integration.session_count(), 0);
306        assert_eq!(integration.widget.session_count(), 0);
307    }
308
309    #[test]
310    fn test_session_status_display() {
311        let mut integration = SessionIntegration::new(5);
312        let context = create_test_context();
313
314        integration
315            .create_session("Test Session".to_string(), context)
316            .unwrap();
317
318        let tui_session = integration.widget.current_session().unwrap();
319        assert_eq!(tui_session.status, TuiSessionStatus::Active);
320    }
321
322    #[test]
323    fn test_message_routing() {
324        let mut integration = SessionIntegration::new(5);
325        let context = create_test_context();
326
327        let session_id = integration
328            .create_session("Test Session".to_string(), context)
329            .unwrap();
330
331        let routed_id = integration.add_message_to_active("Hello").unwrap();
332
333        assert_eq!(routed_id, session_id);
334
335        let session = integration.get_session(&session_id).unwrap();
336        assert_eq!(session.history.len(), 1);
337        assert_eq!(session.history[0].content, "Hello");
338    }
339
340    #[test]
341    fn test_session_limit_enforcement() {
342        let mut integration = SessionIntegration::new(2);
343        let context = create_test_context();
344
345        integration
346            .create_session("Session 1".to_string(), context.clone())
347            .unwrap();
348        integration
349            .create_session("Session 2".to_string(), context.clone())
350            .unwrap();
351
352        assert!(integration.is_limit_reached());
353
354        let result = integration.create_session("Session 3".to_string(), context);
355        assert!(result.is_err());
356    }
357}