notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }