update.rs (29437B)
1 //! Helper functions for the Dave update loop. 2 //! 3 //! These are standalone functions with explicit inputs to reduce the complexity 4 //! of the main Dave struct and make the code more testable and reusable. 5 6 use crate::backend::AiBackend; 7 use crate::config::AiMode; 8 use crate::focus_queue::{FocusPriority, FocusQueue}; 9 use crate::messages::{ 10 AnswerSummary, AnswerSummaryEntry, AskUserQuestionInput, Message, PermissionResponse, 11 QuestionAnswer, 12 }; 13 use crate::session::{ChatSession, EditorJob, PermissionMessageState, SessionId, SessionManager}; 14 use crate::ui::{AgentScene, DirectoryPicker}; 15 use claude_agent_sdk_rs::PermissionMode; 16 use std::path::PathBuf; 17 use std::time::Instant; 18 19 /// Timeout for confirming interrupt (in seconds) 20 pub const INTERRUPT_CONFIRM_TIMEOUT_SECS: f32 = 1.5; 21 22 // ============================================================================= 23 // Interrupt Handling 24 // ============================================================================= 25 26 /// Handle an interrupt request - requires double-Escape to confirm. 27 /// Returns the new pending_since state. 28 pub fn handle_interrupt_request( 29 session_manager: &SessionManager, 30 backend: &dyn AiBackend, 31 pending_since: Option<Instant>, 32 ctx: &egui::Context, 33 ) -> Option<Instant> { 34 // Only allow interrupt if there's an active AI operation 35 let has_active_operation = session_manager 36 .get_active() 37 .map(|s| s.incoming_tokens.is_some()) 38 .unwrap_or(false); 39 40 if !has_active_operation { 41 return None; 42 } 43 44 let now = Instant::now(); 45 46 if let Some(pending) = pending_since { 47 if now.duration_since(pending).as_secs_f32() < INTERRUPT_CONFIRM_TIMEOUT_SECS { 48 // Second Escape within timeout - confirm interrupt 49 if let Some(session) = session_manager.get_active() { 50 let session_id = format!("dave-session-{}", session.id); 51 backend.interrupt_session(session_id, ctx.clone()); 52 } 53 None 54 } else { 55 // Timeout expired, treat as new first press 56 Some(now) 57 } 58 } else { 59 // First Escape press 60 Some(now) 61 } 62 } 63 64 /// Execute the actual interrupt on the active session. 65 pub fn execute_interrupt( 66 session_manager: &mut SessionManager, 67 backend: &dyn AiBackend, 68 ctx: &egui::Context, 69 ) { 70 if let Some(session) = session_manager.get_active_mut() { 71 let session_id = format!("dave-session-{}", session.id); 72 backend.interrupt_session(session_id, ctx.clone()); 73 session.incoming_tokens = None; 74 if let Some(agentic) = &mut session.agentic { 75 agentic.pending_permissions.clear(); 76 } 77 tracing::debug!("Interrupted session {}", session.id); 78 } 79 } 80 81 /// Check if interrupt confirmation has timed out. 82 /// Returns None if timed out, otherwise returns the original value. 83 pub fn check_interrupt_timeout(pending_since: Option<Instant>) -> Option<Instant> { 84 pending_since.filter(|pending| { 85 Instant::now().duration_since(*pending).as_secs_f32() < INTERRUPT_CONFIRM_TIMEOUT_SECS 86 }) 87 } 88 89 // ============================================================================= 90 // Plan Mode 91 // ============================================================================= 92 93 /// Toggle plan mode for the active session. 94 pub fn toggle_plan_mode( 95 session_manager: &mut SessionManager, 96 backend: &dyn AiBackend, 97 ctx: &egui::Context, 98 ) { 99 if let Some(session) = session_manager.get_active_mut() { 100 if let Some(agentic) = &mut session.agentic { 101 let new_mode = match agentic.permission_mode { 102 PermissionMode::Plan => PermissionMode::Default, 103 _ => PermissionMode::Plan, 104 }; 105 agentic.permission_mode = new_mode; 106 107 let session_id = format!("dave-session-{}", session.id); 108 backend.set_permission_mode(session_id, new_mode, ctx.clone()); 109 110 tracing::debug!( 111 "Toggled plan mode for session {} to {:?}", 112 session.id, 113 new_mode 114 ); 115 } 116 } 117 } 118 119 /// Exit plan mode for the active session (switch to Default mode). 120 pub fn exit_plan_mode( 121 session_manager: &mut SessionManager, 122 backend: &dyn AiBackend, 123 ctx: &egui::Context, 124 ) { 125 if let Some(session) = session_manager.get_active_mut() { 126 if let Some(agentic) = &mut session.agentic { 127 agentic.permission_mode = PermissionMode::Default; 128 let session_id = format!("dave-session-{}", session.id); 129 backend.set_permission_mode(session_id, PermissionMode::Default, ctx.clone()); 130 tracing::debug!("Exited plan mode for session {}", session.id); 131 } 132 } 133 } 134 135 // ============================================================================= 136 // Permission Handling 137 // ============================================================================= 138 139 /// Get the first pending permission request ID for the active session. 140 pub fn first_pending_permission(session_manager: &SessionManager) -> Option<uuid::Uuid> { 141 session_manager 142 .get_active() 143 .and_then(|session| session.agentic.as_ref()) 144 .and_then(|agentic| agentic.pending_permissions.keys().next().copied()) 145 } 146 147 /// Get the tool name of the first pending permission request. 148 pub fn pending_permission_tool_name(session_manager: &SessionManager) -> Option<&str> { 149 let session = session_manager.get_active()?; 150 let agentic = session.agentic.as_ref()?; 151 let request_id = agentic.pending_permissions.keys().next()?; 152 153 for msg in &session.chat { 154 if let Message::PermissionRequest(req) = msg { 155 if &req.id == request_id { 156 return Some(&req.tool_name); 157 } 158 } 159 } 160 161 None 162 } 163 164 /// Check if the first pending permission is an AskUserQuestion tool call. 165 pub fn has_pending_question(session_manager: &SessionManager) -> bool { 166 pending_permission_tool_name(session_manager) == Some("AskUserQuestion") 167 } 168 169 /// Check if the first pending permission is an ExitPlanMode tool call. 170 pub fn has_pending_exit_plan_mode(session_manager: &SessionManager) -> bool { 171 pending_permission_tool_name(session_manager) == Some("ExitPlanMode") 172 } 173 174 /// Handle a permission response (from UI button or keybinding). 175 pub fn handle_permission_response( 176 session_manager: &mut SessionManager, 177 request_id: uuid::Uuid, 178 response: PermissionResponse, 179 ) { 180 let Some(session) = session_manager.get_active_mut() else { 181 return; 182 }; 183 184 // Record the response type in the message for UI display 185 let response_type = match &response { 186 PermissionResponse::Allow { .. } => crate::messages::PermissionResponseType::Allowed, 187 PermissionResponse::Deny { .. } => crate::messages::PermissionResponseType::Denied, 188 }; 189 190 // If Allow has a message, add it as a User message to the chat 191 if let PermissionResponse::Allow { message: Some(msg) } = &response { 192 if !msg.is_empty() { 193 session.chat.push(Message::User(msg.clone())); 194 } 195 } 196 197 // Clear permission message state (agentic only) 198 if let Some(agentic) = &mut session.agentic { 199 agentic.permission_message_state = PermissionMessageState::None; 200 } 201 202 for msg in &mut session.chat { 203 if let Message::PermissionRequest(req) = msg { 204 if req.id == request_id { 205 req.response = Some(response_type); 206 break; 207 } 208 } 209 } 210 211 if let Some(agentic) = &mut session.agentic { 212 if let Some(sender) = agentic.pending_permissions.remove(&request_id) { 213 if sender.send(response).is_err() { 214 tracing::error!( 215 "Failed to send permission response for request {}", 216 request_id 217 ); 218 } 219 } else { 220 tracing::warn!("No pending permission found for request {}", request_id); 221 } 222 } 223 } 224 225 /// Handle a user's response to an AskUserQuestion tool call. 226 pub fn handle_question_response( 227 session_manager: &mut SessionManager, 228 request_id: uuid::Uuid, 229 answers: Vec<QuestionAnswer>, 230 ) { 231 let Some(session) = session_manager.get_active_mut() else { 232 return; 233 }; 234 235 // Find the original AskUserQuestion request to get the question labels 236 let questions_input = session.chat.iter().find_map(|msg| { 237 if let Message::PermissionRequest(req) = msg { 238 if req.id == request_id && req.tool_name == "AskUserQuestion" { 239 serde_json::from_value::<AskUserQuestionInput>(req.tool_input.clone()).ok() 240 } else { 241 None 242 } 243 } else { 244 None 245 } 246 }); 247 248 // Format answers as JSON for the tool response, and build summary for display 249 let (formatted_response, answer_summary) = if let Some(ref questions) = questions_input { 250 let mut answers_obj = serde_json::Map::new(); 251 let mut summary_entries = Vec::with_capacity(questions.questions.len()); 252 253 for (q_idx, (question, answer)) in 254 questions.questions.iter().zip(answers.iter()).enumerate() 255 { 256 let mut answer_obj = serde_json::Map::new(); 257 258 // Map selected indices to option labels 259 let selected_labels: Vec<String> = answer 260 .selected 261 .iter() 262 .filter_map(|&idx| question.options.get(idx).map(|o| o.label.clone())) 263 .collect(); 264 265 answer_obj.insert( 266 "selected".to_string(), 267 serde_json::Value::Array( 268 selected_labels 269 .iter() 270 .cloned() 271 .map(serde_json::Value::String) 272 .collect(), 273 ), 274 ); 275 276 // Build display text for summary 277 let mut display_parts = selected_labels; 278 if let Some(ref other) = answer.other_text { 279 if !other.is_empty() { 280 answer_obj.insert( 281 "other".to_string(), 282 serde_json::Value::String(other.clone()), 283 ); 284 display_parts.push(format!("Other: {}", other)); 285 } 286 } 287 288 // Use header as the key, fall back to question index 289 let key = if !question.header.is_empty() { 290 question.header.clone() 291 } else { 292 format!("question_{}", q_idx) 293 }; 294 answers_obj.insert(key.clone(), serde_json::Value::Object(answer_obj)); 295 296 summary_entries.push(AnswerSummaryEntry { 297 header: key, 298 answer: display_parts.join(", "), 299 }); 300 } 301 302 ( 303 serde_json::json!({ "answers": answers_obj }).to_string(), 304 Some(AnswerSummary { 305 entries: summary_entries, 306 }), 307 ) 308 } else { 309 // Fallback: just serialize the answers directly 310 ( 311 serde_json::to_string(&answers).unwrap_or_else(|_| "{}".to_string()), 312 None, 313 ) 314 }; 315 316 // Mark the request as allowed in the UI and store the summary for display 317 for msg in &mut session.chat { 318 if let Message::PermissionRequest(req) = msg { 319 if req.id == request_id { 320 req.response = Some(crate::messages::PermissionResponseType::Allowed); 321 req.answer_summary = answer_summary.clone(); 322 break; 323 } 324 } 325 } 326 327 // Clean up transient answer state and send response (agentic only) 328 if let Some(agentic) = &mut session.agentic { 329 agentic.question_answers.remove(&request_id); 330 agentic.question_index.remove(&request_id); 331 332 // Send the response through the permission channel 333 if let Some(sender) = agentic.pending_permissions.remove(&request_id) { 334 let response = PermissionResponse::Allow { 335 message: Some(formatted_response), 336 }; 337 if sender.send(response).is_err() { 338 tracing::error!( 339 "Failed to send question response for request {}", 340 request_id 341 ); 342 } 343 } else { 344 tracing::warn!("No pending permission found for request {}", request_id); 345 } 346 } 347 } 348 349 // ============================================================================= 350 // Agent Navigation 351 // ============================================================================= 352 353 /// Switch to agent by index in the ordered list (0-indexed). 354 pub fn switch_to_agent_by_index( 355 session_manager: &mut SessionManager, 356 scene: &mut AgentScene, 357 show_scene: bool, 358 index: usize, 359 ) { 360 let ids = session_manager.session_ids(); 361 if let Some(&id) = ids.get(index) { 362 session_manager.switch_to(id); 363 if show_scene { 364 scene.select(id); 365 } 366 if let Some(session) = session_manager.get_mut(id) { 367 if !session.has_pending_permissions() { 368 session.focus_requested = true; 369 } 370 } 371 } 372 } 373 374 /// Cycle to the next agent. 375 pub fn cycle_next_agent( 376 session_manager: &mut SessionManager, 377 scene: &mut AgentScene, 378 show_scene: bool, 379 ) { 380 let ids = session_manager.session_ids(); 381 if ids.is_empty() { 382 return; 383 } 384 let current_idx = session_manager 385 .active_id() 386 .and_then(|active| ids.iter().position(|&id| id == active)) 387 .unwrap_or(0); 388 let next_idx = (current_idx + 1) % ids.len(); 389 if let Some(&id) = ids.get(next_idx) { 390 session_manager.switch_to(id); 391 if show_scene { 392 scene.select(id); 393 } 394 if let Some(session) = session_manager.get_mut(id) { 395 if !session.has_pending_permissions() { 396 session.focus_requested = true; 397 } 398 } 399 } 400 } 401 402 /// Cycle to the previous agent. 403 pub fn cycle_prev_agent( 404 session_manager: &mut SessionManager, 405 scene: &mut AgentScene, 406 show_scene: bool, 407 ) { 408 let ids = session_manager.session_ids(); 409 if ids.is_empty() { 410 return; 411 } 412 let current_idx = session_manager 413 .active_id() 414 .and_then(|active| ids.iter().position(|&id| id == active)) 415 .unwrap_or(0); 416 let prev_idx = if current_idx == 0 { 417 ids.len() - 1 418 } else { 419 current_idx - 1 420 }; 421 if let Some(&id) = ids.get(prev_idx) { 422 session_manager.switch_to(id); 423 if show_scene { 424 scene.select(id); 425 } 426 if let Some(session) = session_manager.get_mut(id) { 427 if !session.has_pending_permissions() { 428 session.focus_requested = true; 429 } 430 } 431 } 432 } 433 434 // ============================================================================= 435 // Focus Queue Operations 436 // ============================================================================= 437 438 /// Navigate to the next item in the focus queue. 439 pub fn focus_queue_next( 440 session_manager: &mut SessionManager, 441 focus_queue: &mut FocusQueue, 442 scene: &mut AgentScene, 443 show_scene: bool, 444 ) { 445 if let Some(session_id) = focus_queue.next() { 446 session_manager.switch_to(session_id); 447 if show_scene { 448 scene.select(session_id); 449 if let Some(session) = session_manager.get(session_id) { 450 if let Some(agentic) = &session.agentic { 451 scene.focus_on(agentic.scene_position); 452 } 453 } 454 } 455 if let Some(session) = session_manager.get_mut(session_id) { 456 if !session.has_pending_permissions() { 457 session.focus_requested = true; 458 } 459 } 460 } 461 } 462 463 /// Navigate to the previous item in the focus queue. 464 pub fn focus_queue_prev( 465 session_manager: &mut SessionManager, 466 focus_queue: &mut FocusQueue, 467 scene: &mut AgentScene, 468 show_scene: bool, 469 ) { 470 if let Some(session_id) = focus_queue.prev() { 471 session_manager.switch_to(session_id); 472 if show_scene { 473 scene.select(session_id); 474 if let Some(session) = session_manager.get(session_id) { 475 if let Some(agentic) = &session.agentic { 476 scene.focus_on(agentic.scene_position); 477 } 478 } 479 } 480 if let Some(session) = session_manager.get_mut(session_id) { 481 if !session.has_pending_permissions() { 482 session.focus_requested = true; 483 } 484 } 485 } 486 } 487 488 /// Toggle Done status for the current focus queue item. 489 pub fn focus_queue_toggle_done(focus_queue: &mut FocusQueue) { 490 if let Some(entry) = focus_queue.current() { 491 if entry.priority == FocusPriority::Done { 492 focus_queue.dequeue(entry.session_id); 493 } 494 } 495 } 496 497 /// Toggle auto-steal focus mode. 498 /// Returns the new auto_steal_focus state. 499 pub fn toggle_auto_steal( 500 session_manager: &mut SessionManager, 501 scene: &mut AgentScene, 502 show_scene: bool, 503 auto_steal_focus: bool, 504 home_session: &mut Option<SessionId>, 505 ) -> bool { 506 let new_state = !auto_steal_focus; 507 508 if new_state { 509 // Enabling: record current session as home 510 *home_session = session_manager.active_id(); 511 tracing::debug!("Auto-steal focus enabled, home session: {:?}", home_session); 512 } else { 513 // Disabling: switch back to home session if set 514 if let Some(home_id) = home_session.take() { 515 session_manager.switch_to(home_id); 516 if show_scene { 517 scene.select(home_id); 518 if let Some(session) = session_manager.get(home_id) { 519 if let Some(agentic) = &session.agentic { 520 scene.focus_on(agentic.scene_position); 521 } 522 } 523 } 524 tracing::debug!("Auto-steal focus disabled, returned to home session"); 525 } 526 } 527 528 // Request focus on input after toggle 529 if let Some(session) = session_manager.get_active_mut() { 530 session.focus_requested = true; 531 } 532 533 new_state 534 } 535 536 /// Process auto-steal focus logic: switch to focus queue items as needed. 537 pub fn process_auto_steal_focus( 538 session_manager: &mut SessionManager, 539 focus_queue: &mut FocusQueue, 540 scene: &mut AgentScene, 541 show_scene: bool, 542 auto_steal_focus: bool, 543 home_session: &mut Option<SessionId>, 544 ) { 545 if !auto_steal_focus { 546 return; 547 } 548 549 let has_needs_input = focus_queue.has_needs_input(); 550 551 if has_needs_input { 552 // There are NeedsInput items - check if we need to steal focus 553 let current_session = session_manager.active_id(); 554 let current_priority = current_session.and_then(|id| focus_queue.get_session_priority(id)); 555 let already_on_needs_input = current_priority == Some(FocusPriority::NeedsInput); 556 557 if !already_on_needs_input { 558 // Save current session before stealing (only if we haven't saved yet) 559 if home_session.is_none() { 560 *home_session = current_session; 561 tracing::debug!("Auto-steal: saved home session {:?}", home_session); 562 } 563 564 // Jump to first NeedsInput item 565 if let Some(idx) = focus_queue.first_needs_input_index() { 566 focus_queue.set_cursor(idx); 567 if let Some(entry) = focus_queue.current() { 568 session_manager.switch_to(entry.session_id); 569 if show_scene { 570 scene.select(entry.session_id); 571 if let Some(session) = session_manager.get(entry.session_id) { 572 if let Some(agentic) = &session.agentic { 573 scene.focus_on(agentic.scene_position); 574 } 575 } 576 } 577 tracing::debug!("Auto-steal: switched to session {:?}", entry.session_id); 578 } 579 } 580 } 581 } else if let Some(home_id) = home_session.take() { 582 // No more NeedsInput items - return to saved session 583 session_manager.switch_to(home_id); 584 if show_scene { 585 scene.select(home_id); 586 if let Some(session) = session_manager.get(home_id) { 587 if let Some(agentic) = &session.agentic { 588 scene.focus_on(agentic.scene_position); 589 } 590 } 591 } 592 tracing::debug!("Auto-steal: returned to home session {:?}", home_id); 593 } 594 } 595 596 // ============================================================================= 597 // External Editor 598 // ============================================================================= 599 600 /// Try to find a common terminal emulator. 601 pub fn find_terminal() -> Option<String> { 602 use std::process::Command; 603 let terminals = [ 604 "alacritty", 605 "kitty", 606 "gnome-terminal", 607 "konsole", 608 "urxvtc", 609 "urxvt", 610 "xterm", 611 ]; 612 for term in terminals { 613 if Command::new("which") 614 .arg(term) 615 .output() 616 .map(|o| o.status.success()) 617 .unwrap_or(false) 618 { 619 return Some(term.to_string()); 620 } 621 } 622 None 623 } 624 625 /// Open an external editor for composing the input text (non-blocking). 626 pub fn open_external_editor(session_manager: &mut SessionManager) { 627 use std::process::Command; 628 629 // Don't spawn another editor if one is already pending 630 if session_manager.pending_editor.is_some() { 631 tracing::warn!("External editor already in progress"); 632 return; 633 } 634 635 let Some(session) = session_manager.get_active_mut() else { 636 return; 637 }; 638 let session_id = session.id; 639 let input_content = session.input.clone(); 640 641 // Create temp file with current input content 642 let temp_path = std::env::temp_dir().join("notedeck_input.txt"); 643 if let Err(e) = std::fs::write(&temp_path, &input_content) { 644 tracing::error!("Failed to write temp file for external editor: {}", e); 645 return; 646 } 647 648 // Try $VISUAL first (GUI editors), then fall back to terminal + $EDITOR 649 let visual = std::env::var("VISUAL").ok(); 650 let editor = std::env::var("EDITOR").ok(); 651 652 let spawn_result = if let Some(visual_editor) = visual { 653 // $VISUAL is set - use it directly (assumes GUI editor) 654 tracing::debug!("Opening external editor via $VISUAL: {}", visual_editor); 655 Command::new(&visual_editor).arg(&temp_path).spawn() 656 } else { 657 // Fall back to terminal + $EDITOR 658 let editor_cmd = editor.unwrap_or_else(|| "vim".to_string()); 659 let terminal = std::env::var("TERMINAL") 660 .ok() 661 .or_else(find_terminal) 662 .unwrap_or_else(|| "xterm".to_string()); 663 664 tracing::debug!( 665 "Opening external editor via terminal: {} -e {} {}", 666 terminal, 667 editor_cmd, 668 temp_path.display() 669 ); 670 Command::new(&terminal) 671 .arg("-e") 672 .arg(&editor_cmd) 673 .arg(&temp_path) 674 .spawn() 675 }; 676 677 match spawn_result { 678 Ok(child) => { 679 session_manager.pending_editor = Some(EditorJob { 680 child, 681 temp_path, 682 session_id, 683 }); 684 tracing::debug!("External editor spawned for session {}", session_id); 685 } 686 Err(e) => { 687 tracing::error!("Failed to spawn external editor: {}", e); 688 let _ = std::fs::remove_file(&temp_path); 689 } 690 } 691 } 692 693 /// Poll for external editor completion (called each frame). 694 pub fn poll_editor_job(session_manager: &mut SessionManager) { 695 let Some(ref mut job) = session_manager.pending_editor else { 696 return; 697 }; 698 699 // Non-blocking check if child has exited 700 match job.child.try_wait() { 701 Ok(Some(status)) => { 702 let session_id = job.session_id; 703 let temp_path = job.temp_path.clone(); 704 705 if status.success() { 706 match std::fs::read_to_string(&temp_path) { 707 Ok(content) => { 708 if let Some(session) = session_manager.get_mut(session_id) { 709 session.input = content; 710 session.focus_requested = true; 711 tracing::debug!( 712 "External editor completed, updated input for session {}", 713 session_id 714 ); 715 } 716 } 717 Err(e) => { 718 tracing::error!("Failed to read temp file after editing: {}", e); 719 } 720 } 721 } else { 722 tracing::warn!("External editor exited with status: {}", status); 723 } 724 725 if let Err(e) = std::fs::remove_file(&temp_path) { 726 tracing::error!("Failed to remove temp file: {}", e); 727 } 728 729 session_manager.pending_editor = None; 730 } 731 Ok(None) => { 732 // Editor still running 733 } 734 Err(e) => { 735 tracing::error!("Failed to poll editor process: {}", e); 736 let temp_path = job.temp_path.clone(); 737 let _ = std::fs::remove_file(&temp_path); 738 session_manager.pending_editor = None; 739 } 740 } 741 } 742 743 // ============================================================================= 744 // Session Management 745 // ============================================================================= 746 747 /// Create a new session with the given cwd. 748 pub fn create_session_with_cwd( 749 session_manager: &mut SessionManager, 750 directory_picker: &mut DirectoryPicker, 751 scene: &mut AgentScene, 752 show_scene: bool, 753 ai_mode: AiMode, 754 cwd: PathBuf, 755 ) -> SessionId { 756 directory_picker.add_recent(cwd.clone()); 757 758 let id = session_manager.new_session(cwd, ai_mode); 759 if let Some(session) = session_manager.get_mut(id) { 760 session.focus_requested = true; 761 if show_scene { 762 scene.select(id); 763 if let Some(agentic) = &session.agentic { 764 scene.focus_on(agentic.scene_position); 765 } 766 } 767 } 768 id 769 } 770 771 /// Create a new session that resumes an existing Claude conversation. 772 #[allow(clippy::too_many_arguments)] 773 pub fn create_resumed_session_with_cwd( 774 session_manager: &mut SessionManager, 775 directory_picker: &mut DirectoryPicker, 776 scene: &mut AgentScene, 777 show_scene: bool, 778 ai_mode: AiMode, 779 cwd: PathBuf, 780 resume_session_id: String, 781 title: String, 782 ) -> SessionId { 783 directory_picker.add_recent(cwd.clone()); 784 785 let id = session_manager.new_resumed_session(cwd, resume_session_id, title, ai_mode); 786 if let Some(session) = session_manager.get_mut(id) { 787 session.focus_requested = true; 788 if show_scene { 789 scene.select(id); 790 if let Some(agentic) = &session.agentic { 791 scene.focus_on(agentic.scene_position); 792 } 793 } 794 } 795 id 796 } 797 798 /// Clone the active agent, creating a new session with the same working directory. 799 pub fn clone_active_agent( 800 session_manager: &mut SessionManager, 801 directory_picker: &mut DirectoryPicker, 802 scene: &mut AgentScene, 803 show_scene: bool, 804 ai_mode: AiMode, 805 ) -> Option<SessionId> { 806 let cwd = session_manager 807 .get_active() 808 .and_then(|s| s.cwd().cloned())?; 809 Some(create_session_with_cwd( 810 session_manager, 811 directory_picker, 812 scene, 813 show_scene, 814 ai_mode, 815 cwd, 816 )) 817 } 818 819 /// Delete a session and clean up backend resources. 820 pub fn delete_session( 821 session_manager: &mut SessionManager, 822 focus_queue: &mut FocusQueue, 823 backend: &dyn AiBackend, 824 directory_picker: &mut DirectoryPicker, 825 id: SessionId, 826 ) -> bool { 827 focus_queue.remove_session(id); 828 if session_manager.delete_session(id) { 829 let session_id = format!("dave-session-{}", id); 830 backend.cleanup_session(session_id); 831 832 if session_manager.is_empty() { 833 directory_picker.open(); 834 } 835 true 836 } else { 837 false 838 } 839 } 840 841 // ============================================================================= 842 // Send Action Handling 843 // ============================================================================= 844 845 /// Handle the /cd command if present in input. 846 /// Returns Some(Ok(path)) if cd succeeded, Some(Err(())) if cd failed, None if not a cd command. 847 pub fn handle_cd_command(session: &mut ChatSession) -> Option<Result<PathBuf, ()>> { 848 let input = session.input.trim().to_string(); 849 if !input.starts_with("/cd ") { 850 return None; 851 } 852 853 let path_str = input.strip_prefix("/cd ").unwrap().trim(); 854 let path = PathBuf::from(path_str); 855 session.input.clear(); 856 857 if path.exists() && path.is_dir() { 858 if let Some(agentic) = &mut session.agentic { 859 agentic.cwd = path.clone(); 860 } 861 session.chat.push(Message::System(format!( 862 "Working directory set to: {}", 863 path.display() 864 ))); 865 Some(Ok(path)) 866 } else { 867 session 868 .chat 869 .push(Message::Error(format!("Invalid directory: {}", path_str))); 870 Some(Err(())) 871 } 872 }