@@ -145,7 +145,7 @@ pub const NON_REBINDABLE: &[&str] = &["ctrl+c", "ctrl+d", "ctrl+m"];
145145///
146146/// # Standard Keybindings (Phase 1 Implementation)
147147/// - **Ctrl+L**: Clear current input line (like bash) [Chat context only due to conflict]
148- /// - **Ctrl+A**: Open the model picker
148+ /// - **Ctrl+Shift+ A**: Open the model picker
149149/// - **Ctrl+K**: Open the command palette
150150/// - **Ctrl+U**: Kill input from cursor to start of line (Emacs-style)
151151/// - **Alt+←/Alt+→**: Navigate to previous/next message in transcript
@@ -190,6 +190,7 @@ pub fn default_bindings() -> Vec<ParsedBinding> {
190190 // Line start/end navigation
191191 ( "home" , "goLineStart" , KeyContext :: Chat ) ,
192192 ( "cmd+left" , "goLineStart" , KeyContext :: Chat ) ,
193+ ( "ctrl+a" , "goLineStart" , KeyContext :: Chat ) ,
193194 ( "end" , "goLineEnd" , KeyContext :: Chat ) ,
194195 ( "cmd+right" , "goLineEnd" , KeyContext :: Chat ) ,
195196 ( "ctrl+e" , "goLineEnd" , KeyContext :: Chat ) ,
@@ -240,7 +241,7 @@ pub fn default_bindings() -> Vec<ParsedBinding> {
240241 ( "pagedown" , "scrollDown" , KeyContext :: Chat ) ,
241242
242243 // App shortcuts
243- ( "ctrl+a" , "openModelPicker" , KeyContext :: Chat ) ,
244+ ( "ctrl+shift+ a" , "openModelPicker" , KeyContext :: Chat ) ,
244245 ( "ctrl+k" , "openCommandPalette" , KeyContext :: Chat ) ,
245246
246247 // ========== CONFIRMATION DIALOGS ==========
@@ -456,7 +457,7 @@ impl UserKeybindings {
456457 } )
457458 . collect ( ) ;
458459 Ok ( Self {
459- schema_version : KEYBINDINGS_SCHEMA_VERSION ,
460+ schema_version : 0 ,
460461 bindings,
461462 } )
462463 }
@@ -475,6 +476,22 @@ impl UserKeybindings {
475476 let mut user_customizations: std:: collections:: HashMap < String , Option < String > > =
476477 std:: collections:: HashMap :: new ( ) ;
477478 for binding in & self . bindings {
479+ // Migration: remove old bindings that have changed in defaults
480+ // This distinguishes between "user customized" and "old default that changed"
481+
482+ // Old: ctrl+a -> openModelPicker (moved to ctrl+shift+a)
483+ if binding. chord == "ctrl+a" && binding. action . as_deref ( ) == Some ( "openModelPicker" ) {
484+ continue ;
485+ }
486+
487+ // Old: tab -> togglePreview in Chat context (changed to indent)
488+ if binding. chord == "tab"
489+ && binding. context . as_deref ( ) == Some ( "Chat" )
490+ && binding. action . as_deref ( ) == Some ( "togglePreview" )
491+ {
492+ continue ;
493+ }
494+
478495 user_customizations
479496 . insert ( binding. chord . clone ( ) , binding. action . clone ( ) ) ;
480497 }
@@ -713,12 +730,13 @@ mod tests {
713730 }
714731
715732 #[ test]
716- fn test_default_bindings_map_ctrl_a_and_ctrl_k_to_app_shortcuts ( ) {
733+ fn test_default_bindings_map_ctrl_shift_a_and_ctrl_k_to_app_shortcuts ( ) {
717734 let bindings = default_bindings ( ) ;
718735
719- let ctrl_a = bindings. iter ( ) . find ( |b| {
736+ let ctrl_shift_a = bindings. iter ( ) . find ( |b| {
720737 b. chord . len ( ) == 1
721738 && b. chord [ 0 ] . ctrl
739+ && b. chord [ 0 ] . shift
722740 && b. chord [ 0 ] . key == "a"
723741 && b. context == KeyContext :: Chat
724742 } ) ;
@@ -729,7 +747,7 @@ mod tests {
729747 && b. context == KeyContext :: Chat
730748 } ) ;
731749
732- assert_eq ! ( ctrl_a . and_then( |b| b. action. as_deref( ) ) , Some ( "openModelPicker" ) ) ;
750+ assert_eq ! ( ctrl_shift_a . and_then( |b| b. action. as_deref( ) ) , Some ( "openModelPicker" ) ) ;
733751 assert_eq ! (
734752 ctrl_k. and_then( |b| b. action. as_deref( ) ) ,
735753 Some ( "openCommandPalette" )
@@ -847,4 +865,57 @@ mod tests {
847865 actions. len( )
848866 ) ;
849867 }
868+
869+ #[ test]
870+ fn test_old_format_keybindings_get_upgraded ( ) {
871+ let old_format_json = r#"{
872+ "bindings": [
873+ {
874+ "context": "Chat",
875+ "bindings": {
876+ "ctrl+shift+a": "openModelPicker",
877+ "ctrl+e": "goLineEnd"
878+ }
879+ }
880+ ]
881+ }"# ;
882+
883+ let mut kb = UserKeybindings :: from_json_str ( old_format_json) ;
884+ assert_eq ! ( kb. schema_version, 0 , "Old format should start at version 0" ) ;
885+
886+ kb. smart_merge_with_defaults ( ) ;
887+
888+ assert_eq ! ( kb. schema_version, 1 , "Should be upgraded to version 1" ) ;
889+ assert ! (
890+ kb. bindings. iter( ) . any( |b| b. chord == "meta+left" ) ,
891+ "meta+left (cmd+left) should be added from defaults after merge"
892+ ) ;
893+ assert ! (
894+ kb. bindings. iter( ) . any( |b| b. chord == "ctrl+shift+a" && b. action. as_deref( ) == Some ( "openModelPicker" ) ) ,
895+ "User customization (ctrl+shift+a -> openModelPicker) should be preserved"
896+ ) ;
897+ }
898+
899+ #[ test]
900+ fn test_cmd_left_resolves_to_go_line_start ( ) {
901+ let user = UserKeybindings :: default ( ) ;
902+ let mut resolver = KeybindingResolver :: new ( & user) ;
903+
904+ // Create a keystroke for CMD+Left (SUPER modifier + left arrow)
905+ let keystroke = ParsedKeystroke {
906+ key : "left" . to_string ( ) ,
907+ ctrl : false ,
908+ alt : false ,
909+ shift : false ,
910+ meta : true ,
911+ } ;
912+
913+ let result = resolver. process ( keystroke, & KeyContext :: Chat ) ;
914+ match result {
915+ KeybindingResult :: Action ( action) => {
916+ assert_eq ! ( action, "goLineStart" , "CMD+Left should map to goLineStart" ) ;
917+ }
918+ other => panic ! ( "Expected Action(\" goLineStart\" ), got {:?}" , other) ,
919+ }
920+ }
850921}
0 commit comments