mod.rs (26793B)
1 mod ask_question; 2 pub mod badge; 3 mod dave; 4 pub mod diff; 5 pub mod directory_picker; 6 pub mod keybind_hint; 7 pub mod keybindings; 8 pub mod path_utils; 9 mod pill; 10 mod query_ui; 11 pub mod scene; 12 pub mod session_list; 13 pub mod session_picker; 14 mod settings; 15 mod top_buttons; 16 17 pub use ask_question::{ask_user_question_summary_ui, ask_user_question_ui}; 18 pub use dave::{DaveAction, DaveResponse, DaveUi}; 19 pub use directory_picker::{DirectoryPicker, DirectoryPickerAction}; 20 pub use keybind_hint::{keybind_hint, paint_keybind_hint}; 21 pub use keybindings::{check_keybindings, KeyAction}; 22 pub use scene::{AgentScene, SceneAction, SceneResponse}; 23 pub use session_list::{SessionListAction, SessionListUi}; 24 pub use session_picker::{SessionPicker, SessionPickerAction}; 25 pub use settings::{DaveSettingsPanel, SettingsPanelAction}; 26 27 // ============================================================================= 28 // Standalone UI Functions 29 // ============================================================================= 30 31 use crate::agent_status::AgentStatus; 32 use crate::config::{AiMode, DaveSettings, ModelConfig}; 33 use crate::focus_queue::FocusQueue; 34 use crate::messages::PermissionResponse; 35 use crate::session::{PermissionMessageState, SessionId, SessionManager}; 36 use crate::session_discovery::discover_sessions; 37 use crate::update; 38 use crate::DaveOverlay; 39 40 /// UI result from overlay rendering 41 pub enum OverlayResult { 42 /// No action taken 43 None, 44 /// Close the overlay 45 Close, 46 /// Directory was selected (no resumable sessions) 47 DirectorySelected(std::path::PathBuf), 48 /// Show session picker for the given directory 49 ShowSessionPicker(std::path::PathBuf), 50 /// Resume a session 51 ResumeSession { 52 cwd: std::path::PathBuf, 53 session_id: String, 54 title: String, 55 }, 56 /// Create a new session in the given directory 57 NewSession { cwd: std::path::PathBuf }, 58 /// Go back to directory picker 59 BackToDirectoryPicker, 60 /// Apply new settings 61 ApplySettings(DaveSettings), 62 } 63 64 /// Render the settings overlay UI. 65 pub fn settings_overlay_ui( 66 settings_panel: &mut DaveSettingsPanel, 67 settings: &DaveSettings, 68 ui: &mut egui::Ui, 69 ) -> OverlayResult { 70 if let Some(action) = settings_panel.overlay_ui(ui, settings) { 71 match action { 72 SettingsPanelAction::Save(new_settings) => { 73 return OverlayResult::ApplySettings(new_settings); 74 } 75 SettingsPanelAction::Cancel => { 76 return OverlayResult::Close; 77 } 78 } 79 } 80 OverlayResult::None 81 } 82 83 /// Render the directory picker overlay UI. 84 pub fn directory_picker_overlay_ui( 85 directory_picker: &mut DirectoryPicker, 86 has_sessions: bool, 87 ui: &mut egui::Ui, 88 ) -> OverlayResult { 89 if let Some(action) = directory_picker.overlay_ui(ui, has_sessions) { 90 match action { 91 DirectoryPickerAction::DirectorySelected(path) => { 92 let resumable_sessions = discover_sessions(&path); 93 if resumable_sessions.is_empty() { 94 return OverlayResult::DirectorySelected(path); 95 } else { 96 return OverlayResult::ShowSessionPicker(path); 97 } 98 } 99 DirectoryPickerAction::Cancelled => { 100 if has_sessions { 101 return OverlayResult::Close; 102 } 103 } 104 DirectoryPickerAction::BrowseRequested => {} 105 } 106 } 107 OverlayResult::None 108 } 109 110 /// Render the session picker overlay UI. 111 pub fn session_picker_overlay_ui( 112 session_picker: &mut SessionPicker, 113 ui: &mut egui::Ui, 114 ) -> OverlayResult { 115 if let Some(action) = session_picker.overlay_ui(ui) { 116 match action { 117 SessionPickerAction::ResumeSession { 118 cwd, 119 session_id, 120 title, 121 } => { 122 return OverlayResult::ResumeSession { 123 cwd, 124 session_id, 125 title, 126 }; 127 } 128 SessionPickerAction::NewSession { cwd } => { 129 return OverlayResult::NewSession { cwd }; 130 } 131 SessionPickerAction::BackToDirectoryPicker => { 132 return OverlayResult::BackToDirectoryPicker; 133 } 134 } 135 } 136 OverlayResult::None 137 } 138 139 /// Scene view action returned after rendering 140 pub enum SceneViewAction { 141 None, 142 ToggleToListView, 143 SpawnAgent, 144 DeleteSelected(Vec<SessionId>), 145 } 146 147 /// Render the scene view with RTS-style agent visualization and chat side panel. 148 #[allow(clippy::too_many_arguments)] 149 pub fn scene_ui( 150 session_manager: &mut SessionManager, 151 scene: &mut AgentScene, 152 focus_queue: &FocusQueue, 153 model_config: &ModelConfig, 154 is_interrupt_pending: bool, 155 auto_steal_focus: bool, 156 app_ctx: &mut notedeck::AppContext, 157 ui: &mut egui::Ui, 158 ) -> (DaveResponse, SceneViewAction) { 159 use egui_extras::{Size, StripBuilder}; 160 161 let mut dave_response = DaveResponse::default(); 162 let mut scene_response_opt: Option<SceneResponse> = None; 163 let mut view_action = SceneViewAction::None; 164 165 let ctrl_held = ui.input(|i| i.modifiers.ctrl); 166 167 StripBuilder::new(ui) 168 .size(Size::relative(0.25)) 169 .size(Size::remainder()) 170 .clip(true) 171 .horizontal(|mut strip| { 172 strip.cell(|ui| { 173 ui.horizontal(|ui| { 174 if ui 175 .button("+ New Agent") 176 .on_hover_text("Hold Ctrl to see keybindings") 177 .clicked() 178 { 179 view_action = SceneViewAction::SpawnAgent; 180 } 181 if ctrl_held { 182 keybind_hint(ui, "N"); 183 } 184 ui.separator(); 185 if ui 186 .button("List View") 187 .on_hover_text("Ctrl+L to toggle views") 188 .clicked() 189 { 190 view_action = SceneViewAction::ToggleToListView; 191 } 192 if ctrl_held { 193 keybind_hint(ui, "L"); 194 } 195 }); 196 ui.separator(); 197 scene_response_opt = Some(scene.ui(session_manager, focus_queue, ui, ctrl_held)); 198 }); 199 200 strip.cell(|ui| { 201 egui::Frame::new() 202 .fill(ui.visuals().faint_bg_color) 203 .inner_margin(egui::Margin::symmetric(8, 12)) 204 .show(ui, |ui| { 205 if let Some(selected_id) = scene.primary_selection() { 206 if let Some(session) = session_manager.get_mut(selected_id) { 207 ui.heading(&session.title); 208 ui.separator(); 209 210 let is_working = session.status() == AgentStatus::Working; 211 let has_pending_permission = session.has_pending_permissions(); 212 let plan_mode_active = session.is_plan_mode(); 213 214 let mut ui_builder = DaveUi::new( 215 model_config.trial, 216 &session.chat, 217 &mut session.input, 218 &mut session.focus_requested, 219 session.ai_mode, 220 ) 221 .compact(true) 222 .is_working(is_working) 223 .interrupt_pending(is_interrupt_pending) 224 .has_pending_permission(has_pending_permission) 225 .plan_mode_active(plan_mode_active) 226 .auto_steal_focus(auto_steal_focus); 227 228 if let Some(agentic) = &mut session.agentic { 229 ui_builder = ui_builder 230 .permission_message_state(agentic.permission_message_state) 231 .question_answers(&mut agentic.question_answers) 232 .question_index(&mut agentic.question_index) 233 .is_compacting(agentic.is_compacting); 234 } 235 236 let response = ui_builder.ui(app_ctx, ui); 237 if response.action.is_some() { 238 dave_response = response; 239 } 240 } 241 } else { 242 ui.centered_and_justified(|ui| { 243 ui.label("Select an agent to view chat"); 244 }); 245 } 246 }); 247 }); 248 }); 249 250 // Handle scene actions 251 if let Some(response) = scene_response_opt { 252 if let Some(action) = response.action { 253 match action { 254 SceneAction::SelectionChanged(ids) => { 255 if let Some(id) = ids.first() { 256 session_manager.switch_to(*id); 257 } 258 } 259 SceneAction::SpawnAgent => { 260 view_action = SceneViewAction::SpawnAgent; 261 } 262 SceneAction::DeleteSelected => { 263 view_action = SceneViewAction::DeleteSelected(scene.selected.clone()); 264 } 265 SceneAction::AgentMoved { id, position } => { 266 if let Some(session) = session_manager.get_mut(id) { 267 if let Some(agentic) = &mut session.agentic { 268 agentic.scene_position = position; 269 } 270 } 271 } 272 } 273 } 274 } 275 276 (dave_response, view_action) 277 } 278 279 /// Desktop layout with sidebar for session list. 280 #[allow(clippy::too_many_arguments)] 281 pub fn desktop_ui( 282 session_manager: &mut SessionManager, 283 focus_queue: &FocusQueue, 284 model_config: &ModelConfig, 285 is_interrupt_pending: bool, 286 auto_steal_focus: bool, 287 ai_mode: AiMode, 288 app_ctx: &mut notedeck::AppContext, 289 ui: &mut egui::Ui, 290 ) -> (DaveResponse, Option<SessionListAction>, bool) { 291 let available = ui.available_rect_before_wrap(); 292 let sidebar_width = 280.0; 293 let ctrl_held = ui.input(|i| i.modifiers.ctrl); 294 let mut toggle_scene = false; 295 296 let sidebar_rect = 297 egui::Rect::from_min_size(available.min, egui::vec2(sidebar_width, available.height())); 298 let chat_rect = egui::Rect::from_min_size( 299 egui::pos2(available.min.x + sidebar_width, available.min.y), 300 egui::vec2(available.width() - sidebar_width, available.height()), 301 ); 302 303 let session_action = ui 304 .allocate_new_ui(egui::UiBuilder::new().max_rect(sidebar_rect), |ui| { 305 egui::Frame::new() 306 .fill(ui.visuals().faint_bg_color) 307 .inner_margin(egui::Margin::symmetric(8, 12)) 308 .show(ui, |ui| { 309 if ai_mode == AiMode::Agentic { 310 ui.horizontal(|ui| { 311 if ui 312 .button("Scene View") 313 .on_hover_text("Ctrl+L to toggle views") 314 .clicked() 315 { 316 toggle_scene = true; 317 } 318 if ctrl_held { 319 keybind_hint(ui, "L"); 320 } 321 }); 322 ui.separator(); 323 } 324 SessionListUi::new(session_manager, focus_queue, ctrl_held, ai_mode).ui(ui) 325 }) 326 .inner 327 }) 328 .inner; 329 330 let chat_response = ui 331 .allocate_new_ui(egui::UiBuilder::new().max_rect(chat_rect), |ui| { 332 if let Some(session) = session_manager.get_active_mut() { 333 let is_working = session.status() == AgentStatus::Working; 334 let has_pending_permission = session.has_pending_permissions(); 335 let plan_mode_active = session.is_plan_mode(); 336 337 let mut ui_builder = DaveUi::new( 338 model_config.trial, 339 &session.chat, 340 &mut session.input, 341 &mut session.focus_requested, 342 session.ai_mode, 343 ) 344 .is_working(is_working) 345 .interrupt_pending(is_interrupt_pending) 346 .has_pending_permission(has_pending_permission) 347 .plan_mode_active(plan_mode_active) 348 .auto_steal_focus(auto_steal_focus); 349 350 if let Some(agentic) = &mut session.agentic { 351 ui_builder = ui_builder 352 .permission_message_state(agentic.permission_message_state) 353 .question_answers(&mut agentic.question_answers) 354 .question_index(&mut agentic.question_index) 355 .is_compacting(agentic.is_compacting); 356 } 357 358 ui_builder.ui(app_ctx, ui) 359 } else { 360 DaveResponse::default() 361 } 362 }) 363 .inner; 364 365 (chat_response, session_action, toggle_scene) 366 } 367 368 /// Narrow/mobile layout - shows either session list or chat. 369 #[allow(clippy::too_many_arguments)] 370 pub fn narrow_ui( 371 session_manager: &mut SessionManager, 372 focus_queue: &FocusQueue, 373 model_config: &ModelConfig, 374 is_interrupt_pending: bool, 375 auto_steal_focus: bool, 376 ai_mode: AiMode, 377 show_session_list: bool, 378 app_ctx: &mut notedeck::AppContext, 379 ui: &mut egui::Ui, 380 ) -> (DaveResponse, Option<SessionListAction>) { 381 if show_session_list { 382 let ctrl_held = ui.input(|i| i.modifiers.ctrl); 383 let session_action = egui::Frame::new() 384 .fill(ui.visuals().faint_bg_color) 385 .inner_margin(egui::Margin::symmetric(8, 12)) 386 .show(ui, |ui| { 387 SessionListUi::new(session_manager, focus_queue, ctrl_held, ai_mode).ui(ui) 388 }) 389 .inner; 390 (DaveResponse::default(), session_action) 391 } else if let Some(session) = session_manager.get_active_mut() { 392 let is_working = session.status() == AgentStatus::Working; 393 let has_pending_permission = session.has_pending_permissions(); 394 let plan_mode_active = session.is_plan_mode(); 395 396 let mut ui_builder = DaveUi::new( 397 model_config.trial, 398 &session.chat, 399 &mut session.input, 400 &mut session.focus_requested, 401 session.ai_mode, 402 ) 403 .is_working(is_working) 404 .interrupt_pending(is_interrupt_pending) 405 .has_pending_permission(has_pending_permission) 406 .plan_mode_active(plan_mode_active) 407 .auto_steal_focus(auto_steal_focus); 408 409 if let Some(agentic) = &mut session.agentic { 410 ui_builder = ui_builder 411 .permission_message_state(agentic.permission_message_state) 412 .question_answers(&mut agentic.question_answers) 413 .question_index(&mut agentic.question_index) 414 .is_compacting(agentic.is_compacting); 415 } 416 417 (ui_builder.ui(app_ctx, ui), None) 418 } else { 419 (DaveResponse::default(), None) 420 } 421 } 422 423 /// Result from handling a key action 424 pub enum KeyActionResult { 425 None, 426 ToggleView, 427 HandleInterrupt, 428 CloneAgent, 429 DeleteSession(SessionId), 430 SetAutoSteal(bool), 431 } 432 433 /// Handle a keybinding action. 434 #[allow(clippy::too_many_arguments)] 435 pub fn handle_key_action( 436 key_action: KeyAction, 437 session_manager: &mut SessionManager, 438 scene: &mut AgentScene, 439 focus_queue: &mut FocusQueue, 440 backend: &dyn crate::backend::AiBackend, 441 show_scene: bool, 442 auto_steal_focus: bool, 443 home_session: &mut Option<SessionId>, 444 active_overlay: &mut DaveOverlay, 445 ctx: &egui::Context, 446 ) -> KeyActionResult { 447 match key_action { 448 KeyAction::AcceptPermission => { 449 if let Some(request_id) = update::first_pending_permission(session_manager) { 450 update::handle_permission_response( 451 session_manager, 452 request_id, 453 PermissionResponse::Allow { message: None }, 454 ); 455 if let Some(session) = session_manager.get_active_mut() { 456 session.focus_requested = true; 457 } 458 } 459 KeyActionResult::None 460 } 461 KeyAction::DenyPermission => { 462 if let Some(request_id) = update::first_pending_permission(session_manager) { 463 update::handle_permission_response( 464 session_manager, 465 request_id, 466 PermissionResponse::Deny { 467 reason: "User denied".into(), 468 }, 469 ); 470 if let Some(session) = session_manager.get_active_mut() { 471 session.focus_requested = true; 472 } 473 } 474 KeyActionResult::None 475 } 476 KeyAction::TentativeAccept => { 477 if let Some(session) = session_manager.get_active_mut() { 478 if let Some(agentic) = &mut session.agentic { 479 agentic.permission_message_state = PermissionMessageState::TentativeAccept; 480 } 481 session.focus_requested = true; 482 } 483 KeyActionResult::None 484 } 485 KeyAction::TentativeDeny => { 486 if let Some(session) = session_manager.get_active_mut() { 487 if let Some(agentic) = &mut session.agentic { 488 agentic.permission_message_state = PermissionMessageState::TentativeDeny; 489 } 490 session.focus_requested = true; 491 } 492 KeyActionResult::None 493 } 494 KeyAction::CancelTentative => { 495 if let Some(session) = session_manager.get_active_mut() { 496 if let Some(agentic) = &mut session.agentic { 497 agentic.permission_message_state = PermissionMessageState::None; 498 } 499 } 500 KeyActionResult::None 501 } 502 KeyAction::SwitchToAgent(index) => { 503 update::switch_to_agent_by_index(session_manager, scene, show_scene, index); 504 KeyActionResult::None 505 } 506 KeyAction::NextAgent => { 507 update::cycle_next_agent(session_manager, scene, show_scene); 508 KeyActionResult::None 509 } 510 KeyAction::PreviousAgent => { 511 update::cycle_prev_agent(session_manager, scene, show_scene); 512 KeyActionResult::None 513 } 514 KeyAction::NewAgent => { 515 *active_overlay = DaveOverlay::DirectoryPicker; 516 KeyActionResult::None 517 } 518 KeyAction::CloneAgent => KeyActionResult::CloneAgent, 519 KeyAction::Interrupt => KeyActionResult::HandleInterrupt, 520 KeyAction::ToggleView => KeyActionResult::ToggleView, 521 KeyAction::TogglePlanMode => { 522 update::toggle_plan_mode(session_manager, backend, ctx); 523 if let Some(session) = session_manager.get_active_mut() { 524 session.focus_requested = true; 525 } 526 KeyActionResult::None 527 } 528 KeyAction::DeleteActiveSession => { 529 if let Some(id) = session_manager.active_id() { 530 KeyActionResult::DeleteSession(id) 531 } else { 532 KeyActionResult::None 533 } 534 } 535 KeyAction::FocusQueueNext => { 536 update::focus_queue_next(session_manager, focus_queue, scene, show_scene); 537 KeyActionResult::None 538 } 539 KeyAction::FocusQueuePrev => { 540 update::focus_queue_prev(session_manager, focus_queue, scene, show_scene); 541 KeyActionResult::None 542 } 543 KeyAction::FocusQueueToggleDone => { 544 update::focus_queue_toggle_done(focus_queue); 545 KeyActionResult::None 546 } 547 KeyAction::ToggleAutoSteal => { 548 let new_state = update::toggle_auto_steal( 549 session_manager, 550 scene, 551 show_scene, 552 auto_steal_focus, 553 home_session, 554 ); 555 KeyActionResult::SetAutoSteal(new_state) 556 } 557 KeyAction::OpenExternalEditor => { 558 update::open_external_editor(session_manager); 559 KeyActionResult::None 560 } 561 } 562 } 563 564 /// Result from handling a send action 565 pub enum SendActionResult { 566 /// Permission response was sent, no further action needed 567 Handled, 568 /// Normal send - caller should send the user message 569 SendMessage, 570 } 571 572 /// Handle the Send action, including tentative permission states. 573 pub fn handle_send_action( 574 session_manager: &mut SessionManager, 575 backend: &dyn crate::backend::AiBackend, 576 ctx: &egui::Context, 577 ) -> SendActionResult { 578 let tentative_state = session_manager 579 .get_active() 580 .and_then(|s| s.agentic.as_ref()) 581 .map(|a| a.permission_message_state) 582 .unwrap_or(PermissionMessageState::None); 583 584 match tentative_state { 585 PermissionMessageState::TentativeAccept => { 586 let is_exit_plan_mode = update::has_pending_exit_plan_mode(session_manager); 587 if let Some(request_id) = update::first_pending_permission(session_manager) { 588 let message = session_manager 589 .get_active() 590 .map(|s| s.input.clone()) 591 .filter(|m| !m.is_empty()); 592 if let Some(session) = session_manager.get_active_mut() { 593 session.input.clear(); 594 } 595 if is_exit_plan_mode { 596 update::exit_plan_mode(session_manager, backend, ctx); 597 } 598 update::handle_permission_response( 599 session_manager, 600 request_id, 601 PermissionResponse::Allow { message }, 602 ); 603 } 604 SendActionResult::Handled 605 } 606 PermissionMessageState::TentativeDeny => { 607 if let Some(request_id) = update::first_pending_permission(session_manager) { 608 let reason = session_manager 609 .get_active() 610 .map(|s| s.input.clone()) 611 .filter(|m| !m.is_empty()) 612 .unwrap_or_else(|| "User denied".into()); 613 if let Some(session) = session_manager.get_active_mut() { 614 session.input.clear(); 615 } 616 update::handle_permission_response( 617 session_manager, 618 request_id, 619 PermissionResponse::Deny { reason }, 620 ); 621 } 622 SendActionResult::Handled 623 } 624 PermissionMessageState::None => SendActionResult::SendMessage, 625 } 626 } 627 628 /// Result from handling a UI action 629 pub enum UiActionResult { 630 /// Action was fully handled 631 Handled, 632 /// Send action - caller should handle send 633 SendAction, 634 /// Return an AppAction 635 AppAction(notedeck::AppAction), 636 } 637 638 /// Handle a UI action from DaveUi. 639 #[allow(clippy::too_many_arguments)] 640 pub fn handle_ui_action( 641 action: DaveAction, 642 session_manager: &mut SessionManager, 643 backend: &dyn crate::backend::AiBackend, 644 active_overlay: &mut DaveOverlay, 645 show_session_list: &mut bool, 646 ctx: &egui::Context, 647 ) -> UiActionResult { 648 match action { 649 DaveAction::ToggleChrome => UiActionResult::AppAction(notedeck::AppAction::ToggleChrome), 650 DaveAction::Note(n) => UiActionResult::AppAction(notedeck::AppAction::Note(n)), 651 DaveAction::NewChat => { 652 *active_overlay = DaveOverlay::DirectoryPicker; 653 UiActionResult::Handled 654 } 655 DaveAction::Send => UiActionResult::SendAction, 656 DaveAction::ShowSessionList => { 657 *show_session_list = !*show_session_list; 658 UiActionResult::Handled 659 } 660 DaveAction::OpenSettings => { 661 *active_overlay = DaveOverlay::Settings; 662 UiActionResult::Handled 663 } 664 DaveAction::UpdateSettings(_settings) => UiActionResult::Handled, 665 DaveAction::PermissionResponse { 666 request_id, 667 response, 668 } => { 669 update::handle_permission_response(session_manager, request_id, response); 670 UiActionResult::Handled 671 } 672 DaveAction::Interrupt => { 673 update::execute_interrupt(session_manager, backend, ctx); 674 UiActionResult::Handled 675 } 676 DaveAction::TentativeAccept => { 677 if let Some(session) = session_manager.get_active_mut() { 678 if let Some(agentic) = &mut session.agentic { 679 agentic.permission_message_state = PermissionMessageState::TentativeAccept; 680 } 681 session.focus_requested = true; 682 } 683 UiActionResult::Handled 684 } 685 DaveAction::TentativeDeny => { 686 if let Some(session) = session_manager.get_active_mut() { 687 if let Some(agentic) = &mut session.agentic { 688 agentic.permission_message_state = PermissionMessageState::TentativeDeny; 689 } 690 session.focus_requested = true; 691 } 692 UiActionResult::Handled 693 } 694 DaveAction::QuestionResponse { 695 request_id, 696 answers, 697 } => { 698 update::handle_question_response(session_manager, request_id, answers); 699 UiActionResult::Handled 700 } 701 DaveAction::ExitPlanMode { 702 request_id, 703 approved, 704 } => { 705 if approved { 706 update::exit_plan_mode(session_manager, backend, ctx); 707 update::handle_permission_response( 708 session_manager, 709 request_id, 710 PermissionResponse::Allow { message: None }, 711 ); 712 } else { 713 update::handle_permission_response( 714 session_manager, 715 request_id, 716 PermissionResponse::Deny { 717 reason: "User rejected plan".into(), 718 }, 719 ); 720 } 721 UiActionResult::Handled 722 } 723 } 724 }