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