session.rs (15544B)
1 use std::collections::HashMap; 2 use std::path::PathBuf; 3 use std::sync::mpsc::Receiver; 4 5 use crate::agent_status::AgentStatus; 6 use crate::config::AiMode; 7 use crate::messages::{ 8 CompactionInfo, PermissionResponse, QuestionAnswer, SessionInfo, SubagentStatus, 9 }; 10 use crate::{DaveApiResponse, Message}; 11 use claude_agent_sdk_rs::PermissionMode; 12 use tokio::sync::oneshot; 13 use uuid::Uuid; 14 15 pub type SessionId = u32; 16 17 /// State for permission response with message 18 #[derive(Default, Clone, Copy, PartialEq)] 19 pub enum PermissionMessageState { 20 #[default] 21 None, 22 /// User pressed Shift+1, waiting for message then will Allow 23 TentativeAccept, 24 /// User pressed Shift+2, waiting for message then will Deny 25 TentativeDeny, 26 } 27 28 /// Agentic-mode specific session data (Claude backend only) 29 pub struct AgenticSessionData { 30 /// Pending permission requests waiting for user response 31 pub pending_permissions: HashMap<Uuid, oneshot::Sender<PermissionResponse>>, 32 /// Position in the RTS scene (in scene coordinates) 33 pub scene_position: egui::Vec2, 34 /// Permission mode for Claude (Default or Plan) 35 pub permission_mode: PermissionMode, 36 /// State for permission response message (tentative accept/deny) 37 pub permission_message_state: PermissionMessageState, 38 /// State for pending AskUserQuestion responses (keyed by request UUID) 39 pub question_answers: HashMap<Uuid, Vec<QuestionAnswer>>, 40 /// Current question index for multi-question AskUserQuestion (keyed by request UUID) 41 pub question_index: HashMap<Uuid, usize>, 42 /// Working directory for claude-code subprocess 43 pub cwd: PathBuf, 44 /// Session info from Claude Code CLI (tools, model, agents, etc.) 45 pub session_info: Option<SessionInfo>, 46 /// Indices of subagent messages in chat (keyed by task_id) 47 pub subagent_indices: HashMap<String, usize>, 48 /// Whether conversation compaction is in progress 49 pub is_compacting: bool, 50 /// Info from the last completed compaction (for display) 51 pub last_compaction: Option<CompactionInfo>, 52 /// Claude session ID to resume (UUID from Claude CLI's session storage) 53 /// When set, the backend will use --resume to continue this session 54 pub resume_session_id: Option<String>, 55 } 56 57 impl AgenticSessionData { 58 pub fn new(id: SessionId, cwd: PathBuf) -> Self { 59 // Arrange sessions in a grid pattern 60 let col = (id as i32 - 1) % 4; 61 let row = (id as i32 - 1) / 4; 62 let x = col as f32 * 150.0 - 225.0; // Center around origin 63 let y = row as f32 * 150.0 - 75.0; 64 65 AgenticSessionData { 66 pending_permissions: HashMap::new(), 67 scene_position: egui::Vec2::new(x, y), 68 permission_mode: PermissionMode::Default, 69 permission_message_state: PermissionMessageState::None, 70 question_answers: HashMap::new(), 71 question_index: HashMap::new(), 72 cwd, 73 session_info: None, 74 subagent_indices: HashMap::new(), 75 is_compacting: false, 76 last_compaction: None, 77 resume_session_id: None, 78 } 79 } 80 81 /// Update a subagent's output (appending new content, keeping only the tail) 82 pub fn update_subagent_output( 83 &mut self, 84 chat: &mut [Message], 85 task_id: &str, 86 new_output: &str, 87 ) { 88 if let Some(&idx) = self.subagent_indices.get(task_id) { 89 if let Some(Message::Subagent(subagent)) = chat.get_mut(idx) { 90 subagent.output.push_str(new_output); 91 // Keep only the most recent content up to max_output_size 92 if subagent.output.len() > subagent.max_output_size { 93 let keep_from = subagent.output.len() - subagent.max_output_size; 94 subagent.output = subagent.output[keep_from..].to_string(); 95 } 96 } 97 } 98 } 99 100 /// Mark a subagent as completed 101 pub fn complete_subagent(&mut self, chat: &mut [Message], task_id: &str, result: &str) { 102 if let Some(&idx) = self.subagent_indices.get(task_id) { 103 if let Some(Message::Subagent(subagent)) = chat.get_mut(idx) { 104 subagent.status = SubagentStatus::Completed; 105 subagent.output = result.to_string(); 106 } 107 } 108 } 109 } 110 111 /// A single chat session with Dave 112 pub struct ChatSession { 113 pub id: SessionId, 114 pub title: String, 115 pub chat: Vec<Message>, 116 pub input: String, 117 pub incoming_tokens: Option<Receiver<DaveApiResponse>>, 118 /// Handle to the background task processing this session's AI requests. 119 /// Aborted on drop to clean up the subprocess. 120 pub task_handle: Option<tokio::task::JoinHandle<()>>, 121 /// Cached status for the agent (derived from session state) 122 cached_status: AgentStatus, 123 /// Whether this session's input should be focused on the next frame 124 pub focus_requested: bool, 125 /// AI interaction mode for this session (Chat vs Agentic) 126 pub ai_mode: AiMode, 127 /// Agentic-mode specific data (None in Chat mode) 128 pub agentic: Option<AgenticSessionData>, 129 } 130 131 impl Drop for ChatSession { 132 fn drop(&mut self) { 133 if let Some(handle) = self.task_handle.take() { 134 handle.abort(); 135 } 136 } 137 } 138 139 impl ChatSession { 140 pub fn new(id: SessionId, cwd: PathBuf, ai_mode: AiMode) -> Self { 141 let agentic = match ai_mode { 142 AiMode::Agentic => Some(AgenticSessionData::new(id, cwd)), 143 AiMode::Chat => None, 144 }; 145 146 ChatSession { 147 id, 148 title: "New Chat".to_string(), 149 chat: vec![], 150 input: String::new(), 151 incoming_tokens: None, 152 task_handle: None, 153 cached_status: AgentStatus::Idle, 154 focus_requested: false, 155 ai_mode, 156 agentic, 157 } 158 } 159 160 /// Create a new session that resumes an existing Claude conversation 161 pub fn new_resumed( 162 id: SessionId, 163 cwd: PathBuf, 164 resume_session_id: String, 165 title: String, 166 ai_mode: AiMode, 167 ) -> Self { 168 let mut session = Self::new(id, cwd, ai_mode); 169 if let Some(ref mut agentic) = session.agentic { 170 agentic.resume_session_id = Some(resume_session_id); 171 } 172 session.title = title; 173 session 174 } 175 176 // === Helper methods for accessing agentic data === 177 178 /// Get agentic data, panics if not in agentic mode (use in agentic-only code paths) 179 pub fn agentic(&self) -> &AgenticSessionData { 180 self.agentic 181 .as_ref() 182 .expect("agentic data only available in Agentic mode") 183 } 184 185 /// Get mutable agentic data 186 pub fn agentic_mut(&mut self) -> &mut AgenticSessionData { 187 self.agentic 188 .as_mut() 189 .expect("agentic data only available in Agentic mode") 190 } 191 192 /// Check if session has agentic capabilities 193 pub fn is_agentic(&self) -> bool { 194 self.agentic.is_some() 195 } 196 197 /// Check if session has pending permission requests 198 pub fn has_pending_permissions(&self) -> bool { 199 self.agentic 200 .as_ref() 201 .is_some_and(|a| !a.pending_permissions.is_empty()) 202 } 203 204 /// Check if session is in plan mode 205 pub fn is_plan_mode(&self) -> bool { 206 self.agentic 207 .as_ref() 208 .is_some_and(|a| a.permission_mode == PermissionMode::Plan) 209 } 210 211 /// Get the working directory (agentic only) 212 pub fn cwd(&self) -> Option<&PathBuf> { 213 self.agentic.as_ref().map(|a| &a.cwd) 214 } 215 216 /// Update a subagent's output (appending new content, keeping only the tail) 217 pub fn update_subagent_output(&mut self, task_id: &str, new_output: &str) { 218 if let Some(ref mut agentic) = self.agentic { 219 agentic.update_subagent_output(&mut self.chat, task_id, new_output); 220 } 221 } 222 223 /// Mark a subagent as completed 224 pub fn complete_subagent(&mut self, task_id: &str, result: &str) { 225 if let Some(ref mut agentic) = self.agentic { 226 agentic.complete_subagent(&mut self.chat, task_id, result); 227 } 228 } 229 230 /// Update the session title from the last message (user or assistant) 231 pub fn update_title_from_last_message(&mut self) { 232 for msg in self.chat.iter().rev() { 233 let text = match msg { 234 Message::User(text) | Message::Assistant(text) => text, 235 _ => continue, 236 }; 237 // Use first ~30 chars of last message as title 238 let title: String = text.chars().take(30).collect(); 239 self.title = if text.len() > 30 { 240 format!("{}...", title) 241 } else { 242 title 243 }; 244 break; 245 } 246 } 247 248 /// Get the current status of this session/agent 249 pub fn status(&self) -> AgentStatus { 250 self.cached_status 251 } 252 253 /// Update the cached status based on current session state 254 pub fn update_status(&mut self) { 255 self.cached_status = self.derive_status(); 256 } 257 258 /// Derive status from the current session state 259 fn derive_status(&self) -> AgentStatus { 260 // Check for pending permission requests (needs input) - agentic only 261 if self.has_pending_permissions() { 262 return AgentStatus::NeedsInput; 263 } 264 265 // Check for error in last message 266 if let Some(Message::Error(_)) = self.chat.last() { 267 return AgentStatus::Error; 268 } 269 270 // Check if actively working (has task handle and receiving tokens) 271 if self.task_handle.is_some() && self.incoming_tokens.is_some() { 272 return AgentStatus::Working; 273 } 274 275 // Check if done (has messages and no active task) 276 if !self.chat.is_empty() && self.task_handle.is_none() { 277 // Check if the last meaningful message was from assistant 278 for msg in self.chat.iter().rev() { 279 match msg { 280 Message::Assistant(_) => return AgentStatus::Done, 281 Message::User(_) => return AgentStatus::Idle, // Waiting for response 282 Message::Error(_) => return AgentStatus::Error, 283 _ => continue, 284 } 285 } 286 } 287 288 AgentStatus::Idle 289 } 290 } 291 292 /// Tracks a pending external editor process 293 pub struct EditorJob { 294 /// The spawned editor process 295 pub child: std::process::Child, 296 /// Path to the temp file being edited 297 pub temp_path: PathBuf, 298 /// Session ID that initiated the editor 299 pub session_id: SessionId, 300 } 301 302 /// Manages multiple chat sessions 303 pub struct SessionManager { 304 sessions: HashMap<SessionId, ChatSession>, 305 order: Vec<SessionId>, // Sorted by recency (most recent first) 306 active: Option<SessionId>, 307 next_id: SessionId, 308 /// Pending external editor job (only one at a time) 309 pub pending_editor: Option<EditorJob>, 310 } 311 312 impl Default for SessionManager { 313 fn default() -> Self { 314 Self::new() 315 } 316 } 317 318 impl SessionManager { 319 pub fn new() -> Self { 320 SessionManager { 321 sessions: HashMap::new(), 322 order: Vec::new(), 323 active: None, 324 next_id: 1, 325 pending_editor: None, 326 } 327 } 328 329 /// Create a new session with the given cwd and make it active 330 pub fn new_session(&mut self, cwd: PathBuf, ai_mode: AiMode) -> SessionId { 331 let id = self.next_id; 332 self.next_id += 1; 333 334 let session = ChatSession::new(id, cwd, ai_mode); 335 self.sessions.insert(id, session); 336 self.order.insert(0, id); // Most recent first 337 self.active = Some(id); 338 339 id 340 } 341 342 /// Create a new session that resumes an existing Claude conversation 343 pub fn new_resumed_session( 344 &mut self, 345 cwd: PathBuf, 346 resume_session_id: String, 347 title: String, 348 ai_mode: AiMode, 349 ) -> SessionId { 350 let id = self.next_id; 351 self.next_id += 1; 352 353 let session = ChatSession::new_resumed(id, cwd, resume_session_id, title, ai_mode); 354 self.sessions.insert(id, session); 355 self.order.insert(0, id); // Most recent first 356 self.active = Some(id); 357 358 id 359 } 360 361 /// Get a reference to the active session 362 pub fn get_active(&self) -> Option<&ChatSession> { 363 self.active.and_then(|id| self.sessions.get(&id)) 364 } 365 366 /// Get a mutable reference to the active session 367 pub fn get_active_mut(&mut self) -> Option<&mut ChatSession> { 368 self.active.and_then(|id| self.sessions.get_mut(&id)) 369 } 370 371 /// Get the active session ID 372 pub fn active_id(&self) -> Option<SessionId> { 373 self.active 374 } 375 376 /// Switch to a different session 377 pub fn switch_to(&mut self, id: SessionId) -> bool { 378 if self.sessions.contains_key(&id) { 379 self.active = Some(id); 380 true 381 } else { 382 false 383 } 384 } 385 386 /// Delete a session 387 /// Returns true if the session was deleted, false if it didn't exist. 388 /// If the last session is deleted, active will be None and the caller 389 /// should open the directory picker to create a new session. 390 pub fn delete_session(&mut self, id: SessionId) -> bool { 391 if self.sessions.remove(&id).is_some() { 392 self.order.retain(|&x| x != id); 393 394 // If we deleted the active session, switch to another 395 if self.active == Some(id) { 396 self.active = self.order.first().copied(); 397 } 398 true 399 } else { 400 false 401 } 402 } 403 404 /// Get sessions in order of recency (most recent first) 405 pub fn sessions_ordered(&self) -> Vec<&ChatSession> { 406 self.order 407 .iter() 408 .filter_map(|id| self.sessions.get(id)) 409 .collect() 410 } 411 412 /// Update the recency of a session (move to front of order) 413 pub fn touch(&mut self, id: SessionId) { 414 if self.sessions.contains_key(&id) { 415 self.order.retain(|&x| x != id); 416 self.order.insert(0, id); 417 } 418 } 419 420 /// Get the number of sessions 421 pub fn len(&self) -> usize { 422 self.sessions.len() 423 } 424 425 /// Check if there are no sessions 426 pub fn is_empty(&self) -> bool { 427 self.sessions.is_empty() 428 } 429 430 /// Get a reference to a session by ID 431 pub fn get(&self, id: SessionId) -> Option<&ChatSession> { 432 self.sessions.get(&id) 433 } 434 435 /// Get a mutable reference to a session by ID 436 pub fn get_mut(&mut self, id: SessionId) -> Option<&mut ChatSession> { 437 self.sessions.get_mut(&id) 438 } 439 440 /// Iterate over all sessions 441 pub fn iter(&self) -> impl Iterator<Item = &ChatSession> { 442 self.sessions.values() 443 } 444 445 /// Iterate over all sessions mutably 446 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut ChatSession> { 447 self.sessions.values_mut() 448 } 449 450 /// Update status for all sessions 451 pub fn update_all_statuses(&mut self) { 452 for session in self.sessions.values_mut() { 453 session.update_status(); 454 } 455 } 456 457 /// Get the first session that needs attention (NeedsInput status) 458 pub fn find_needs_attention(&self) -> Option<SessionId> { 459 for session in self.sessions.values() { 460 if session.status() == AgentStatus::NeedsInput { 461 return Some(session.id); 462 } 463 } 464 None 465 } 466 467 /// Get all session IDs 468 pub fn session_ids(&self) -> Vec<SessionId> { 469 self.order.clone() 470 } 471 }