@@ -15,6 +15,14 @@ export interface Keybind {
1515 shift ?: boolean ;
1616 alt ?: boolean ;
1717 meta ?: boolean ;
18+ /**
19+ * On macOS, Ctrl-based shortcuts traditionally use Cmd instead.
20+ * Use this field to control that behavior:
21+ * - "either" (default): accept Ctrl or Cmd
22+ * - "command": require Cmd specifically
23+ * - "control": require the Control key specifically
24+ */
25+ macCtrlBehavior ?: "either" | "command" | "control" ;
1826}
1927
2028/**
@@ -35,7 +43,7 @@ export function isMac(): boolean {
3543
3644/**
3745 * Check if a keyboard event matches a keybind definition.
38- * On macOS, ctrl in the definition matches either ctrl OR meta ( Cmd) in the event .
46+ * On macOS, ctrl in the definition defaults to matching Ctrl or Cmd unless overridden .
3947 */
4048export function matchesKeybind (
4149 event : React . KeyboardEvent | KeyboardEvent ,
@@ -46,23 +54,66 @@ export function matchesKeybind(
4654 return false ;
4755 }
4856
49- // On Mac, treat ctrl and meta as equivalent
50- const ctrlOrMeta = isMac ( ) ? event . ctrlKey || event . metaKey : event . ctrlKey ;
57+ const onMac = isMac ( ) ;
58+ const macCtrlBehavior = keybind . macCtrlBehavior ?? "either" ;
59+ const ctrlPressed = event . ctrlKey ;
60+ const metaPressed = event . metaKey ;
61+
62+ let ctrlRequired = false ;
63+ let ctrlAllowed = false ;
64+ let metaRequired = keybind . meta ?? false ;
65+ let metaAllowed = metaRequired ;
66+
67+ if ( keybind . ctrl ) {
68+ if ( onMac ) {
69+ switch ( macCtrlBehavior ) {
70+ case "control" : {
71+ ctrlRequired = true ;
72+ ctrlAllowed = true ;
73+ // Only allow Cmd if explicitly requested via meta flag
74+ break ;
75+ }
76+ case "command" : {
77+ metaRequired = true ;
78+ metaAllowed = true ;
79+ ctrlAllowed = true ;
80+ break ;
81+ }
82+ case "either" : {
83+ ctrlAllowed = true ;
84+ metaAllowed = true ;
85+ if ( ! ctrlPressed && ! metaPressed ) return false ;
86+ break ;
87+ }
88+ }
89+ } else {
90+ ctrlRequired = true ;
91+ ctrlAllowed = true ;
92+ }
93+ } else {
94+ ctrlAllowed = false ;
95+ }
5196
52- // Check modifiers
53- if ( keybind . ctrl && ! ctrlOrMeta ) return false ;
54- if ( ! keybind . ctrl && ctrlOrMeta ) return false ;
97+ if ( ctrlRequired && ! ctrlPressed ) return false ;
98+ if ( ! ctrlAllowed && ctrlPressed ) return false ;
5599
56100 if ( keybind . shift && ! event . shiftKey ) return false ;
57101 if ( ! keybind . shift && event . shiftKey ) return false ;
58102
59103 if ( keybind . alt && ! event . altKey ) return false ;
60104 if ( ! keybind . alt && event . altKey ) return false ;
61105
62- // meta is explicit (only check when not handled by ctrl equivalence)
63- if ( ! isMac ( ) ) {
64- if ( keybind . meta && ! event . metaKey ) return false ;
65- if ( ! keybind . meta && event . metaKey ) return false ;
106+ if ( metaRequired && ! metaPressed ) return false ;
107+
108+ if ( ! metaAllowed ) {
109+ // If Cmd is allowed implicitly via ctrl behavior, mark it now
110+ if ( onMac && keybind . ctrl && macCtrlBehavior !== "control" ) {
111+ metaAllowed = true ;
112+ }
113+ }
114+
115+ if ( ! metaAllowed && metaPressed ) {
116+ return false ;
66117 }
67118
68119 return true ;
@@ -92,7 +143,12 @@ export function formatKeybind(keybind: Keybind): string {
92143 // Mac-style formatting with symbols (using Unicode escapes for safety)
93144 // For ctrl on Mac, we actually mean Cmd in most cases since matcher treats them as equivalent
94145 if ( keybind . ctrl && ! keybind . meta ) {
95- parts . push ( "\u2318" ) ; // ⌘ Command
146+ const macCtrlBehavior = keybind . macCtrlBehavior ?? "either" ;
147+ if ( macCtrlBehavior === "control" ) {
148+ parts . push ( "\u2303" ) ; // ⌃ Control
149+ } else {
150+ parts . push ( "\u2318" ) ; // ⌘ Command
151+ }
96152 } else if ( keybind . ctrl ) {
97153 parts . push ( "\u2303" ) ; // ⌃ Control
98154 }
@@ -134,7 +190,7 @@ export const KEYBINDS = {
134190 CANCEL : { key : "Escape" } ,
135191
136192 /** Interrupt active stream (destructive - stops AI generation) */
137- INTERRUPT_STREAM : { key : "c" , ctrl : true } ,
193+ INTERRUPT_STREAM : { key : "c" , ctrl : true , macCtrlBehavior : "control" } ,
138194
139195 /** Focus chat input */
140196 FOCUS_INPUT_I : { key : "i" } ,
0 commit comments