keybindings.rs (8142B)
1 use crate::config::AiMode; 2 use egui::Key; 3 4 /// Keybinding actions that can be triggered globally 5 #[derive(Debug, Clone, PartialEq)] 6 pub enum KeyAction { 7 /// Accept/Allow a pending permission request 8 AcceptPermission, 9 /// Deny a pending permission request 10 DenyPermission, 11 /// Tentatively accept, waiting for message (Shift+1) 12 TentativeAccept, 13 /// Tentatively deny, waiting for message (Shift+2) 14 TentativeDeny, 15 /// Cancel tentative state (Escape when tentative) 16 CancelTentative, 17 /// Switch to agent by number (0-indexed) 18 SwitchToAgent(usize), 19 /// Cycle to next agent 20 NextAgent, 21 /// Cycle to previous agent 22 PreviousAgent, 23 /// Spawn a new agent (Ctrl+T) 24 NewAgent, 25 /// Interrupt/stop the current AI operation 26 Interrupt, 27 /// Toggle between scene view and classic view 28 ToggleView, 29 /// Toggle plan mode for the active session (Ctrl+M) 30 TogglePlanMode, 31 /// Delete the active session 32 DeleteActiveSession, 33 /// Navigate to next item in focus queue (Ctrl+N) 34 FocusQueueNext, 35 /// Navigate to previous item in focus queue (Ctrl+P) 36 FocusQueuePrev, 37 /// Toggle Done status for current focus queue item (Ctrl+D) 38 FocusQueueToggleDone, 39 /// Toggle auto-steal focus mode (Ctrl+\) 40 ToggleAutoSteal, 41 /// Open external editor for composing input (Ctrl+G) 42 OpenExternalEditor, 43 /// Clone the active agent with the same working directory (Ctrl+Shift+T) 44 CloneAgent, 45 } 46 47 /// Check for keybinding actions. 48 /// Most keybindings use Ctrl modifier to avoid conflicts with text input. 49 /// Exception: 1/2 for permission responses work without Ctrl but only when no text input has focus. 50 /// In Chat mode, agentic-specific keybindings (scene view, plan mode, focus queue) are disabled. 51 pub fn check_keybindings( 52 ctx: &egui::Context, 53 has_pending_permission: bool, 54 has_pending_question: bool, 55 in_tentative_state: bool, 56 ai_mode: AiMode, 57 ) -> Option<KeyAction> { 58 let is_agentic = ai_mode == AiMode::Agentic; 59 60 // Escape in tentative state cancels the tentative mode (agentic only) 61 if is_agentic && in_tentative_state && ctx.input(|i| i.key_pressed(Key::Escape)) { 62 return Some(KeyAction::CancelTentative); 63 } 64 65 // Escape otherwise works to interrupt AI (even when text input has focus) 66 if ctx.input(|i| i.key_pressed(Key::Escape)) { 67 return Some(KeyAction::Interrupt); 68 } 69 70 let ctrl = egui::Modifiers::CTRL; 71 let ctrl_shift = egui::Modifiers::CTRL | egui::Modifiers::SHIFT; 72 73 // Ctrl+Tab / Ctrl+Shift+Tab for cycling through agents/chats 74 // Works even with text input focus since Ctrl modifier makes it unambiguous 75 // IMPORTANT: Check Ctrl+Shift+Tab first because consume_key uses matches_logically 76 // which ignores extra Shift, so Ctrl+Tab would consume Ctrl+Shift+Tab otherwise 77 if let Some(action) = ctx.input_mut(|i| { 78 if i.consume_key(ctrl_shift, Key::Tab) { 79 Some(KeyAction::PreviousAgent) 80 } else if i.consume_key(ctrl, Key::Tab) { 81 Some(KeyAction::NextAgent) 82 } else { 83 None 84 } 85 }) { 86 return Some(action); 87 } 88 89 // Focus queue navigation - agentic only 90 if is_agentic { 91 // Ctrl+N for higher priority (toward NeedsInput) 92 if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::N)) { 93 return Some(KeyAction::FocusQueueNext); 94 } 95 96 // Ctrl+P for lower priority (toward Done) 97 if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::P)) { 98 return Some(KeyAction::FocusQueuePrev); 99 } 100 } 101 102 // Ctrl+Shift+T to clone the active agent (check before Ctrl+T) - agentic only 103 if is_agentic && ctx.input(|i| i.modifiers.matches_exact(ctrl_shift) && i.key_pressed(Key::T)) { 104 return Some(KeyAction::CloneAgent); 105 } 106 107 // Ctrl+T to spawn a new agent/chat 108 if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::T)) { 109 return Some(KeyAction::NewAgent); 110 } 111 112 // Ctrl+L to toggle between scene view and list view - agentic only 113 if is_agentic && ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::L)) { 114 return Some(KeyAction::ToggleView); 115 } 116 117 // Ctrl+G to open external editor for composing input 118 if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::G)) { 119 return Some(KeyAction::OpenExternalEditor); 120 } 121 122 // Ctrl+M to toggle plan mode - agentic only 123 if is_agentic && ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::M)) { 124 return Some(KeyAction::TogglePlanMode); 125 } 126 127 // Ctrl+D to toggle Done status for current focus queue item - agentic only 128 if is_agentic && ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::D)) { 129 return Some(KeyAction::FocusQueueToggleDone); 130 } 131 132 // Ctrl+\ to toggle auto-steal focus mode (Ctrl+Space conflicts with macOS input source switching) - agentic only 133 if is_agentic && ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::Backslash)) 134 { 135 return Some(KeyAction::ToggleAutoSteal); 136 } 137 138 // Delete key to delete active session (only when no text input has focus) 139 if !ctx.wants_keyboard_input() && ctx.input(|i| i.key_pressed(Key::Delete)) { 140 return Some(KeyAction::DeleteActiveSession); 141 } 142 143 // Ctrl+1-9 for switching agents/chats (works even with text input focus) 144 // Check this BEFORE permission bindings so Ctrl+number always switches agents 145 if let Some(action) = ctx.input(|i| { 146 if !i.modifiers.matches_exact(ctrl) { 147 return None; 148 } 149 150 for (idx, key) in [ 151 Key::Num1, 152 Key::Num2, 153 Key::Num3, 154 Key::Num4, 155 Key::Num5, 156 Key::Num6, 157 Key::Num7, 158 Key::Num8, 159 Key::Num9, 160 ] 161 .iter() 162 .enumerate() 163 { 164 if i.key_pressed(*key) { 165 return Some(KeyAction::SwitchToAgent(idx)); 166 } 167 } 168 169 None 170 }) { 171 return Some(action); 172 } 173 174 // Permission keybindings - agentic only 175 // When there's a pending permission (but NOT an AskUserQuestion): 176 // - 1 = accept, 2 = deny (no modifiers) 177 // - Shift+1 = tentative accept, Shift+2 = tentative deny (for adding message) 178 // This is checked AFTER Ctrl+number so Ctrl bindings take precedence 179 // IMPORTANT: Only handle these when no text input has focus, to avoid 180 // capturing keypresses when user is typing a message in tentative state 181 // AskUserQuestion uses number keys for option selection, so we skip these bindings 182 if is_agentic && has_pending_permission && !has_pending_question && !ctx.wants_keyboard_input() 183 { 184 // Shift+1 = tentative accept, Shift+2 = tentative deny 185 // Note: egui may report shifted keys as their symbol (e.g., Shift+1 as Exclamationmark) 186 // We check for both the symbol key and Shift+Num key to handle different behaviors 187 if let Some(action) = ctx.input_mut(|i| { 188 // Shift+1: check for '!' (Exclamationmark) which egui reports on some systems 189 if i.key_pressed(Key::Exclamationmark) { 190 return Some(KeyAction::TentativeAccept); 191 } 192 // Shift+2: check with shift modifier (egui may report Num2 with shift held) 193 if i.modifiers.shift && i.key_pressed(Key::Num2) { 194 return Some(KeyAction::TentativeDeny); 195 } 196 None 197 }) { 198 return Some(action); 199 } 200 201 // Bare keypresses (no modifiers) for immediate accept/deny 202 if let Some(action) = ctx.input(|i| { 203 if !i.modifiers.any() { 204 if i.key_pressed(Key::Num1) { 205 return Some(KeyAction::AcceptPermission); 206 } else if i.key_pressed(Key::Num2) { 207 return Some(KeyAction::DenyPermission); 208 } 209 } 210 None 211 }) { 212 return Some(action); 213 } 214 } 215 216 None 217 }