Skip to main content

limit_cli/tui/commands/
session.rs

1//! Session management commands
2//!
3//! Handles /session list, /session new, /session load
4
5use super::{Command, CommandContext, CommandResult};
6use crate::error::CliError;
7
8/// Session command - manages sessions
9pub struct SessionCommand {
10    // No state needed - all context is in CommandContext
11}
12
13impl SessionCommand {
14    pub fn new() -> Self {
15        Self {}
16    }
17
18    fn handle_list(&self, ctx: &CommandContext) -> Result<CommandResult, CliError> {
19        let session_manager = ctx.session_manager.lock().unwrap();
20        let current_session_id = ctx.session_id.clone();
21
22        match session_manager.list_sessions() {
23            Ok(sessions) => {
24                if sessions.is_empty() {
25                    ctx.add_system_message("No sessions found.".to_string());
26                } else {
27                    let mut output = vec!["Sessions (most recent first):".to_string()];
28                    for (i, session) in sessions.iter().enumerate() {
29                        let current = if session.id == current_session_id {
30                            " (current)"
31                        } else {
32                            ""
33                        };
34                        let short_id = if session.id.len() > 8 {
35                            &session.id[..8]
36                        } else {
37                            &session.id
38                        };
39                        output.push(format!(
40                            "  {}. {}{} - {} messages, {} in tokens, {} out tokens",
41                            i + 1,
42                            short_id,
43                            current,
44                            session.message_count,
45                            session.total_input_tokens,
46                            session.total_output_tokens
47                        ));
48                    }
49                    ctx.add_system_message(output.join("\n"));
50                }
51            }
52            Err(e) => {
53                ctx.add_system_message(format!("Error listing sessions: {}", e));
54            }
55        }
56
57        Ok(CommandResult::Continue)
58    }
59
60    fn handle_new(&self, ctx: &mut CommandContext) -> Result<CommandResult, CliError> {
61        // Save current session first
62        let save_result = {
63            let session_manager = ctx.session_manager.lock().unwrap();
64            let messages = ctx.messages.lock().unwrap().clone();
65            let input_tokens = *ctx.total_input_tokens.lock().unwrap();
66            let output_tokens = *ctx.total_output_tokens.lock().unwrap();
67
68            session_manager.save_session(&ctx.session_id, &messages, input_tokens, output_tokens)
69        };
70
71        if let Err(e) = save_result {
72            tracing::error!("Failed to save current session: {}", e);
73            ctx.add_system_message(format!("⚠ Warning: Failed to save current session: {}", e));
74        }
75
76        // Create new session
77        let new_session_id = {
78            let session_manager = ctx.session_manager.lock().map_err(|e| {
79                CliError::ConfigError(format!("Failed to acquire session manager lock: {}", e))
80            })?;
81
82            session_manager
83                .create_new_session()
84                .map_err(|e| CliError::ConfigError(format!("Failed to create session: {}", e)))?
85        };
86
87        // Update session ID in context
88        ctx.session_id = new_session_id.clone();
89
90        // Clear messages and reset token counts
91        ctx.messages.lock().unwrap().clear();
92        *ctx.total_input_tokens.lock().unwrap() = 0;
93        *ctx.total_output_tokens.lock().unwrap() = 0;
94
95        // Clear chat view
96        ctx.chat_view.lock().unwrap().clear();
97
98        tracing::info!("Created new session: {}", new_session_id);
99
100        // Add system message
101        let session_short_id = if new_session_id.len() > 8 {
102            &new_session_id[new_session_id.len().saturating_sub(8)..]
103        } else {
104            &new_session_id
105        };
106        ctx.add_system_message(format!("🆕 New session created: {}", session_short_id));
107
108        Ok(CommandResult::NewSession)
109    }
110
111    fn handle_load(
112        &self,
113        session_id: &str,
114        ctx: &mut CommandContext,
115    ) -> Result<CommandResult, CliError> {
116        tracing::info!("Session load command detected for session: {}", session_id);
117
118        // Save current session first
119        let save_result = {
120            let session_manager = ctx.session_manager.lock().unwrap();
121            let messages = ctx.messages.lock().unwrap().clone();
122            let input_tokens = *ctx.total_input_tokens.lock().unwrap();
123            let output_tokens = *ctx.total_output_tokens.lock().unwrap();
124
125            session_manager.save_session(&ctx.session_id, &messages, input_tokens, output_tokens)
126        };
127
128        if let Err(e) = save_result {
129            tracing::error!("Failed to save current session: {}", e);
130            ctx.add_system_message(format!("⚠ Warning: Failed to save current session: {}", e));
131        }
132
133        // Find session ID from partial match
134        let (full_session_id, session_info, messages) = {
135            let session_manager = ctx.session_manager.lock().map_err(|e| {
136                CliError::ConfigError(format!("Failed to acquire session manager lock: {}", e))
137            })?;
138
139            let sessions = session_manager
140                .list_sessions()
141                .map_err(|e| CliError::ConfigError(format!("Failed to list sessions: {}", e)))?;
142
143            let matched_session = if session_id.len() >= 8 {
144                // Try exact match first
145                sessions
146                    .iter()
147                    .find(|s| s.id == session_id)
148                    // Then try prefix match
149                    .or_else(|| sessions.iter().find(|s| s.id.starts_with(session_id)))
150            } else {
151                // Try prefix match for short IDs
152                sessions.iter().find(|s| s.id.starts_with(session_id))
153            };
154
155            match matched_session {
156                Some(info) => {
157                    let full_id = info.id.clone();
158                    let msgs = session_manager.load_session(&full_id).map_err(|e| {
159                        CliError::ConfigError(format!("Failed to load session {}: {}", full_id, e))
160                    })?;
161                    (full_id, info.clone(), msgs)
162                }
163                None => {
164                    ctx.add_system_message(format!("❌ Session not found: {}", session_id));
165                    return Ok(CommandResult::Continue);
166                }
167            }
168        };
169
170        // Update context with loaded session data
171        ctx.session_id = full_session_id.clone();
172        *ctx.total_input_tokens.lock().unwrap() = session_info.total_input_tokens;
173        *ctx.total_output_tokens.lock().unwrap() = session_info.total_output_tokens;
174        *ctx.messages.lock().unwrap() = messages.clone();
175
176        // Clear chat view and reload messages
177        {
178            let mut chat = ctx.chat_view.lock().unwrap();
179            chat.clear();
180
181            for msg in &messages {
182                match msg.role {
183                    limit_llm::Role::User => {
184                        let content = msg.content.as_deref().unwrap_or("");
185                        let chat_msg = limit_tui::components::Message::user(content.to_string());
186                        chat.add_message(chat_msg);
187                    }
188                    limit_llm::Role::Assistant => {
189                        let content = msg.content.as_deref().unwrap_or("");
190                        let chat_msg =
191                            limit_tui::components::Message::assistant(content.to_string());
192                        chat.add_message(chat_msg);
193                    }
194                    _ => {}
195                }
196            }
197        }
198
199        tracing::info!(
200            "Loaded session: {} ({} messages)",
201            full_session_id,
202            messages.len()
203        );
204
205        // Add system message
206        let session_short_id = if full_session_id.len() > 8 {
207            &full_session_id[full_session_id.len().saturating_sub(8)..]
208        } else {
209            &full_session_id
210        };
211        ctx.add_system_message(format!(
212            "📂 Loaded session: {} ({} messages, {} in tokens, {} out tokens)",
213            session_short_id,
214            messages.len(),
215            session_info.total_input_tokens,
216            session_info.total_output_tokens
217        ));
218
219        Ok(CommandResult::LoadSession(full_session_id))
220    }
221}
222
223impl Command for SessionCommand {
224    fn name(&self) -> &str {
225        "session"
226    }
227
228    fn description(&self) -> &str {
229        "Manage conversation sessions"
230    }
231
232    fn usage(&self) -> Vec<&str> {
233        vec!["/session list", "/session new", "/session load <id>"]
234    }
235
236    fn execute(&self, args: &str, ctx: &mut CommandContext) -> Result<CommandResult, CliError> {
237        let args = args.trim();
238
239        if args == "list" {
240            self.handle_list(ctx)
241        } else if args == "new" {
242            self.handle_new(ctx)
243        } else if args.starts_with("load ") {
244            let session_id = args.strip_prefix("load ").unwrap();
245            self.handle_load(session_id, ctx)
246        } else {
247            ctx.add_system_message(
248                "Usage: /session list, /session new, /session load <id>".to_string(),
249            );
250            Ok(CommandResult::Continue)
251        }
252    }
253}
254
255impl Default for SessionCommand {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_session_command() {
267        let cmd = SessionCommand::new();
268        assert_eq!(cmd.name(), "session");
269    }
270}