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
185                            .content
186                            .as_ref()
187                            .map(|c| c.to_text())
188                            .unwrap_or_default();
189                        let chat_msg = limit_tui::components::Message::user(content.to_string());
190                        chat.add_message(chat_msg);
191                    }
192                    limit_llm::Role::Assistant => {
193                        let content = msg
194                            .content
195                            .as_ref()
196                            .map(|c| c.to_text())
197                            .unwrap_or_default();
198                        let chat_msg =
199                            limit_tui::components::Message::assistant(content.to_string());
200                        chat.add_message(chat_msg);
201                    }
202                    _ => {}
203                }
204            }
205        }
206
207        tracing::info!(
208            "Loaded session: {} ({} messages)",
209            full_session_id,
210            messages.len()
211        );
212
213        // Add system message
214        let session_short_id = if full_session_id.len() > 8 {
215            &full_session_id[full_session_id.len().saturating_sub(8)..]
216        } else {
217            &full_session_id
218        };
219        ctx.add_system_message(format!(
220            "📂 Loaded session: {} ({} messages, {} in tokens, {} out tokens)",
221            session_short_id,
222            messages.len(),
223            session_info.total_input_tokens,
224            session_info.total_output_tokens
225        ));
226
227        Ok(CommandResult::LoadSession(full_session_id))
228    }
229}
230
231impl Command for SessionCommand {
232    fn name(&self) -> &str {
233        "session"
234    }
235
236    fn description(&self) -> &str {
237        "Manage conversation sessions"
238    }
239
240    fn usage(&self) -> Vec<&str> {
241        vec!["/session list", "/session new", "/session load <id>"]
242    }
243
244    fn execute(&self, args: &str, ctx: &mut CommandContext) -> Result<CommandResult, CliError> {
245        let args = args.trim();
246
247        if args == "list" {
248            self.handle_list(ctx)
249        } else if args == "new" {
250            self.handle_new(ctx)
251        } else if args.starts_with("load ") {
252            let session_id = args.strip_prefix("load ").unwrap();
253            self.handle_load(session_id, ctx)
254        } else {
255            ctx.add_system_message(
256                "Usage: /session list, /session new, /session load <id>".to_string(),
257            );
258            Ok(CommandResult::Continue)
259        }
260    }
261}
262
263impl Default for SessionCommand {
264    fn default() -> Self {
265        Self::new()
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_session_command() {
275        let cmd = SessionCommand::new();
276        assert_eq!(cmd.name(), "session");
277    }
278}