diff --git a/docs/cli/keyboard-shortcuts.md b/docs/cli/keyboard-shortcuts.md
index 265fce8319..831410b59c 100644
--- a/docs/cli/keyboard-shortcuts.md
+++ b/docs/cli/keyboard-shortcuts.md
@@ -19,14 +19,14 @@ available combinations.
| Action | Keys |
| ------------------------------------------- | ------------------------------------------------------------ |
-| Move the cursor to the start of the line. | `Ctrl + A`
`Home (no Ctrl, no Shift)` |
-| Move the cursor to the end of the line. | `Ctrl + E`
`End (no Ctrl, no Shift)` |
-| Move the cursor up one line. | `Up Arrow (no Ctrl, no Cmd)` |
-| Move the cursor down one line. | `Down Arrow (no Ctrl, no Cmd)` |
-| Move the cursor one character to the left. | `Left Arrow (no Ctrl, no Cmd)`
`Ctrl + B` |
-| Move the cursor one character to the right. | `Right Arrow (no Ctrl, no Cmd)`
`Ctrl + F` |
-| Move the cursor one word to the left. | `Ctrl + Left Arrow`
`Cmd + Left Arrow`
`Cmd + B` |
-| Move the cursor one word to the right. | `Ctrl + Right Arrow`
`Cmd + Right Arrow`
`Cmd + F` |
+| Move the cursor to the start of the line. | `Ctrl + A`
`Home (no Shift, Ctrl)` |
+| Move the cursor to the end of the line. | `Ctrl + E`
`End (no Shift, Ctrl)` |
+| Move the cursor up one line. | `Up Arrow (no Shift, Alt, Ctrl, Cmd)` |
+| Move the cursor down one line. | `Down Arrow (no Shift, Alt, Ctrl, Cmd)` |
+| Move the cursor one character to the left. | `Left Arrow (no Shift, Alt, Ctrl, Cmd)`
`Ctrl + B` |
+| Move the cursor one character to the right. | `Right Arrow (no Shift, Alt, Ctrl, Cmd)`
`Ctrl + F` |
+| Move the cursor one word to the left. | `Ctrl + Left Arrow`
`Alt + Left Arrow`
`Alt + B` |
+| Move the cursor one word to the right. | `Ctrl + Right Arrow`
`Alt + Right Arrow`
`Alt + F` |
#### Editing
@@ -35,12 +35,12 @@ available combinations.
| Delete from the cursor to the end of the line. | `Ctrl + K` |
| Delete from the cursor to the start of the line. | `Ctrl + U` |
| Clear all text in the input field. | `Ctrl + C` |
-| Delete the previous word. | `Ctrl + Backspace`
`Cmd + Backspace`
`Ctrl + W` |
-| Delete the next word. | `Ctrl + Delete`
`Cmd + Delete` |
+| Delete the previous word. | `Ctrl + Backspace`
`Alt + Backspace`
`Ctrl + W` |
+| Delete the next word. | `Ctrl + Delete`
`Alt + Delete` |
| Delete the character to the left. | `Backspace`
`Ctrl + H` |
| Delete the character to the right. | `Delete`
`Ctrl + D` |
| Undo the most recent text edit. | `Ctrl + Z (no Shift)` |
-| Redo the most recent undone text edit. | `Ctrl + Shift + Z` |
+| Redo the most recent undone text edit. | `Shift + Ctrl + Z` |
#### Scrolling
@@ -84,12 +84,12 @@ available combinations.
#### Text Input
-| Action | Keys |
-| ---------------------------------------------- | ---------------------------------------------------------------------- |
-| Submit the current prompt. | `Enter (no Ctrl, no Shift, no Cmd)` |
-| Insert a newline without submitting. | `Ctrl + Enter`
`Cmd + Enter`
`Shift + Enter`
`Ctrl + J` |
-| Open the current prompt in an external editor. | `Ctrl + X` |
-| Paste from the clipboard. | `Ctrl + V`
`Cmd + V` |
+| Action | Keys |
+| ---------------------------------------------- | ----------------------------------------------------------------------------------------- |
+| Submit the current prompt. | `Enter (no Shift, Alt, Ctrl, Cmd)` |
+| Insert a newline without submitting. | `Ctrl + Enter`
`Cmd + Enter`
`Alt + Enter`
`Shift + Enter`
`Ctrl + J` |
+| Open the current prompt in an external editor. | `Ctrl + X` |
+| Paste from the clipboard. | `Ctrl + V`
`Cmd + V`
`Alt + V` |
#### App Controls
@@ -98,7 +98,7 @@ available combinations.
| Toggle detailed error information. | `F12` |
| Toggle the full TODO list. | `Ctrl + T` |
| Show IDE context details. | `Ctrl + G` |
-| Toggle Markdown rendering. | `Cmd + M` |
+| Toggle Markdown rendering. | `Alt + M` |
| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` |
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` |
diff --git a/packages/cli/src/config/keyBindings.test.ts b/packages/cli/src/config/keyBindings.test.ts
index ca2f91ea7e..c2abc32d27 100644
--- a/packages/cli/src/config/keyBindings.test.ts
+++ b/packages/cli/src/config/keyBindings.test.ts
@@ -33,14 +33,17 @@ describe('keyBindings config', () => {
expect(binding.key.length).toBeGreaterThan(0);
// Modifier properties should be boolean or undefined
- if (binding.ctrl !== undefined) {
- expect(typeof binding.ctrl).toBe('boolean');
- }
if (binding.shift !== undefined) {
expect(typeof binding.shift).toBe('boolean');
}
- if (binding.command !== undefined) {
- expect(typeof binding.command).toBe('boolean');
+ if (binding.alt !== undefined) {
+ expect(typeof binding.alt).toBe('boolean');
+ }
+ if (binding.ctrl !== undefined) {
+ expect(typeof binding.ctrl).toBe('boolean');
+ }
+ if (binding.cmd !== undefined) {
+ expect(typeof binding.cmd).toBe('boolean');
}
}
}
diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts
index 455c9dba39..553bfeff47 100644
--- a/packages/cli/src/config/keyBindings.ts
+++ b/packages/cli/src/config/keyBindings.ts
@@ -90,12 +90,14 @@ export enum Command {
export interface KeyBinding {
/** The key name (e.g., 'a', 'return', 'tab', 'escape') */
key: string;
- /** Control key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
- ctrl?: boolean;
/** Shift key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
shift?: boolean;
- /** Command/meta key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
- command?: boolean;
+ /** Alt/Option key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
+ alt?: boolean;
+ /** Control key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
+ ctrl?: boolean;
+ /** Command/Windows/Super key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
+ cmd?: boolean;
}
/**
@@ -119,51 +121,54 @@ export const defaultKeyBindings: KeyBindingConfig = {
// Cursor Movement
[Command.HOME]: [
{ key: 'a', ctrl: true },
- { key: 'home', ctrl: false, shift: false },
+ { key: 'home', shift: false, ctrl: false },
],
[Command.END]: [
{ key: 'e', ctrl: true },
- { key: 'end', ctrl: false, shift: false },
+ { key: 'end', shift: false, ctrl: false },
+ ],
+ [Command.MOVE_UP]: [
+ { key: 'up', shift: false, alt: false, ctrl: false, cmd: false },
+ ],
+ [Command.MOVE_DOWN]: [
+ { key: 'down', shift: false, alt: false, ctrl: false, cmd: false },
],
- [Command.MOVE_UP]: [{ key: 'up', ctrl: false, command: false }],
- [Command.MOVE_DOWN]: [{ key: 'down', ctrl: false, command: false }],
[Command.MOVE_LEFT]: [
- { key: 'left', ctrl: false, command: false },
+ { key: 'left', shift: false, alt: false, ctrl: false, cmd: false },
{ key: 'b', ctrl: true },
],
[Command.MOVE_RIGHT]: [
- { key: 'right', ctrl: false, command: false },
+ { key: 'right', shift: false, alt: false, ctrl: false, cmd: false },
{ key: 'f', ctrl: true },
],
[Command.MOVE_WORD_LEFT]: [
{ key: 'left', ctrl: true },
- { key: 'left', command: true },
- { key: 'b', command: true },
+ { key: 'left', alt: true },
+ { key: 'b', alt: true },
],
[Command.MOVE_WORD_RIGHT]: [
{ key: 'right', ctrl: true },
- { key: 'right', command: true },
- { key: 'f', command: true },
+ { key: 'right', alt: true },
+ { key: 'f', alt: true },
],
// Editing
[Command.KILL_LINE_RIGHT]: [{ key: 'k', ctrl: true }],
[Command.KILL_LINE_LEFT]: [{ key: 'u', ctrl: true }],
[Command.CLEAR_INPUT]: [{ key: 'c', ctrl: true }],
- // Added command (meta/alt/option) for mac compatibility
[Command.DELETE_WORD_BACKWARD]: [
{ key: 'backspace', ctrl: true },
- { key: 'backspace', command: true },
+ { key: 'backspace', alt: true },
{ key: 'w', ctrl: true },
],
[Command.DELETE_WORD_FORWARD]: [
{ key: 'delete', ctrl: true },
- { key: 'delete', command: true },
+ { key: 'delete', alt: true },
],
[Command.DELETE_CHAR_LEFT]: [{ key: 'backspace' }, { key: 'h', ctrl: true }],
[Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
- [Command.UNDO]: [{ key: 'z', ctrl: true, shift: false }],
- [Command.REDO]: [{ key: 'z', ctrl: true, shift: true }],
+ [Command.UNDO]: [{ key: 'z', shift: false, ctrl: true }],
+ [Command.REDO]: [{ key: 'z', shift: true, ctrl: true }],
// Scrolling
[Command.SCROLL_UP]: [{ key: 'up', shift: true }],
@@ -180,10 +185,9 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.PAGE_DOWN]: [{ key: 'pagedown' }],
// History & Search
- [Command.HISTORY_UP]: [{ key: 'p', ctrl: true, shift: false }],
- [Command.HISTORY_DOWN]: [{ key: 'n', ctrl: true, shift: false }],
+ [Command.HISTORY_UP]: [{ key: 'p', shift: false, ctrl: true }],
+ [Command.HISTORY_DOWN]: [{ key: 'n', shift: false, ctrl: true }],
[Command.REVERSE_SEARCH]: [{ key: 'r', ctrl: true }],
- // Note: original logic ONLY checked ctrl=false, ignored meta/shift/paste
[Command.SUBMIT_REVERSE_SEARCH]: [{ key: 'return', ctrl: false }],
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab' }],
@@ -203,14 +207,13 @@ export const defaultKeyBindings: KeyBindingConfig = {
// Suggestions & Completions
[Command.ACCEPT_SUGGESTION]: [{ key: 'tab' }, { key: 'return', ctrl: false }],
- // Completion navigation (arrow or Ctrl+P/N)
[Command.COMPLETION_UP]: [
{ key: 'up', shift: false },
- { key: 'p', ctrl: true, shift: false },
+ { key: 'p', shift: false, ctrl: true },
],
[Command.COMPLETION_DOWN]: [
{ key: 'down', shift: false },
- { key: 'n', ctrl: true, shift: false },
+ { key: 'n', shift: false, ctrl: true },
],
[Command.EXPAND_SUGGESTION]: [{ key: 'right' }],
[Command.COLLAPSE_SUGGESTION]: [{ key: 'left' }],
@@ -220,30 +223,31 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.SUBMIT]: [
{
key: 'return',
- ctrl: false,
- command: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
},
],
- // Split into multiple data-driven bindings
- // Now also includes shift+enter for multi-line input
[Command.NEWLINE]: [
{ key: 'return', ctrl: true },
- { key: 'return', command: true },
+ { key: 'return', cmd: true },
+ { key: 'return', alt: true },
{ key: 'return', shift: true },
{ key: 'j', ctrl: true },
],
[Command.OPEN_EXTERNAL_EDITOR]: [{ key: 'x', ctrl: true }],
[Command.PASTE_CLIPBOARD]: [
{ key: 'v', ctrl: true },
- { key: 'v', command: true },
+ { key: 'v', cmd: true },
+ { key: 'v', alt: true },
],
// App Controls
[Command.SHOW_ERROR_DETAILS]: [{ key: 'f12' }],
[Command.SHOW_FULL_TODOS]: [{ key: 't', ctrl: true }],
[Command.SHOW_IDE_CONTEXT_DETAIL]: [{ key: 'g', ctrl: true }],
- [Command.TOGGLE_MARKDOWN]: [{ key: 'm', command: true }],
+ [Command.TOGGLE_MARKDOWN]: [{ key: 'm', alt: true }],
[Command.TOGGLE_COPY_MODE]: [{ key: 's', ctrl: true }],
[Command.TOGGLE_YOLO]: [{ key: 'y', ctrl: true }],
[Command.CYCLE_APPROVAL_MODE]: [{ key: 'tab', shift: true }],
diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx
index dc5d4613f7..0e1db23583 100644
--- a/packages/cli/src/ui/AppContainer.test.tsx
+++ b/packages/cli/src/ui/AppContainer.test.tsx
@@ -1660,9 +1660,10 @@ describe('AppContainer State Management', () => {
act(() => {
handleGlobalKeypress({
name: 'c',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
...key,
} as Key);
});
@@ -1870,9 +1871,10 @@ describe('AppContainer State Management', () => {
act(() => {
handleGlobalKeypress({
name: 's',
- ctrl: true,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: true,
+ cmd: false,
insertable: false,
sequence: '\x13',
});
@@ -1896,9 +1898,10 @@ describe('AppContainer State Management', () => {
act(() => {
handleGlobalKeypress({
name: 's',
- ctrl: true,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: true,
+ cmd: false,
insertable: false,
sequence: '\x13',
});
@@ -1910,9 +1913,10 @@ describe('AppContainer State Management', () => {
act(() => {
handleGlobalKeypress({
name: 'any', // Any key should exit copy mode
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: 'a',
});
@@ -1930,9 +1934,10 @@ describe('AppContainer State Management', () => {
act(() => {
handleGlobalKeypress({
name: 's',
- ctrl: true,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: true,
+ cmd: false,
insertable: false,
sequence: '\x13',
});
@@ -1945,9 +1950,10 @@ describe('AppContainer State Management', () => {
act(() => {
handleGlobalKeypress({
name: 'a',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: 'a',
});
diff --git a/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx b/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx
index e752c616a0..ea67bdcf6c 100644
--- a/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx
+++ b/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx
@@ -108,10 +108,10 @@ describe('ApiAuthDialog', () => {
keypressHandler({
name: keyName,
- sequence,
- ctrl: false,
- meta: false,
shift: false,
+ ctrl: false,
+ cmd: false,
+ sequence,
});
expect(expectedCall).toHaveBeenCalledWith(...args);
@@ -137,9 +137,9 @@ describe('ApiAuthDialog', () => {
await keypressHandler({
name: 'c',
- ctrl: true,
- meta: false,
shift: false,
+ ctrl: true,
+ cmd: false,
});
expect(clearApiKey).toHaveBeenCalled();
diff --git a/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx b/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx
index a7e857bd3b..907f1447db 100644
--- a/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx
+++ b/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx
@@ -48,10 +48,10 @@ describe('LoginWithGoogleRestartDialog', () => {
keypressHandler({
name: 'escape',
- sequence: '\u001b',
- ctrl: false,
- meta: false,
shift: false,
+ ctrl: false,
+ cmd: false,
+ sequence: '\u001b',
});
expect(onDismiss).toHaveBeenCalledTimes(1);
@@ -67,10 +67,10 @@ describe('LoginWithGoogleRestartDialog', () => {
keypressHandler({
name: keyName,
- sequence: keyName,
- ctrl: false,
- meta: false,
shift: false,
+ ctrl: false,
+ cmd: false,
+ sequence: keyName,
});
// Advance timers to trigger the setTimeout callback
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index b81dc4a601..064bb60d31 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -851,8 +851,9 @@ export const InputPrompt: React.FC = ({
completion.promptCompletion.text &&
key.sequence &&
key.sequence.length === 1 &&
+ !key.alt &&
!key.ctrl &&
- !key.meta
+ !key.cmd
) {
completion.promptCompletion.clear();
setExpandedSuggestionIndex(-1);
diff --git a/packages/cli/src/ui/components/MultiFolderTrustDialog.test.tsx b/packages/cli/src/ui/components/MultiFolderTrustDialog.test.tsx
index 9ec91f53c4..c03c36bf10 100644
--- a/packages/cli/src/ui/components/MultiFolderTrustDialog.test.tsx
+++ b/packages/cli/src/ui/components/MultiFolderTrustDialog.test.tsx
@@ -91,9 +91,10 @@ describe('MultiFolderTrustDialog', () => {
await act(async () => {
keypressCallback({
name: 'escape',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
sequence: '',
insertable: false,
});
diff --git a/packages/cli/src/ui/components/SessionBrowser.test.tsx b/packages/cli/src/ui/components/SessionBrowser.test.tsx
index 3fa4da896d..5a461a551e 100644
--- a/packages/cli/src/ui/components/SessionBrowser.test.tsx
+++ b/packages/cli/src/ui/components/SessionBrowser.test.tsx
@@ -93,10 +93,10 @@ const createMockConfig = (overrides: Partial = {}): Config =>
const triggerKey = (
partialKey: Partial<{
name: string;
- ctrl: boolean;
- meta: boolean;
shift: boolean;
- paste: boolean;
+ alt: boolean;
+ ctrl: boolean;
+ cmd: boolean;
insertable: boolean;
sequence: string;
}>,
@@ -108,9 +108,10 @@ const triggerKey = (
const key = {
name: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: false,
sequence: '',
...partialKey,
@@ -263,7 +264,13 @@ describe('SessionBrowser component', () => {
// Type the query "query".
for (const ch of ['q', 'u', 'e', 'r', 'y']) {
- triggerKey({ sequence: ch, name: ch, ctrl: false, meta: false });
+ triggerKey({
+ sequence: ch,
+ name: ch,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ });
}
await waitFor(() => {
diff --git a/packages/cli/src/ui/components/SessionBrowser.tsx b/packages/cli/src/ui/components/SessionBrowser.tsx
index 835028146a..9e5836057c 100644
--- a/packages/cli/src/ui/components/SessionBrowser.tsx
+++ b/packages/cli/src/ui/components/SessionBrowser.tsx
@@ -781,9 +781,10 @@ export const useSessionBrowserInput = (
state.setScrollOffset(0);
} else if (
key.sequence &&
+ key.sequence.length === 1 &&
+ !key.alt &&
!key.ctrl &&
- !key.meta &&
- key.sequence.length === 1
+ !key.cmd
) {
state.setSearchQuery((prev) => prev + key.sequence);
state.setActiveIndex(0);
diff --git a/packages/cli/src/ui/components/ShellInputPrompt.test.tsx b/packages/cli/src/ui/components/ShellInputPrompt.test.tsx
index 815cfcadf7..5a204b0580 100644
--- a/packages/cli/src/ui/components/ShellInputPrompt.test.tsx
+++ b/packages/cli/src/ui/components/ShellInputPrompt.test.tsx
@@ -53,7 +53,14 @@ describe('ShellInputPrompt', () => {
const handler = mockUseKeypress.mock.calls[0][0];
// Simulate keypress
- handler({ name, sequence, ctrl: false, shift: false, meta: false });
+ handler({
+ name,
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence,
+ });
expect(mockWriteToPty).toHaveBeenCalledWith(1, sequence);
});
@@ -66,7 +73,7 @@ describe('ShellInputPrompt', () => {
const handler = mockUseKeypress.mock.calls[0][0];
- handler({ name: key, ctrl: true, shift: true, meta: false });
+ handler({ name: key, shift: true, alt: false, ctrl: true, cmd: false });
expect(mockScrollPty).toHaveBeenCalledWith(1, direction);
});
@@ -78,10 +85,11 @@ describe('ShellInputPrompt', () => {
handler({
name: 'a',
- sequence: 'a',
- ctrl: false,
shift: false,
- meta: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: 'a',
});
expect(mockWriteToPty).not.toHaveBeenCalled();
@@ -94,10 +102,11 @@ describe('ShellInputPrompt', () => {
handler({
name: 'a',
- sequence: 'a',
- ctrl: false,
shift: false,
- meta: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: 'a',
});
expect(mockWriteToPty).not.toHaveBeenCalled();
diff --git a/packages/cli/src/ui/components/shared/TextInput.test.tsx b/packages/cli/src/ui/components/shared/TextInput.test.tsx
index 56c3edbd37..d32480fc5b 100644
--- a/packages/cli/src/ui/components/shared/TextInput.test.tsx
+++ b/packages/cli/src/ui/components/shared/TextInput.test.tsx
@@ -151,18 +151,20 @@ describe('TextInput', () => {
keypressHandler({
name: 'a',
- sequence: 'a',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: 'a',
});
expect(mockBuffer.handleInput).toHaveBeenCalledWith({
name: 'a',
- sequence: 'a',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: 'a',
});
expect(mockBuffer.text).toBe('a');
});
@@ -176,18 +178,20 @@ describe('TextInput', () => {
keypressHandler({
name: 'backspace',
- sequence: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: '',
});
expect(mockBuffer.handleInput).toHaveBeenCalledWith({
name: 'backspace',
- sequence: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: '',
});
expect(mockBuffer.text).toBe('tes');
});
@@ -201,10 +205,11 @@ describe('TextInput', () => {
keypressHandler({
name: 'left',
- sequence: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: '',
});
// Cursor moves from end to before 't'
@@ -221,10 +226,11 @@ describe('TextInput', () => {
keypressHandler({
name: 'right',
- sequence: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: '',
});
expect(mockBuffer.visualCursor[1]).toBe(3);
@@ -239,10 +245,11 @@ describe('TextInput', () => {
keypressHandler({
name: 'return',
- sequence: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: '',
});
expect(onSubmit).toHaveBeenCalledWith('test');
@@ -257,10 +264,11 @@ describe('TextInput', () => {
keypressHandler({
name: 'escape',
- sequence: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ sequence: '',
});
await vi.runAllTimersAsync();
diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts
index fbad68a1ed..308e7ea89e 100644
--- a/packages/cli/src/ui/components/shared/text-buffer.test.ts
+++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts
@@ -1059,9 +1059,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'h',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: 'h',
}),
@@ -1069,9 +1070,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'i',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: 'i',
}),
@@ -1086,9 +1088,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'return',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: '\r',
}),
@@ -1103,9 +1106,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'j',
- ctrl: true,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: true,
+ cmd: false,
insertable: false,
sequence: '\n',
}),
@@ -1120,9 +1124,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'tab',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: false,
sequence: '\t',
}),
@@ -1137,9 +1142,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'tab',
- ctrl: false,
- meta: false,
shift: true,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: false,
sequence: '\u001b[9;2u',
}),
@@ -1159,9 +1165,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'backspace',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: false,
sequence: '\x7f',
}),
@@ -1183,25 +1190,28 @@ describe('useTextBuffer', () => {
act(() => {
result.current.handleInput({
name: 'backspace',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: false,
sequence: '\x7f',
});
result.current.handleInput({
name: 'backspace',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: false,
sequence: '\x7f',
});
result.current.handleInput({
name: 'backspace',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: false,
sequence: '\x7f',
});
@@ -1258,24 +1268,26 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'left',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: false,
sequence: '\x1b[D',
}),
- ); // cursor [0,1]
+ );
expect(getBufferState(result).cursor).toEqual([0, 1]);
act(() =>
result.current.handleInput({
name: 'right',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: false,
sequence: '\x1b[C',
}),
- ); // cursor [0,2]
+ );
expect(getBufferState(result).cursor).toEqual([0, 2]);
});
@@ -1288,9 +1300,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: textWithAnsi,
}),
@@ -1305,9 +1318,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'return',
- ctrl: false,
- meta: false,
shift: true,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: '\r',
}),
@@ -1509,13 +1523,13 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
describe('Input Sanitization', () => {
const createInput = (sequence: string) => ({
name: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence,
});
-
it.each([
{
input: '\x1B[31mHello\x1B[0m \x1B[32mWorld\x1B[0m',
@@ -1567,9 +1581,10 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
act(() =>
result.current.handleInput({
name: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: largeTextWithUnsafe,
}),
@@ -1601,9 +1616,10 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
act(() =>
result.current.handleInput({
name: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: largeTextWithAnsi,
}),
@@ -1625,9 +1641,10 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
act(() =>
result.current.handleInput({
name: '',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: emojis,
}),
@@ -1816,9 +1833,10 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
act(() =>
result.current.handleInput({
name: 'return',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: '\r',
}),
@@ -1837,9 +1855,10 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
act(() =>
result.current.handleInput({
name: 'f1',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: false,
sequence: '\u001bOP',
}),
diff --git a/packages/cli/src/ui/constants/tips.ts b/packages/cli/src/ui/constants/tips.ts
index e3c06e02b6..db6f0e2df9 100644
--- a/packages/cli/src/ui/constants/tips.ts
+++ b/packages/cli/src/ui/constants/tips.ts
@@ -91,7 +91,7 @@ export const INFORMATIVE_TIPS = [
'See full, untruncated responses with Ctrl+S…',
'Toggle auto-approval (YOLO mode) for all tools with Ctrl+Y…',
'Cycle through approval modes (Default, Plan, Auto-Edit) with Shift+Tab…',
- 'Toggle Markdown rendering (raw markdown mode) with Option+M…',
+ 'Toggle Markdown rendering (raw markdown mode) with Alt+M…',
'Toggle shell mode by typing ! in an empty prompt…',
'Insert a newline with a backslash (\\) followed by Enter…',
'Navigate your prompt history with the Up and Down arrows…',
diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx
index c557706b64..974498e2cd 100644
--- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx
+++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx
@@ -101,9 +101,9 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'return',
- ctrl: false,
- meta: false,
shift: false,
+ ctrl: false,
+ cmd: false,
}),
);
});
@@ -116,9 +116,9 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'return',
- ctrl: false,
- meta: false,
shift: true,
+ ctrl: false,
+ cmd: false,
}),
);
});
@@ -127,17 +127,17 @@ describe('KeypressContext', () => {
{
modifier: 'Shift',
sequence: '\x1b[57414;2u',
- expected: { ctrl: false, meta: false, shift: true },
+ expected: { shift: true, ctrl: false, cmd: false },
},
{
modifier: 'Ctrl',
sequence: '\x1b[57414;5u',
- expected: { ctrl: true, meta: false, shift: false },
+ expected: { shift: false, ctrl: true, cmd: false },
},
{
modifier: 'Alt',
sequence: '\x1b[57414;3u',
- expected: { ctrl: false, meta: true, shift: false },
+ expected: { shift: false, alt: true, ctrl: false, cmd: false },
},
])(
'should handle numpad enter with $modifier modifier',
@@ -163,9 +163,9 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'j',
- ctrl: true,
- meta: false,
shift: false,
+ ctrl: true,
+ cmd: false,
}),
);
});
@@ -178,9 +178,10 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'return',
- ctrl: false,
- meta: true,
shift: false,
+ alt: true,
+ ctrl: false,
+ cmd: false,
}),
);
});
@@ -202,7 +203,13 @@ describe('KeypressContext', () => {
act(() => stdin.write('a'));
expect(keyHandler).toHaveBeenLastCalledWith(
- expect.objectContaining({ name: 'a' }),
+ expect.objectContaining({
+ name: 'a',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ }),
);
act(() => stdin.write('\r'));
@@ -212,6 +219,10 @@ describe('KeypressContext', () => {
name: 'return',
sequence: '\r',
insertable: true,
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
}),
);
});
@@ -228,6 +239,10 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenLastCalledWith(
expect.objectContaining({
name: 'return',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
}),
);
});
@@ -245,6 +260,10 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'escape',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
}),
);
});
@@ -266,11 +285,21 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenNthCalledWith(
1,
- expect.objectContaining({ name: 'escape', meta: true }),
+ expect.objectContaining({
+ name: 'escape',
+ shift: false,
+ alt: true,
+ cmd: false,
+ }),
);
expect(keyHandler).toHaveBeenNthCalledWith(
2,
- expect.objectContaining({ name: 'escape', meta: true }),
+ expect.objectContaining({
+ name: 'escape',
+ shift: false,
+ alt: true,
+ cmd: false,
+ }),
);
});
});
@@ -296,7 +325,9 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'escape',
- meta: true,
+ shift: false,
+ alt: true,
+ cmd: false,
}),
);
});
@@ -318,17 +349,17 @@ describe('KeypressContext', () => {
{
name: 'Backspace',
inputSequence: '\x1b[127u',
- expected: { name: 'backspace', meta: false },
+ expected: { name: 'backspace', alt: false, cmd: false },
},
{
- name: 'Option+Backspace',
+ name: 'Alt+Backspace',
inputSequence: '\x1b[127;3u',
- expected: { name: 'backspace', meta: true },
+ expected: { name: 'backspace', alt: true, cmd: false },
},
{
name: 'Ctrl+Backspace',
inputSequence: '\x1b[127;5u',
- expected: { name: 'backspace', ctrl: true },
+ expected: { name: 'backspace', alt: false, ctrl: true, cmd: false },
},
{
name: 'Shift+Space',
@@ -612,14 +643,17 @@ describe('KeypressContext', () => {
{ sequence: `\x1b[27;5;9~`, expected: { name: 'tab', ctrl: true } },
{
sequence: `\x1b[27;6;9~`,
- expected: { name: 'tab', ctrl: true, shift: true },
+ expected: { name: 'tab', shift: true, ctrl: true },
},
// XTerm Function Key
{ sequence: `\x1b[1;129A`, expected: { name: 'up' } },
{ sequence: `\x1b[1;2H`, expected: { name: 'home', shift: true } },
{ sequence: `\x1b[1;5F`, expected: { name: 'end', ctrl: true } },
{ sequence: `\x1b[1;1P`, expected: { name: 'f1' } },
- { sequence: `\x1b[1;3Q`, expected: { name: 'f2', meta: true } },
+ {
+ sequence: `\x1b[1;3Q`,
+ expected: { name: 'f2', alt: true, cmd: false },
+ },
// Tilde Function Keys
{ sequence: `\x1b[3~`, expected: { name: 'delete' } },
{ sequence: `\x1b[5~`, expected: { name: 'pageup' } },
@@ -637,33 +671,75 @@ describe('KeypressContext', () => {
// Legacy Arrows
{
sequence: `\x1b[A`,
- expected: { name: 'up', ctrl: false, meta: false, shift: false },
+ expected: {
+ name: 'up',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ },
},
{
sequence: `\x1b[B`,
- expected: { name: 'down', ctrl: false, meta: false, shift: false },
+ expected: {
+ name: 'down',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ },
},
{
sequence: `\x1b[C`,
- expected: { name: 'right', ctrl: false, meta: false, shift: false },
+ expected: {
+ name: 'right',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ },
},
{
sequence: `\x1b[D`,
- expected: { name: 'left', ctrl: false, meta: false, shift: false },
+ expected: {
+ name: 'left',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ },
},
// Legacy Home/End
{
sequence: `\x1b[H`,
- expected: { name: 'home', ctrl: false, meta: false, shift: false },
+ expected: {
+ name: 'home',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ },
},
{
sequence: `\x1b[F`,
- expected: { name: 'end', ctrl: false, meta: false, shift: false },
+ expected: {
+ name: 'end',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ },
},
{
sequence: `\x1b[5H`,
- expected: { name: 'home', ctrl: true, meta: false, shift: false },
+ expected: {
+ name: 'home',
+ shift: false,
+ alt: false,
+ ctrl: true,
+ cmd: false,
+ },
},
])(
'should recognize sequence "$sequence" as $expected.name',
@@ -690,11 +766,23 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenNthCalledWith(
1,
- expect.objectContaining({ name: 'delete' }),
+ expect.objectContaining({
+ name: 'delete',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ }),
);
expect(keyHandler).toHaveBeenNthCalledWith(
2,
- expect.objectContaining({ name: 'delete' }),
+ expect.objectContaining({
+ name: 'delete',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ }),
);
});
@@ -751,9 +839,10 @@ describe('KeypressContext', () => {
chunk: `\x1b[${keycode};3u`,
expected: {
name: key,
- ctrl: false,
- meta: true,
shift: false,
+ alt: true,
+ ctrl: false,
+ cmd: false,
},
};
} else if (terminal === 'MacTerminal') {
@@ -766,24 +855,26 @@ describe('KeypressContext', () => {
expected: {
sequence: `\x1b${key}`,
name: key,
- ctrl: false,
- meta: true,
shift: false,
+ alt: true,
+ ctrl: false,
+ cmd: false,
},
};
} else {
// iTerm2 and VSCode send accented characters (å, ø, µ)
- // Note: µ (mu) is sent with meta:false on iTerm2/VSCode but
- // gets converted to m with meta:true
+ // Note: µ (mu) is sent with alt:false on iTerm2/VSCode but
+ // gets converted to m with alt:true
return {
terminal,
key,
chunk: accentedChar,
expected: {
name: key,
- ctrl: false,
- meta: true, // Always expect meta:true after conversion
shift: false,
+ alt: true, // Always expect alt:true after conversion
+ ctrl: false,
+ cmd: false,
sequence: accentedChar,
},
};
@@ -825,7 +916,10 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
sequence: '\\',
- meta: false,
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
}),
);
});
@@ -858,6 +952,10 @@ describe('KeypressContext', () => {
expect.objectContaining({
name: 'undefined',
sequence: INCOMPLETE_KITTY_SEQUENCE,
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
}),
);
});
@@ -876,6 +974,10 @@ describe('KeypressContext', () => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
sequence: '\x1b[m',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
}),
);
});
@@ -1048,6 +1150,10 @@ describe('KeypressContext', () => {
expect.objectContaining({
name: 'a',
sequence: 'a',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
}),
);
});
@@ -1162,7 +1268,14 @@ describe('KeypressContext', () => {
});
expect(keyHandler).toHaveBeenCalledWith(
- expect.objectContaining({ name: 'f12', sequence: '\u001b[24~' }),
+ expect.objectContaining({
+ name: 'f12',
+ sequence: '\u001b[24~',
+ shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
+ }),
);
});
});
diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx
index 88aad71a27..2d5b121b84 100644
--- a/packages/cli/src/ui/contexts/KeypressContext.tsx
+++ b/packages/cli/src/ui/contexts/KeypressContext.tsx
@@ -251,9 +251,10 @@ function bufferPaste(keypressHandler: KeypressHandler): KeypressHandler {
if (buffer.length > 0) {
keypressHandler({
name: 'paste',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: buffer,
});
@@ -300,9 +301,10 @@ function* emitKeys(
let escaped = false;
let name = undefined;
- let ctrl = false;
- let meta = false;
let shift = false;
+ let alt = false;
+ let ctrl = false;
+ let cmd = false;
let code = undefined;
let insertable = false;
@@ -353,9 +355,10 @@ function* emitKeys(
const decoded = Buffer.from(base64Data, 'base64').toString('utf-8');
keypressHandler({
name: 'paste',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: decoded,
});
@@ -490,9 +493,10 @@ function* emitKeys(
}
// Parse the key modifier
- ctrl = !!(modifier & 4);
- meta = !!(modifier & 10); // use 10 to catch both alt (2) and meta (8).
shift = !!(modifier & 1);
+ alt = !!(modifier & 2);
+ ctrl = !!(modifier & 4);
+ cmd = !!(modifier & 8);
const keyInfo = KEY_INFO_MAP[code];
if (keyInfo) {
@@ -503,13 +507,16 @@ function* emitKeys(
if (keyInfo.ctrl) {
ctrl = true;
}
- if (name === 'space' && !ctrl && !meta) {
+ if (name === 'space' && !ctrl && !cmd && !alt) {
sequence = ' ';
insertable = true;
}
} else {
name = 'undefined';
- if ((ctrl || meta) && (code.endsWith('u') || code.endsWith('~'))) {
+ if (
+ (ctrl || cmd || alt) &&
+ (code.endsWith('u') || code.endsWith('~'))
+ ) {
// CSI-u or tilde-coded functional keys: ESC [ ; (u|~)
const codeNumber = parseInt(code.slice(1, -1), 10);
if (
@@ -523,26 +530,26 @@ function* emitKeys(
} else if (ch === '\r') {
// carriage return
name = 'return';
- meta = escaped;
+ alt = escaped;
} else if (escaped && ch === '\n') {
// Alt+Enter (linefeed), should be consistent with carriage return
name = 'return';
- meta = escaped;
+ alt = escaped;
} else if (ch === '\t') {
// tab
name = 'tab';
- meta = escaped;
+ alt = escaped;
} else if (ch === '\b' || ch === '\x7f') {
// backspace or ctrl+h
name = 'backspace';
- meta = escaped;
+ alt = escaped;
} else if (ch === ESC) {
// escape key
name = 'escape';
- meta = escaped;
+ alt = escaped;
} else if (ch === ' ') {
name = 'space';
- meta = escaped;
+ alt = escaped;
insertable = true;
} else if (!escaped && ch <= '\x1a') {
// ctrl+letter
@@ -552,29 +559,30 @@ function* emitKeys(
// Letter, number, shift+letter
name = ch.toLowerCase();
shift = /^[A-Z]$/.exec(ch) !== null;
- meta = escaped;
+ alt = escaped;
insertable = true;
} else if (MAC_ALT_KEY_CHARACTER_MAP[ch] && process.platform === 'darwin') {
name = MAC_ALT_KEY_CHARACTER_MAP[ch];
- meta = true;
+ alt = true;
} else if (sequence === `${ESC}${ESC}`) {
// Double escape
name = 'escape';
- meta = true;
+ alt = true;
// Emit first escape key here, then continue processing
keypressHandler({
name: 'escape',
- ctrl,
- meta,
shift,
+ alt,
+ ctrl,
+ cmd,
insertable: false,
sequence: ESC,
});
} else if (escaped) {
// Escape sequence timeout
name = ch.length ? undefined : 'escape';
- meta = true;
+ alt = true;
} else {
// Any other character is considered printable.
insertable = true;
@@ -586,9 +594,10 @@ function* emitKeys(
) {
keypressHandler({
name: name || '',
- ctrl,
- meta,
shift,
+ alt,
+ ctrl,
+ cmd,
insertable,
sequence,
});
@@ -599,9 +608,10 @@ function* emitKeys(
export interface Key {
name: string;
- ctrl: boolean;
- meta: boolean;
shift: boolean;
+ alt: boolean;
+ ctrl: boolean;
+ cmd: boolean; // Command/Windows/Super key
insertable: boolean;
sequence: string;
}
diff --git a/packages/cli/src/ui/contexts/MouseContext.test.tsx b/packages/cli/src/ui/contexts/MouseContext.test.tsx
index 0224721568..a3bf76a146 100644
--- a/packages/cli/src/ui/contexts/MouseContext.test.tsx
+++ b/packages/cli/src/ui/contexts/MouseContext.test.tsx
@@ -139,63 +139,63 @@ describe('MouseContext', () => {
sequence: '\x1b[<0;10;20M',
expected: {
name: 'left-press',
+ shift: false,
ctrl: false,
meta: false,
- shift: false,
},
},
{
sequence: '\x1b[<0;10;20m',
expected: {
name: 'left-release',
+ shift: false,
ctrl: false,
meta: false,
- shift: false,
},
},
{
sequence: '\x1b[<2;10;20M',
expected: {
name: 'right-press',
+ shift: false,
ctrl: false,
meta: false,
- shift: false,
},
},
{
sequence: '\x1b[<1;10;20M',
expected: {
name: 'middle-press',
+ shift: false,
ctrl: false,
meta: false,
- shift: false,
},
},
{
sequence: '\x1b[<64;10;20M',
expected: {
name: 'scroll-up',
+ shift: false,
ctrl: false,
meta: false,
- shift: false,
},
},
{
sequence: '\x1b[<65;10;20M',
expected: {
name: 'scroll-down',
+ shift: false,
ctrl: false,
meta: false,
- shift: false,
},
},
{
sequence: '\x1b[<32;10;20M',
expected: {
name: 'move',
+ shift: false,
ctrl: false,
meta: false,
- shift: false,
},
},
{
@@ -208,7 +208,7 @@ describe('MouseContext', () => {
}, // Alt + left press
{
sequence: '\x1b[<20;10;20M',
- expected: { name: 'left-press', ctrl: true, shift: true },
+ expected: { name: 'left-press', shift: true, ctrl: true },
}, // Ctrl + Shift + left press
{
sequence: '\x1b[<68;10;20M',
diff --git a/packages/cli/src/ui/hooks/keyToAnsi.ts b/packages/cli/src/ui/hooks/keyToAnsi.ts
index 1d5549ab0f..56d8466a0e 100644
--- a/packages/cli/src/ui/hooks/keyToAnsi.ts
+++ b/packages/cli/src/ui/hooks/keyToAnsi.ts
@@ -69,7 +69,7 @@ export function keyToAnsi(key: Key): string | null {
}
// If it's a simple character, return it.
- if (!key.ctrl && !key.meta && key.sequence) {
+ if (!key.ctrl && !key.cmd && key.sequence) {
return key.sequence;
}
diff --git a/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts b/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts
index 3480791fe5..785e05aa15 100644
--- a/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts
+++ b/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts
@@ -314,8 +314,8 @@ describe('useApprovalModeIndicator', () => {
act(() => {
capturedUseKeypressHandler({
name: 'a',
- ctrl: true,
shift: true,
+ ctrl: true,
} as Key);
});
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
diff --git a/packages/cli/src/ui/hooks/useKeypress.test.tsx b/packages/cli/src/ui/hooks/useKeypress.test.tsx
index 71d901bcb5..cde15186d9 100644
--- a/packages/cli/src/ui/hooks/useKeypress.test.tsx
+++ b/packages/cli/src/ui/hooks/useKeypress.test.tsx
@@ -114,7 +114,13 @@ describe(`useKeypress`, () => {
const key = { name: 'return', sequence: '\x1B\r' };
act(() => stdin.write(key.sequence));
expect(onKeypress).toHaveBeenCalledWith(
- expect.objectContaining({ ...key, meta: true }),
+ expect.objectContaining({
+ ...key,
+ shift: false,
+ alt: true,
+ ctrl: false,
+ cmd: false,
+ }),
);
});
@@ -140,9 +146,10 @@ describe(`useKeypress`, () => {
expect(onKeypress).toHaveBeenCalledTimes(1);
expect(onKeypress).toHaveBeenCalledWith({
name: 'paste',
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: true,
sequence: pasteText,
});
diff --git a/packages/cli/src/ui/hooks/useSelectionList.test.tsx b/packages/cli/src/ui/hooks/useSelectionList.test.tsx
index 7006f0f5d3..7c01e3cb71 100644
--- a/packages/cli/src/ui/hooks/useSelectionList.test.tsx
+++ b/packages/cli/src/ui/hooks/useSelectionList.test.tsx
@@ -59,7 +59,8 @@ describe('useSelectionList', () => {
name,
sequence,
ctrl: options.ctrl ?? false,
- meta: false,
+ cmd: false,
+ alt: false,
shift: options.shift ?? false,
insertable: false,
};
@@ -328,7 +329,8 @@ describe('useSelectionList', () => {
name,
sequence: name,
ctrl: false,
- meta: false,
+ cmd: false,
+ alt: false,
shift: false,
insertable: true,
};
@@ -377,7 +379,8 @@ describe('useSelectionList', () => {
name,
sequence: name,
ctrl: false,
- meta: false,
+ cmd: false,
+ alt: false,
shift: false,
insertable: false,
};
diff --git a/packages/cli/src/ui/hooks/vim.test.tsx b/packages/cli/src/ui/hooks/vim.test.tsx
index db7d287425..88b7ebb415 100644
--- a/packages/cli/src/ui/hooks/vim.test.tsx
+++ b/packages/cli/src/ui/hooks/vim.test.tsx
@@ -36,9 +36,10 @@ vi.mock('../contexts/VimModeContext.js', () => ({
const createKey = (partial: Partial): Key => ({
name: partial.name || '',
sequence: partial.sequence || '',
- ctrl: partial.ctrl || false,
- meta: partial.meta || false,
shift: partial.shift || false,
+ alt: partial.alt || false,
+ ctrl: partial.ctrl || false,
+ cmd: partial.cmd || false,
insertable: partial.insertable || false,
...partial,
});
diff --git a/packages/cli/src/ui/hooks/vim.ts b/packages/cli/src/ui/hooks/vim.ts
index 46492220a0..2f39c38f43 100644
--- a/packages/cli/src/ui/hooks/vim.ts
+++ b/packages/cli/src/ui/hooks/vim.ts
@@ -280,8 +280,9 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
// Special handling for Enter key to allow command submission (lower priority than completion)
if (
normalizedKey.name === 'return' &&
+ !normalizedKey.alt &&
!normalizedKey.ctrl &&
- !normalizedKey.meta
+ !normalizedKey.cmd
) {
if (buffer.text.trim() && onSubmit) {
// Handle command submission directly
@@ -309,9 +310,10 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
(key: Key): Key => ({
name: key.name || '',
sequence: key.sequence || '',
- ctrl: key.ctrl || false,
- meta: key.meta || false,
shift: key.shift || false,
+ alt: key.alt || false,
+ ctrl: key.ctrl || false,
+ cmd: key.cmd || false,
insertable: key.insertable || false,
}),
[],
diff --git a/packages/cli/src/ui/keyMatchers.test.ts b/packages/cli/src/ui/keyMatchers.test.ts
index dc3003f8e4..8c3edfcfb3 100644
--- a/packages/cli/src/ui/keyMatchers.test.ts
+++ b/packages/cli/src/ui/keyMatchers.test.ts
@@ -13,9 +13,10 @@ import type { Key } from './hooks/useKeypress.js';
describe('keyMatchers', () => {
const createKey = (name: string, mods: Partial = {}): Key => ({
name,
- ctrl: false,
- meta: false,
shift: false,
+ alt: false,
+ ctrl: false,
+ cmd: false,
insertable: false,
sequence: name,
...mods,
@@ -70,8 +71,8 @@ describe('keyMatchers', () => {
command: Command.MOVE_WORD_LEFT,
positive: [
createKey('left', { ctrl: true }),
- createKey('left', { meta: true }),
- createKey('b', { meta: true }),
+ createKey('left', { alt: true }),
+ createKey('b', { alt: true }),
],
negative: [createKey('left'), createKey('b', { ctrl: true })],
},
@@ -79,8 +80,8 @@ describe('keyMatchers', () => {
command: Command.MOVE_WORD_RIGHT,
positive: [
createKey('right', { ctrl: true }),
- createKey('right', { meta: true }),
- createKey('f', { meta: true }),
+ createKey('right', { alt: true }),
+ createKey('f', { alt: true }),
],
negative: [createKey('right'), createKey('f', { ctrl: true })],
},
@@ -115,7 +116,7 @@ describe('keyMatchers', () => {
command: Command.DELETE_WORD_BACKWARD,
positive: [
createKey('backspace', { ctrl: true }),
- createKey('backspace', { meta: true }),
+ createKey('backspace', { alt: true }),
createKey('w', { ctrl: true }),
],
negative: [createKey('backspace'), createKey('delete', { ctrl: true })],
@@ -124,19 +125,19 @@ describe('keyMatchers', () => {
command: Command.DELETE_WORD_FORWARD,
positive: [
createKey('delete', { ctrl: true }),
- createKey('delete', { meta: true }),
+ createKey('delete', { alt: true }),
],
negative: [createKey('delete'), createKey('backspace', { ctrl: true })],
},
{
command: Command.UNDO,
- positive: [createKey('z', { ctrl: true, shift: false })],
- negative: [createKey('z'), createKey('z', { ctrl: true, shift: true })],
+ positive: [createKey('z', { shift: false, ctrl: true })],
+ negative: [createKey('z'), createKey('z', { shift: true, ctrl: true })],
},
{
command: Command.REDO,
- positive: [createKey('z', { ctrl: true, shift: true })],
- negative: [createKey('z'), createKey('z', { ctrl: true, shift: false })],
+ positive: [createKey('z', { shift: true, ctrl: true })],
+ negative: [createKey('z'), createKey('z', { shift: false, ctrl: true })],
},
// Screen control
@@ -243,14 +244,16 @@ describe('keyMatchers', () => {
positive: [createKey('return')],
negative: [
createKey('return', { ctrl: true }),
- createKey('return', { meta: true }),
+ createKey('return', { cmd: true }),
+ createKey('return', { alt: true }),
],
},
{
command: Command.NEWLINE,
positive: [
createKey('return', { ctrl: true }),
- createKey('return', { meta: true }),
+ createKey('return', { cmd: true }),
+ createKey('return', { alt: true }),
],
negative: [createKey('return'), createKey('n')],
},
@@ -285,13 +288,13 @@ describe('keyMatchers', () => {
},
{
command: Command.TOGGLE_MARKDOWN,
- positive: [createKey('m', { meta: true })],
+ positive: [createKey('m', { alt: true })],
negative: [createKey('m'), createKey('m', { shift: true })],
},
{
command: Command.TOGGLE_COPY_MODE,
positive: [createKey('s', { ctrl: true })],
- negative: [createKey('s'), createKey('s', { meta: true })],
+ negative: [createKey('s'), createKey('s', { alt: true })],
},
{
command: Command.QUIT,
@@ -333,7 +336,7 @@ describe('keyMatchers', () => {
{
command: Command.TOGGLE_YOLO,
positive: [createKey('y', { ctrl: true })],
- negative: [createKey('y'), createKey('y', { meta: true })],
+ negative: [createKey('y'), createKey('y', { alt: true })],
},
{
command: Command.CYCLE_APPROVAL_MODE,
@@ -401,13 +404,13 @@ describe('keyMatchers', () => {
...defaultKeyBindings,
[Command.QUIT]: [
{ key: 'q', ctrl: true },
- { key: 'q', command: true },
+ { key: 'q', alt: true },
],
};
const matchers = createKeyMatchers(config);
expect(matchers[Command.QUIT](createKey('q', { ctrl: true }))).toBe(true);
- expect(matchers[Command.QUIT](createKey('q', { meta: true }))).toBe(true);
+ expect(matchers[Command.QUIT](createKey('q', { alt: true }))).toBe(true);
});
});
diff --git a/packages/cli/src/ui/keyMatchers.ts b/packages/cli/src/ui/keyMatchers.ts
index 73636130be..07b6acf173 100644
--- a/packages/cli/src/ui/keyMatchers.ts
+++ b/packages/cli/src/ui/keyMatchers.ts
@@ -13,28 +13,17 @@ import { Command, defaultKeyBindings } from '../config/keyBindings.js';
* Pure data-driven matching logic
*/
function matchKeyBinding(keyBinding: KeyBinding, key: Key): boolean {
- if (keyBinding.key !== key.name) {
- return false;
- }
-
// Check modifiers - follow original logic:
// undefined = ignore this modifier (original behavior)
// true = modifier must be pressed
// false = modifier must NOT be pressed
-
- if (keyBinding.ctrl !== undefined && key.ctrl !== keyBinding.ctrl) {
- return false;
- }
-
- if (keyBinding.shift !== undefined && key.shift !== keyBinding.shift) {
- return false;
- }
-
- if (keyBinding.command !== undefined && key.meta !== keyBinding.command) {
- return false;
- }
-
- return true;
+ return (
+ keyBinding.key === key.name &&
+ (keyBinding.shift === undefined || key.shift === keyBinding.shift) &&
+ (keyBinding.alt === undefined || key.alt === keyBinding.alt) &&
+ (keyBinding.ctrl === undefined || key.ctrl === keyBinding.ctrl) &&
+ (keyBinding.cmd === undefined || key.cmd === keyBinding.cmd)
+ );
}
/**
diff --git a/scripts/generate-keybindings-doc.ts b/scripts/generate-keybindings-doc.ts
index 4c2e6c4618..a23dfe530c 100644
--- a/scripts/generate-keybindings-doc.ts
+++ b/scripts/generate-keybindings-doc.ts
@@ -154,9 +154,10 @@ function formatBindings(bindings: readonly KeyBinding[]): string[] {
function formatBinding(binding: KeyBinding): string {
const modifiers: string[] = [];
- if (binding.ctrl) modifiers.push('Ctrl');
- if (binding.command) modifiers.push('Cmd');
if (binding.shift) modifiers.push('Shift');
+ if (binding.alt) modifiers.push('Alt');
+ if (binding.ctrl) modifiers.push('Ctrl');
+ if (binding.cmd) modifiers.push('Cmd');
const keyName = formatKeyName(binding.key);
if (!keyName) {
@@ -167,12 +168,13 @@ function formatBinding(binding: KeyBinding): string {
let combo = segments.join(' + ');
const restrictions: string[] = [];
- if (binding.ctrl === false) restrictions.push('no Ctrl');
- if (binding.shift === false) restrictions.push('no Shift');
- if (binding.command === false) restrictions.push('no Cmd');
+ if (binding.shift === false) restrictions.push('Shift');
+ if (binding.alt === false) restrictions.push('Alt');
+ if (binding.ctrl === false) restrictions.push('Ctrl');
+ if (binding.cmd === false) restrictions.push('Cmd');
if (restrictions.length > 0) {
- combo = `${combo} (${restrictions.join(', ')})`;
+ combo = `${combo} (no ${restrictions.join(', ')})`;
}
return combo ? `\`${combo}\`` : '';
diff --git a/scripts/tests/generate-keybindings-doc.test.ts b/scripts/tests/generate-keybindings-doc.test.ts
index 3f06147d3d..68a166609b 100644
--- a/scripts/tests/generate-keybindings-doc.test.ts
+++ b/scripts/tests/generate-keybindings-doc.test.ts
@@ -36,7 +36,7 @@ describe('generate-keybindings-doc', () => {
},
{
description: 'Submit with Enter if no modifiers are held.',
- bindings: [{ key: 'return', ctrl: false, shift: false }],
+ bindings: [{ key: 'return', shift: false, ctrl: false }],
},
],
},
@@ -47,7 +47,7 @@ describe('generate-keybindings-doc', () => {
description: 'Move up through results.',
bindings: [
{ key: 'up', shift: false },
- { key: 'p', ctrl: true, shift: false },
+ { key: 'p', shift: false, ctrl: true },
],
},
],
@@ -59,7 +59,7 @@ describe('generate-keybindings-doc', () => {
expect(markdown).toContain('Trigger custom action.');
expect(markdown).toContain('`Ctrl + X`');
expect(markdown).toContain('Submit with Enter if no modifiers are held.');
- expect(markdown).toContain('`Enter (no Ctrl, no Shift)`');
+ expect(markdown).toContain('`Enter (no Shift, Ctrl)`');
expect(markdown).toContain('#### Navigation');
expect(markdown).toContain('Move up through results.');
expect(markdown).toContain('`Up Arrow (no Shift)`');