From 14412c3a727df5a72edf04cd16410e8798fd505d Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Tue, 10 Mar 2026 02:32:40 +0000 Subject: [PATCH 1/4] refactor(cli): rename 'return' key to 'enter' internally (#21796) --- .../cli/src/ui/auth/ApiAuthDialog.test.tsx | 2 +- .../ui/components/ConfigExtensionDialog.tsx | 4 +-- .../cli/src/ui/components/InputPrompt.tsx | 2 +- .../src/ui/components/SessionBrowser.test.tsx | 4 +-- .../cli/src/ui/components/SessionBrowser.tsx | 2 +- .../ui/components/shared/TextInput.test.tsx | 6 ++-- .../ui/components/shared/text-buffer.test.ts | 6 ++-- .../src/ui/contexts/KeypressContext.test.tsx | 18 ++++++------ .../cli/src/ui/contexts/KeypressContext.tsx | 14 +++++----- .../cli/src/ui/hooks/useKeypress.test.tsx | 2 +- .../src/ui/hooks/useSelectionList.test.tsx | 10 +++---- packages/cli/src/ui/hooks/vim.ts | 4 +-- packages/cli/src/ui/key/keyBindings.test.ts | 4 +-- packages/cli/src/ui/key/keyBindings.ts | 25 +++++++---------- packages/cli/src/ui/key/keyMatchers.test.ts | 28 +++++++++---------- packages/cli/src/ui/key/keyToAnsi.ts | 2 +- .../cli/src/ui/key/keybindingUtils.test.ts | 2 +- packages/cli/src/ui/key/keybindingUtils.ts | 2 +- .../tests/generate-keybindings-doc.test.ts | 2 +- 19 files changed, 67 insertions(+), 72 deletions(-) diff --git a/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx b/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx index da8b43dd20..b8de6adb0b 100644 --- a/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx +++ b/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx @@ -103,7 +103,7 @@ describe('ApiAuthDialog', () => { it.each([ { - keyName: 'return', + keyName: 'enter', sequence: '\r', expectedCall: onSubmit, args: ['submitted-key'], diff --git a/packages/cli/src/ui/components/ConfigExtensionDialog.tsx b/packages/cli/src/ui/components/ConfigExtensionDialog.tsx index b6fb8ce1b6..7f09d46491 100644 --- a/packages/cli/src/ui/components/ConfigExtensionDialog.tsx +++ b/packages/cli/src/ui/components/ConfigExtensionDialog.tsx @@ -210,7 +210,7 @@ export const ConfigExtensionDialog: React.FC = ({ useKeypress( (key: Key) => { if (state.type === 'ASK_CONFIRMATION') { - if (key.name === 'y' || key.name === 'return') { + if (key.name === 'y' || key.name === 'enter') { state.resolve(true); return true; } @@ -220,7 +220,7 @@ export const ConfigExtensionDialog: React.FC = ({ } } if (state.type === 'DONE' || state.type === 'ERROR') { - if (key.name === 'return' || key.name === 'escape') { + if (key.name === 'enter' || key.name === 'escape') { onClose(); return true; } diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 785641a556..1cfa2d4215 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -972,7 +972,7 @@ export const InputPrompt: React.FC = ({ if (targetIndex < completion.suggestions.length) { const suggestion = completion.suggestions[targetIndex]; - const isEnterKey = key.name === 'return' && !key.ctrl; + const isEnterKey = key.name === 'enter' && !key.ctrl; if (isEnterKey && shellModeActive) { if (hasUserNavigatedSuggestions.current) { diff --git a/packages/cli/src/ui/components/SessionBrowser.test.tsx b/packages/cli/src/ui/components/SessionBrowser.test.tsx index 2e68cb6898..e97ae310bd 100644 --- a/packages/cli/src/ui/components/SessionBrowser.test.tsx +++ b/packages/cli/src/ui/components/SessionBrowser.test.tsx @@ -324,7 +324,7 @@ describe('SessionBrowser component', () => { await waitUntilReady(); // Press Enter. - triggerKey({ name: 'return', sequence: '\r' }); + triggerKey({ name: 'enter', sequence: '\r' }); await waitUntilReady(); expect(onResumeSession).toHaveBeenCalledTimes(1); @@ -367,7 +367,7 @@ describe('SessionBrowser component', () => { await waitUntilReady(); // Active selection is at 0 (current session). - triggerKey({ name: 'return', sequence: '\r' }); + triggerKey({ name: 'enter', sequence: '\r' }); await waitUntilReady(); expect(onResumeSession).not.toHaveBeenCalled(); diff --git a/packages/cli/src/ui/components/SessionBrowser.tsx b/packages/cli/src/ui/components/SessionBrowser.tsx index 154ad62522..72eb5ef55c 100644 --- a/packages/cli/src/ui/components/SessionBrowser.tsx +++ b/packages/cli/src/ui/components/SessionBrowser.tsx @@ -873,7 +873,7 @@ export const useSessionBrowserInput = ( // Handling regardless of search mode. if ( - key.name === 'return' && + key.name === 'enter' && state.filteredAndSortedSessions[state.activeIndex] ) { const selectedSession = diff --git a/packages/cli/src/ui/components/shared/TextInput.test.tsx b/packages/cli/src/ui/components/shared/TextInput.test.tsx index 7e802bbbe3..a5bc79247c 100644 --- a/packages/cli/src/ui/components/shared/TextInput.test.tsx +++ b/packages/cli/src/ui/components/shared/TextInput.test.tsx @@ -287,7 +287,7 @@ describe('TextInput', () => { await act(async () => { keypressHandler({ - name: 'return', + name: 'enter', shift: false, alt: false, ctrl: false, @@ -314,7 +314,7 @@ describe('TextInput', () => { await act(async () => { keypressHandler({ - name: 'return', + name: 'enter', shift: false, alt: false, ctrl: false, @@ -339,7 +339,7 @@ describe('TextInput', () => { await act(async () => { keypressHandler({ - name: 'return', + name: 'enter', shift: false, alt: false, ctrl: false, 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 51fa728c91..7ea88529ad 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.test.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts @@ -1533,7 +1533,7 @@ describe('useTextBuffer', () => { const { result } = renderHook(() => useTextBuffer({ viewport })); act(() => { result.current.handleInput({ - name: 'return', + name: 'enter', shift: false, alt: false, ctrl: false, @@ -1789,7 +1789,7 @@ describe('useTextBuffer', () => { const { result } = renderHook(() => useTextBuffer({ viewport })); act(() => { result.current.handleInput({ - name: 'return', + name: 'enter', shift: true, alt: false, ctrl: false, @@ -2290,7 +2290,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots ); act(() => { result.current.handleInput({ - name: 'return', + name: 'enter', shift: false, alt: false, ctrl: false, diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx index bc8e198168..1024488cfb 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx @@ -100,7 +100,7 @@ describe('KeypressContext', () => { expect(keyHandler).toHaveBeenCalledWith( expect.objectContaining({ - name: 'return', + name: 'enter', shift: false, ctrl: false, cmd: false, @@ -115,7 +115,7 @@ describe('KeypressContext', () => { expect(keyHandler).toHaveBeenCalledWith( expect.objectContaining({ - name: 'return', + name: 'enter', shift: true, ctrl: false, cmd: false, @@ -148,7 +148,7 @@ describe('KeypressContext', () => { expect(keyHandler).toHaveBeenCalledWith( expect.objectContaining({ - name: 'return', + name: 'enter', ...expected, }), ); @@ -177,7 +177,7 @@ describe('KeypressContext', () => { expect(keyHandler).toHaveBeenCalledWith( expect.objectContaining({ - name: 'return', + name: 'enter', shift: false, alt: true, ctrl: false, @@ -216,7 +216,7 @@ describe('KeypressContext', () => { expect(keyHandler).toHaveBeenLastCalledWith( expect.objectContaining({ - name: 'return', + name: 'enter', sequence: '\r', insertable: true, shift: true, @@ -238,7 +238,7 @@ describe('KeypressContext', () => { expect(keyHandler).toHaveBeenLastCalledWith( expect.objectContaining({ - name: 'return', + name: 'enter', shift: false, alt: false, ctrl: false, @@ -638,8 +638,8 @@ describe('KeypressContext', () => { describe('Parameterized functional keys', () => { it.each([ // ModifyOtherKeys - { sequence: `\x1b[27;2;13~`, expected: { name: 'return', shift: true } }, - { sequence: `\x1b[27;5;13~`, expected: { name: 'return', ctrl: true } }, + { sequence: `\x1b[27;2;13~`, expected: { name: 'enter', shift: true } }, + { sequence: `\x1b[27;5;13~`, expected: { name: 'enter', ctrl: true } }, { sequence: `\x1b[27;5;9~`, expected: { name: 'tab', ctrl: true } }, { sequence: `\x1b[27;6;9~`, @@ -1124,7 +1124,7 @@ describe('KeypressContext', () => { expect(keyHandler).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - name: 'return', + name: 'enter', }), ); expect(keyHandler).toHaveBeenNthCalledWith( diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index d3f9031ffe..7791872865 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -92,11 +92,11 @@ const KEY_INFO_MAP: Record< '[[5~': { name: 'pageup' }, '[[6~': { name: 'pagedown' }, '[9u': { name: 'tab' }, - '[13u': { name: 'return' }, + '[13u': { name: 'enter' }, '[27u': { name: 'escape' }, '[32u': { name: 'space' }, '[127u': { name: 'backspace' }, - '[57414u': { name: 'return' }, // Numpad Enter + '[57414u': { name: 'enter' }, // Numpad Enter '[a': { name: 'up', shift: true }, '[b': { name: 'down', shift: true }, '[c': { name: 'right', shift: true }, @@ -186,10 +186,10 @@ function bufferFastReturn(keypressHandler: KeypressHandler): KeypressHandler { let lastKeyTime = 0; return (key: Key) => { const now = Date.now(); - if (key.name === 'return' && now - lastKeyTime <= FAST_RETURN_TIMEOUT) { + if (key.name === 'enter' && now - lastKeyTime <= FAST_RETURN_TIMEOUT) { keypressHandler({ ...key, - name: 'return', + name: 'enter', shift: true, // to make it a newline, not a submission alt: false, ctrl: false, @@ -232,7 +232,7 @@ function bufferBackslashEnter( if (nextKey === null) { keypressHandler(key); - } else if (nextKey.name === 'return') { + } else if (nextKey.name === 'enter') { keypressHandler({ ...nextKey, shift: true, @@ -582,11 +582,11 @@ function* emitKeys( } } else if (ch === '\r') { // carriage return - name = 'return'; + name = 'enter'; alt = escaped; } else if (escaped && ch === '\n') { // Alt+Enter (linefeed), should be consistent with carriage return - name = 'return'; + name = 'enter'; alt = escaped; } else if (ch === '\t') { // tab diff --git a/packages/cli/src/ui/hooks/useKeypress.test.tsx b/packages/cli/src/ui/hooks/useKeypress.test.tsx index cde15186d9..0ebfb76f8b 100644 --- a/packages/cli/src/ui/hooks/useKeypress.test.tsx +++ b/packages/cli/src/ui/hooks/useKeypress.test.tsx @@ -111,7 +111,7 @@ describe(`useKeypress`, () => { it('should correctly identify alt+enter (meta key)', () => { renderKeypressHook(true); - const key = { name: 'return', sequence: '\x1B\r' }; + const key = { name: 'enter', sequence: '\x1B\r' }; act(() => stdin.write(key.sequence)); expect(onKeypress).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/cli/src/ui/hooks/useSelectionList.test.tsx b/packages/cli/src/ui/hooks/useSelectionList.test.tsx index 4151375280..6a1b82f77a 100644 --- a/packages/cli/src/ui/hooks/useSelectionList.test.tsx +++ b/packages/cli/src/ui/hooks/useSelectionList.test.tsx @@ -356,7 +356,7 @@ describe('useSelectionList', () => { initialIndex: 2, onSelect: mockOnSelect, }); - pressKey('return'); + pressKey('enter'); await waitUntilReady(); expect(mockOnSelect).toHaveBeenCalledTimes(1); expect(mockOnSelect).toHaveBeenCalledWith('C'); @@ -371,7 +371,7 @@ describe('useSelectionList', () => { act(() => result.current.setActiveIndex(1)); await waitUntilReady(); - pressKey('return'); + pressKey('enter'); await waitUntilReady(); expect(mockOnSelect).not.toHaveBeenCalled(); }); @@ -415,7 +415,7 @@ describe('useSelectionList', () => { await waitUntilReady(); // 3. Press Enter. Should select D. act(() => { - press('return'); + press('enter'); }); await waitUntilReady(); @@ -459,7 +459,7 @@ describe('useSelectionList', () => { // All presses happen in same render cycle - React batches the state updates press('down'); // Should move 0 (A) -> 2 (C) press('down'); // Should move 2 (C) -> 3 (D) - press('return'); // Should select D + press('enter'); // Should select D }); await waitUntilReady(); @@ -759,7 +759,7 @@ describe('useSelectionList', () => { pressNumber('1'); await waitUntilReady(); - pressKey('return'); + pressKey('enter'); await waitUntilReady(); expect(mockOnSelect).toHaveBeenCalledTimes(1); diff --git a/packages/cli/src/ui/hooks/vim.ts b/packages/cli/src/ui/hooks/vim.ts index 54de27496f..aa1388be9d 100644 --- a/packages/cli/src/ui/hooks/vim.ts +++ b/packages/cli/src/ui/hooks/vim.ts @@ -396,7 +396,7 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) { // In INSERT mode, let InputPrompt handle completion keys and special commands if ( normalizedKey.name === 'tab' || - (normalizedKey.name === 'return' && !normalizedKey.ctrl) || + (normalizedKey.name === 'enter' && !normalizedKey.ctrl) || normalizedKey.name === 'up' || normalizedKey.name === 'down' || (normalizedKey.ctrl && normalizedKey.name === 'r') @@ -424,7 +424,7 @@ 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.name === 'enter' && !normalizedKey.alt && !normalizedKey.ctrl && !normalizedKey.cmd diff --git a/packages/cli/src/ui/key/keyBindings.test.ts b/packages/cli/src/ui/key/keyBindings.test.ts index b47e8d56b8..3057bf85b6 100644 --- a/packages/cli/src/ui/key/keyBindings.test.ts +++ b/packages/cli/src/ui/key/keyBindings.test.ts @@ -80,8 +80,8 @@ describe('KeyBinding', () => { }); it('should handle named keys with modifiers', () => { - const binding = new KeyBinding('ctrl+return'); - expect(binding.key).toBe('return'); + const binding = new KeyBinding('ctrl+enter'); + expect(binding.key).toBe('enter'); expect(binding.ctrl).toBe(true); }); diff --git a/packages/cli/src/ui/key/keyBindings.ts b/packages/cli/src/ui/key/keyBindings.ts index 209111b53c..b375d991c8 100644 --- a/packages/cli/src/ui/key/keyBindings.ts +++ b/packages/cli/src/ui/key/keyBindings.ts @@ -149,11 +149,9 @@ export class KeyBinding { 'numpad_subtract', 'numpad_decimal', 'numpad_divide', - // Gemini CLI legacy/internal support - 'return', ]); - /** The key name (e.g., 'a', 'return', 'tab', 'escape') */ + /** The key name (e.g., 'a', 'enter', 'tab', 'escape') */ readonly key: string; readonly shift: boolean; readonly alt: boolean; @@ -238,7 +236,7 @@ export type KeyBindingConfig = { */ export const defaultKeyBindings: KeyBindingConfig = { // Basic Controls - [Command.RETURN]: [new KeyBinding('return')], + [Command.RETURN]: [new KeyBinding('enter')], [Command.ESCAPE]: [new KeyBinding('escape'), new KeyBinding('ctrl+[')], [Command.QUIT]: [new KeyBinding('ctrl+c')], [Command.EXIT]: [new KeyBinding('ctrl+d')], @@ -308,7 +306,7 @@ export const defaultKeyBindings: KeyBindingConfig = { [Command.HISTORY_UP]: [new KeyBinding('ctrl+p')], [Command.HISTORY_DOWN]: [new KeyBinding('ctrl+n')], [Command.REVERSE_SEARCH]: [new KeyBinding('ctrl+r')], - [Command.SUBMIT_REVERSE_SEARCH]: [new KeyBinding('return')], + [Command.SUBMIT_REVERSE_SEARCH]: [new KeyBinding('enter')], [Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [new KeyBinding('tab')], // Navigation @@ -325,10 +323,7 @@ export const defaultKeyBindings: KeyBindingConfig = { [Command.DIALOG_PREV]: [new KeyBinding('shift+tab')], // Suggestions & Completions - [Command.ACCEPT_SUGGESTION]: [ - new KeyBinding('tab'), - new KeyBinding('return'), - ], + [Command.ACCEPT_SUGGESTION]: [new KeyBinding('tab'), new KeyBinding('enter')], [Command.COMPLETION_UP]: [new KeyBinding('up'), new KeyBinding('ctrl+p')], [Command.COMPLETION_DOWN]: [new KeyBinding('down'), new KeyBinding('ctrl+n')], [Command.EXPAND_SUGGESTION]: [new KeyBinding('right')], @@ -336,12 +331,12 @@ export const defaultKeyBindings: KeyBindingConfig = { // Text Input // Must also exclude shift to allow shift+enter for newline - [Command.SUBMIT]: [new KeyBinding('return')], + [Command.SUBMIT]: [new KeyBinding('enter')], [Command.NEWLINE]: [ - new KeyBinding('ctrl+return'), - new KeyBinding('cmd+return'), - new KeyBinding('alt+return'), - new KeyBinding('shift+return'), + new KeyBinding('ctrl+enter'), + new KeyBinding('cmd+enter'), + new KeyBinding('alt+enter'), + new KeyBinding('shift+enter'), new KeyBinding('ctrl+j'), ], [Command.OPEN_EXTERNAL_EDITOR]: [new KeyBinding('ctrl+x')], @@ -366,7 +361,7 @@ export const defaultKeyBindings: KeyBindingConfig = { [Command.UNFOCUS_BACKGROUND_SHELL_LIST]: [new KeyBinding('tab')], [Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]: [new KeyBinding('tab')], [Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]: [new KeyBinding('tab')], - [Command.BACKGROUND_SHELL_SELECT]: [new KeyBinding('return')], + [Command.BACKGROUND_SHELL_SELECT]: [new KeyBinding('enter')], [Command.BACKGROUND_SHELL_ESCAPE]: [new KeyBinding('escape')], [Command.SHOW_MORE_LINES]: [new KeyBinding('ctrl+o')], [Command.EXPAND_PASTE]: [new KeyBinding('ctrl+o')], diff --git a/packages/cli/src/ui/key/keyMatchers.test.ts b/packages/cli/src/ui/key/keyMatchers.test.ts index 62766d1a0d..12e2f07bc2 100644 --- a/packages/cli/src/ui/key/keyMatchers.test.ts +++ b/packages/cli/src/ui/key/keyMatchers.test.ts @@ -31,7 +31,7 @@ describe('keyMatchers', () => { // Basic bindings { command: Command.RETURN, - positive: [createKey('return')], + positive: [createKey('enter')], negative: [createKey('r')], }, { @@ -270,8 +270,8 @@ describe('keyMatchers', () => { // Auto-completion { command: Command.ACCEPT_SUGGESTION, - positive: [createKey('tab'), createKey('return')], - negative: [createKey('return', { ctrl: true }), createKey('space')], + positive: [createKey('tab'), createKey('enter')], + negative: [createKey('enter', { ctrl: true }), createKey('space')], }, { command: Command.COMPLETION_UP, @@ -287,21 +287,21 @@ describe('keyMatchers', () => { // Text input { command: Command.SUBMIT, - positive: [createKey('return')], + positive: [createKey('enter')], negative: [ - createKey('return', { ctrl: true }), - createKey('return', { cmd: true }), - createKey('return', { alt: true }), + createKey('enter', { ctrl: true }), + createKey('enter', { cmd: true }), + createKey('enter', { alt: true }), ], }, { command: Command.NEWLINE, positive: [ - createKey('return', { ctrl: true }), - createKey('return', { cmd: true }), - createKey('return', { alt: true }), + createKey('enter', { ctrl: true }), + createKey('enter', { cmd: true }), + createKey('enter', { alt: true }), ], - negative: [createKey('return'), createKey('n')], + negative: [createKey('enter'), createKey('n')], }, // External tools @@ -382,14 +382,14 @@ describe('keyMatchers', () => { }, { command: Command.SUBMIT_REVERSE_SEARCH, - positive: [createKey('return')], - negative: [createKey('return', { ctrl: true }), createKey('tab')], + positive: [createKey('enter')], + negative: [createKey('enter', { ctrl: true }), createKey('tab')], }, { command: Command.ACCEPT_SUGGESTION_REVERSE_SEARCH, positive: [createKey('tab')], negative: [ - createKey('return'), + createKey('enter'), createKey('space'), createKey('tab', { ctrl: true }), ], diff --git a/packages/cli/src/ui/key/keyToAnsi.ts b/packages/cli/src/ui/key/keyToAnsi.ts index adb9874933..6d61c2e114 100644 --- a/packages/cli/src/ui/key/keyToAnsi.ts +++ b/packages/cli/src/ui/key/keyToAnsi.ts @@ -21,7 +21,7 @@ const SPECIAL_KEYS: Record = { end: '\x1b[F', pageup: '\x1b[5~', pagedown: '\x1b[6~', - return: '\r', + enter: '\r', }; /** diff --git a/packages/cli/src/ui/key/keybindingUtils.test.ts b/packages/cli/src/ui/key/keybindingUtils.test.ts index 58a113f4de..633ebbedb2 100644 --- a/packages/cli/src/ui/key/keybindingUtils.test.ts +++ b/packages/cli/src/ui/key/keybindingUtils.test.ts @@ -27,7 +27,7 @@ describe('keybindingUtils', () => { }, { name: 'named key (return)', - binding: new KeyBinding('return'), + binding: new KeyBinding('enter'), expected: { darwin: 'Enter', win32: 'Enter', diff --git a/packages/cli/src/ui/key/keybindingUtils.ts b/packages/cli/src/ui/key/keybindingUtils.ts index c4f4c6b942..f0ec6e37bd 100644 --- a/packages/cli/src/ui/key/keybindingUtils.ts +++ b/packages/cli/src/ui/key/keybindingUtils.ts @@ -16,7 +16,7 @@ import { * Maps internal key names to user-friendly display names. */ const KEY_NAME_MAP: Record = { - return: 'Enter', + enter: 'Enter', escape: 'Esc', backspace: 'Backspace', delete: 'Delete', diff --git a/scripts/tests/generate-keybindings-doc.test.ts b/scripts/tests/generate-keybindings-doc.test.ts index c669fed02e..19ba2e0f98 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', shift: false, ctrl: false }], + bindings: [{ key: 'enter', shift: false, ctrl: false }], }, ], }, From c25ff946082d23c505545514c3f535e0bb6aac0c Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Tue, 10 Mar 2026 00:17:46 -0400 Subject: [PATCH 2/4] build(release): restrict npm bundling to non-stable tags (#21821) --- .github/actions/publish-release/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/publish-release/action.yml b/.github/actions/publish-release/action.yml index 70a413f13a..54c404c7c1 100644 --- a/.github/actions/publish-release/action.yml +++ b/.github/actions/publish-release/action.yml @@ -193,7 +193,7 @@ runs: INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}' - name: '๐Ÿ“ฆ Prepare bundled CLI for npm release' - if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'" + if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/' && inputs.npm-tag != 'latest'" working-directory: '${{ inputs.working-directory }}' shell: 'bash' run: | From 02d4451e77daa9424a59e198efd00613d5163e61 Mon Sep 17 00:00:00 2001 From: Gaurav <39389231+gsquared94@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:41:54 -0700 Subject: [PATCH 3/4] fix(core): override toolRegistry property for sub-agent schedulers (#21766) --- .../core/src/agents/agent-scheduler.test.ts | 51 ++++++++++++++++--- packages/core/src/agents/agent-scheduler.ts | 5 ++ .../browser/browserAgentFactory.test.ts | 39 ++++++++++++++ 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/packages/core/src/agents/agent-scheduler.test.ts b/packages/core/src/agents/agent-scheduler.test.ts index 451fb276a2..02d780128c 100644 --- a/packages/core/src/agents/agent-scheduler.test.ts +++ b/packages/core/src/agents/agent-scheduler.test.ts @@ -19,23 +19,24 @@ vi.mock('../scheduler/scheduler.js', () => ({ })); describe('agent-scheduler', () => { - let mockConfig: Mocked; let mockToolRegistry: Mocked; let mockMessageBus: Mocked; beforeEach(() => { + vi.mocked(Scheduler).mockClear(); mockMessageBus = {} as Mocked; mockToolRegistry = { getTool: vi.fn(), getMessageBus: vi.fn().mockReturnValue(mockMessageBus), } as unknown as Mocked; - mockConfig = { - getMessageBus: vi.fn().mockReturnValue(mockMessageBus), - toolRegistry: mockToolRegistry, - } as unknown as Mocked; }); it('should create a scheduler with agent-specific config', async () => { + const mockConfig = { + getMessageBus: vi.fn().mockReturnValue(mockMessageBus), + toolRegistry: mockToolRegistry, + } as unknown as Mocked; + const requests: ToolCallRequestInfo[] = [ { callId: 'call-1', @@ -68,8 +69,46 @@ describe('agent-scheduler', () => { }), ); - // Verify that the scheduler's config has the overridden tool registry const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].config; expect(schedulerConfig.toolRegistry).toBe(mockToolRegistry); }); + + it('should override toolRegistry getter from prototype chain', async () => { + const mainRegistry = { _id: 'main' } as unknown as Mocked; + const agentRegistry = { + _id: 'agent', + getMessageBus: vi.fn().mockReturnValue(mockMessageBus), + } as unknown as Mocked; + + const config = { + getMessageBus: vi.fn().mockReturnValue(mockMessageBus), + } as unknown as Mocked; + Object.defineProperty(config, 'toolRegistry', { + get: () => mainRegistry, + configurable: true, + }); + + await scheduleAgentTools( + config as unknown as Config, + [ + { + callId: 'c1', + name: 'new_page', + args: {}, + isClientInitiated: false, + prompt_id: 'p1', + }, + ], + { + schedulerId: 'browser-1', + toolRegistry: agentRegistry as unknown as ToolRegistry, + signal: new AbortController().signal, + }, + ); + + const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].config; + expect(schedulerConfig.toolRegistry).toBe(agentRegistry); + expect(schedulerConfig.toolRegistry).not.toBe(mainRegistry); + expect(schedulerConfig.getToolRegistry()).toBe(agentRegistry); + }); }); diff --git a/packages/core/src/agents/agent-scheduler.ts b/packages/core/src/agents/agent-scheduler.ts index 983f814b0f..3b78ec47ee 100644 --- a/packages/core/src/agents/agent-scheduler.ts +++ b/packages/core/src/agents/agent-scheduler.ts @@ -58,6 +58,11 @@ export async function scheduleAgentTools( const agentConfig: Config = Object.create(config); agentConfig.getToolRegistry = () => toolRegistry; agentConfig.getMessageBus = () => toolRegistry.getMessageBus(); + // Override toolRegistry property so AgentLoopContext reads the agent-specific registry. + Object.defineProperty(agentConfig, 'toolRegistry', { + get: () => toolRegistry, + configurable: true, + }); const scheduler = new Scheduler({ config: agentConfig, diff --git a/packages/core/src/agents/browser/browserAgentFactory.test.ts b/packages/core/src/agents/browser/browserAgentFactory.test.ts index a317f3a9ed..96fba50d4f 100644 --- a/packages/core/src/agents/browser/browserAgentFactory.test.ts +++ b/packages/core/src/agents/browser/browserAgentFactory.test.ts @@ -209,6 +209,45 @@ describe('browserAgentFactory', () => { .map((t) => t.name) ?? []; expect(toolNames).toContain('analyze_screenshot'); }); + + it('should include all MCP navigation tools (new_page, navigate_page) in definition', async () => { + mockBrowserManager.getDiscoveredTools.mockResolvedValue([ + { name: 'take_snapshot', description: 'Take snapshot' }, + { name: 'click', description: 'Click element' }, + { name: 'fill', description: 'Fill form field' }, + { name: 'navigate_page', description: 'Navigate to URL' }, + { name: 'new_page', description: 'Open a new page/tab' }, + { name: 'close_page', description: 'Close page' }, + { name: 'select_page', description: 'Select page' }, + { name: 'press_key', description: 'Press key' }, + { name: 'hover', description: 'Hover element' }, + ]); + + const { definition } = await createBrowserAgentDefinition( + mockConfig, + mockMessageBus, + ); + + const toolNames = + definition.toolConfig?.tools + ?.filter( + (t): t is { name: string } => typeof t === 'object' && 'name' in t, + ) + .map((t) => t.name) ?? []; + + // All MCP tools must be present + expect(toolNames).toContain('new_page'); + expect(toolNames).toContain('navigate_page'); + expect(toolNames).toContain('close_page'); + expect(toolNames).toContain('select_page'); + expect(toolNames).toContain('click'); + expect(toolNames).toContain('take_snapshot'); + expect(toolNames).toContain('press_key'); + // Custom composite tool must also be present + expect(toolNames).toContain('type_text'); + // Total: 9 MCP + 1 type_text (no analyze_screenshot without visualModel) + expect(definition.toolConfig?.tools).toHaveLength(10); + }); }); describe('cleanupBrowserAgent', () => { From 6ae6c810ba155e8eb049595e77e9a63e92d3d6eb Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Tue, 10 Mar 2026 01:07:26 -0700 Subject: [PATCH 4/4] fix(cli): make footer items equally spaced (#21843) --- packages/cli/src/ui/components/Footer.tsx | 30 ++-- .../ui/components/FooterConfigDialog.test.tsx | 57 +++++++ .../src/ui/components/FooterConfigDialog.tsx | 2 +- .../__snapshots__/Footer.test.tsx.snap | 22 +-- ...ts-the-active-item-in-the-preview.snap.svg | 31 ++-- ...s-correctly-with-default-settings.snap.svg | 19 ++- ...Show-footer-labels-is-toggled-off.snap.svg | 143 ++++++++++++++++++ .../FooterConfigDialog.test.tsx.snap | 54 ++++++- 8 files changed, 303 insertions(+), 55 deletions(-) create mode 100644 packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-updates-the-preview-when-Show-footer-labels-is-toggled-off.snap.svg diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index e5a1f9e8b6..c6816339f5 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -106,6 +106,7 @@ export interface FooterRowItem { flexGrow?: number; flexShrink?: number; isFocused?: boolean; + alignItems?: 'flex-start' | 'center' | 'flex-end'; } const COLUMN_GAP = 3; @@ -117,10 +118,17 @@ export const FooterRow: React.FC<{ const elements: React.ReactNode[] = []; items.forEach((item, idx) => { - if (idx > 0 && !showLabels) { + if (idx > 0) { elements.push( - - ยท + + {!showLabels && ยท } , ); } @@ -131,6 +139,7 @@ export const FooterRow: React.FC<{ flexDirection="column" flexGrow={item.flexGrow ?? 0} flexShrink={item.flexShrink ?? 1} + alignItems={item.alignItems} backgroundColor={item.isFocused ? theme.background.focus : undefined} > {showLabels && ( @@ -148,12 +157,7 @@ export const FooterRow: React.FC<{ }); return ( - + {elements} ); @@ -441,8 +445,9 @@ export const Footer: React.FC = () => { } } - const rowItems: FooterRowItem[] = columnsToRender.map((col) => { + const rowItems: FooterRowItem[] = columnsToRender.map((col, index) => { const isWorkspace = col.id === 'workspace'; + const isLast = index === columnsToRender.length - 1; // Calculate exact space available for growth to prevent over-estimation truncation const otherItemsWidth = columnsToRender @@ -464,8 +469,10 @@ export const Footer: React.FC = () => { key: col.id, header: col.header, element: col.element(estimatedWidth), - flexGrow: isWorkspace ? 1 : 0, + flexGrow: 0, flexShrink: isWorkspace ? 1 : 0, + alignItems: + isLast && !droppedAny && index > 0 ? 'flex-end' : 'flex-start', }; }); @@ -476,6 +483,7 @@ export const Footer: React.FC = () => { element: โ€ฆ, flexGrow: 0, flexShrink: 0, + alignItems: 'flex-end', }); } diff --git a/packages/cli/src/ui/components/FooterConfigDialog.test.tsx b/packages/cli/src/ui/components/FooterConfigDialog.test.tsx index 3141c3a1d7..9d3688e17a 100644 --- a/packages/cli/src/ui/components/FooterConfigDialog.test.tsx +++ b/packages/cli/src/ui/components/FooterConfigDialog.test.tsx @@ -9,6 +9,7 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { FooterConfigDialog } from './FooterConfigDialog.js'; import { createMockSettings } from '../../test-utils/settings.js'; +import { ALL_ITEMS } from '../../config/footerItems.js'; import { act } from 'react'; describe('', () => { @@ -213,4 +214,60 @@ describe('', () => { expect(bIdxAfter).toBeLessThan(wIdxAfter); }); }); + + it('updates the preview when Show footer labels is toggled off', async () => { + const settings = createMockSettings(); + const renderResult = renderWithProviders( + , + { settings }, + ); + + const { lastFrame, stdin, waitUntilReady } = renderResult; + await waitUntilReady(); + + // By default labels are on + expect(lastFrame()).toContain('workspace (/directory)'); + expect(lastFrame()).toContain('sandbox'); + expect(lastFrame()).toContain('/model'); + + // Move to "Show footer labels" (which is the second to last item) + for (let i = 0; i < ALL_ITEMS.length; i++) { + act(() => { + stdin.write('\u001b[B'); // Down arrow + }); + } + + await waitFor(() => { + expect(lastFrame()).toMatch(/> \[โœ“\] Show footer labels/); + }); + + // Toggle it off + act(() => { + stdin.write('\r'); + }); + + await waitFor(() => { + expect(lastFrame()).toMatch(/> \[ \] Show footer labels/); + // The headers should no longer be in the preview + expect(lastFrame()).not.toContain('workspace (/directory)'); + expect(lastFrame()).not.toContain('/model'); + + // We can't strictly search for "sandbox" because the menu item also says "sandbox". + // Let's assert that the spacer dots are now present in the preview instead. + const previewLine = + lastFrame() + .split('\n') + .find((line) => line.includes('Preview:')) || ''; + const nextLine = + lastFrame().split('\n')[ + lastFrame().split('\n').indexOf(previewLine) + 1 + ] || ''; + expect(nextLine).toContain('ยท'); + expect(nextLine).toContain('~/project/path'); + expect(nextLine).toContain('docker'); + expect(nextLine).toContain('97%'); + }); + + await expect(renderResult).toMatchSvgSnapshot(); + }); }); diff --git a/packages/cli/src/ui/components/FooterConfigDialog.tsx b/packages/cli/src/ui/components/FooterConfigDialog.tsx index cda58574a3..562bbabd81 100644 --- a/packages/cli/src/ui/components/FooterConfigDialog.tsx +++ b/packages/cli/src/ui/components/FooterConfigDialog.tsx @@ -266,7 +266,7 @@ export const FooterConfigDialog: React.FC = ({ key: id, header: ALL_ITEMS.find((i) => i.id === id)?.header ?? id, element: mockData[id], - flexGrow: 1, + flexGrow: 0, isFocused: id === focusKey, })); diff --git a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap index 2d98d66f03..3980ddbd0a 100644 --- a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap @@ -1,26 +1,26 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`