From 3b0649d4084577be5d8f064c446dd8775a2d0fe2 Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Fri, 6 Feb 2026 19:23:59 -0500 Subject: [PATCH] feat(cli): update approval modes UI (#18476) --- .../components/ApprovalModeIndicator.test.tsx | 40 ++++++++++++++----- .../ui/components/ApprovalModeIndicator.tsx | 28 +++++++++---- .../cli/src/ui/components/Composer.test.tsx | 25 ++++++++---- packages/cli/src/ui/components/Composer.tsx | 6 +-- .../ui/hooks/useApprovalModeIndicator.test.ts | 22 +++++----- .../src/ui/hooks/useApprovalModeIndicator.ts | 10 ++--- 6 files changed, 86 insertions(+), 45 deletions(-) diff --git a/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx b/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx index a5ddf5ac34..4e751ad788 100644 --- a/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx +++ b/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx @@ -15,8 +15,20 @@ describe('ApprovalModeIndicator', () => { , ); const output = lastFrame(); - expect(output).toContain('accepting edits'); - expect(output).toContain('(shift + tab to cycle)'); + expect(output).toContain('auto-edit'); + expect(output).toContain('shift + tab to enter default mode'); + }); + + it('renders correctly for AUTO_EDIT mode with plan enabled', () => { + const { lastFrame } = render( + , + ); + const output = lastFrame(); + expect(output).toContain('auto-edit'); + expect(output).toContain('shift + tab to enter default mode'); }); it('renders correctly for PLAN mode', () => { @@ -24,8 +36,8 @@ describe('ApprovalModeIndicator', () => { , ); const output = lastFrame(); - expect(output).toContain('plan mode'); - expect(output).toContain('(shift + tab to cycle)'); + expect(output).toContain('plan'); + expect(output).toContain('shift + tab to enter auto-edit mode'); }); it('renders correctly for YOLO mode', () => { @@ -33,16 +45,26 @@ describe('ApprovalModeIndicator', () => { , ); const output = lastFrame(); - expect(output).toContain('YOLO mode'); - expect(output).toContain('(ctrl + y to toggle)'); + expect(output).toContain('YOLO'); + expect(output).toContain('shift + tab to enter auto-edit mode'); }); - it('renders nothing for DEFAULT mode', () => { + it('renders correctly for DEFAULT mode', () => { const { lastFrame } = render( , ); const output = lastFrame(); - expect(output).not.toContain('accepting edits'); - expect(output).not.toContain('YOLO mode'); + expect(output).toContain('shift + tab to enter auto-edit mode'); + }); + + it('renders correctly for DEFAULT mode with plan enabled', () => { + const { lastFrame } = render( + , + ); + const output = lastFrame(); + expect(output).toContain('shift + tab to enter plan mode'); }); }); diff --git a/packages/cli/src/ui/components/ApprovalModeIndicator.tsx b/packages/cli/src/ui/components/ApprovalModeIndicator.tsx index 875cb0d84b..83adcd8417 100644 --- a/packages/cli/src/ui/components/ApprovalModeIndicator.tsx +++ b/packages/cli/src/ui/components/ApprovalModeIndicator.tsx @@ -11,10 +11,12 @@ import { ApprovalMode } from '@google/gemini-cli-core'; interface ApprovalModeIndicatorProps { approvalMode: ApprovalMode; + isPlanEnabled?: boolean; } export const ApprovalModeIndicator: React.FC = ({ approvalMode, + isPlanEnabled, }) => { let textColor = ''; let textContent = ''; @@ -23,29 +25,39 @@ export const ApprovalModeIndicator: React.FC = ({ switch (approvalMode) { case ApprovalMode.AUTO_EDIT: textColor = theme.status.warning; - textContent = 'accepting edits'; - subText = ' (shift + tab to cycle)'; + textContent = 'auto-edit'; + subText = 'shift + tab to enter default mode'; break; case ApprovalMode.PLAN: textColor = theme.status.success; - textContent = 'plan mode'; - subText = ' (shift + tab to cycle)'; + textContent = 'plan'; + subText = 'shift + tab to enter auto-edit mode'; break; case ApprovalMode.YOLO: textColor = theme.status.error; - textContent = 'YOLO mode'; - subText = ' (ctrl + y to toggle)'; + textContent = 'YOLO'; + subText = 'shift + tab to enter auto-edit mode'; break; case ApprovalMode.DEFAULT: default: + textColor = theme.text.accent; + textContent = ''; + subText = isPlanEnabled + ? 'shift + tab to enter plan mode' + : 'shift + tab to enter auto-edit mode'; break; } return ( - {textContent} - {subText && {subText}} + {textContent ? textContent : null} + {subText ? ( + + {textContent ? ' ' : ''} + {subText} + + ) : null} ); diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index d9094c6ae5..0f6f310637 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -164,6 +164,7 @@ const createMockConfig = (overrides = {}) => ({ getDebugMode: vi.fn(() => false), getAccessibility: vi.fn(() => ({})), getMcpServers: vi.fn(() => ({})), + isPlanEnabled: vi.fn(() => false), getToolRegistry: () => ({ getTool: vi.fn(), }), @@ -485,16 +486,24 @@ describe('Composer', () => { expect(lastFrame()).not.toContain('InputPrompt'); }); - it('shows ApprovalModeIndicator when approval mode is not default and shell mode is inactive', () => { - const uiState = createMockUIState({ - showApprovalModeIndicator: ApprovalMode.YOLO, - shellModeActive: false, - }); + it.each([ + [ApprovalMode.DEFAULT], + [ApprovalMode.AUTO_EDIT], + [ApprovalMode.PLAN], + [ApprovalMode.YOLO], + ])( + 'shows ApprovalModeIndicator when approval mode is %s and shell mode is inactive', + (mode) => { + const uiState = createMockUIState({ + showApprovalModeIndicator: mode, + shellModeActive: false, + }); - const { lastFrame } = renderComposer(uiState); + const { lastFrame } = renderComposer(uiState); - expect(lastFrame()).toMatch(/ApprovalModeIndic[\s\S]*ator/); - }); + expect(lastFrame()).toMatch(/ApprovalModeIndic[\s\S]*ator/); + }, + ); it('shows ShellModeIndicator when shell mode is active', () => { const uiState = createMockUIState({ diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 57afdde943..024b34216f 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -27,7 +27,6 @@ import { useVimMode } from '../contexts/VimModeContext.js'; import { useConfig } from '../contexts/ConfigContext.js'; import { useSettings } from '../contexts/SettingsContext.js'; import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js'; -import { ApprovalMode } from '@google/gemini-cli-core'; import { StreamingState, ToolCallStatus } from '../types.js'; import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js'; import { TodoTray } from './messages/Todo.js'; @@ -68,9 +67,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { (!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) && uiState.streamingState === StreamingState.Responding && !hasPendingActionRequired; - const showApprovalIndicator = - showApprovalModeIndicator !== ApprovalMode.DEFAULT && - !uiState.shellModeActive; + const showApprovalIndicator = !uiState.shellModeActive; const showRawMarkdownIndicator = !uiState.renderMarkdown; const showEscToCancelHint = showLoadingIndicator && @@ -169,6 +166,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { {showApprovalIndicator && ( )} {uiState.shellModeActive && ( diff --git a/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts b/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts index 4fec4edf18..0b61023b18 100644 --- a/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts +++ b/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts @@ -236,7 +236,7 @@ describe('useApprovalModeIndicator', () => { expect(result.current).toBe(ApprovalMode.AUTO_EDIT); }); - it('should cycle through DEFAULT -> AUTO_EDIT -> PLAN -> DEFAULT when plan is enabled', () => { + it('should cycle through DEFAULT -> PLAN -> AUTO_EDIT -> DEFAULT when plan is enabled', () => { mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); mockConfigInstance.isPlanEnabled.mockReturnValue(true); renderHook(() => @@ -246,15 +246,7 @@ describe('useApprovalModeIndicator', () => { }), ); - // DEFAULT -> AUTO_EDIT - act(() => { - capturedUseKeypressHandler({ name: 'tab', shift: true } as Key); - }); - expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.AUTO_EDIT, - ); - - // AUTO_EDIT -> PLAN + // DEFAULT -> PLAN act(() => { capturedUseKeypressHandler({ name: 'tab', shift: true } as Key); }); @@ -262,7 +254,15 @@ describe('useApprovalModeIndicator', () => { ApprovalMode.PLAN, ); - // PLAN -> DEFAULT + // PLAN -> AUTO_EDIT + act(() => { + capturedUseKeypressHandler({ name: 'tab', shift: true } as Key); + }); + expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.AUTO_EDIT, + ); + + // AUTO_EDIT -> DEFAULT act(() => { capturedUseKeypressHandler({ name: 'tab', shift: true } as Key); }); diff --git a/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts b/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts index 3208b41603..c9c1d768c8 100644 --- a/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts +++ b/packages/cli/src/ui/hooks/useApprovalModeIndicator.ts @@ -72,14 +72,14 @@ export function useApprovalModeIndicator({ const currentMode = config.getApprovalMode(); switch (currentMode) { case ApprovalMode.DEFAULT: + nextApprovalMode = config.isPlanEnabled() + ? ApprovalMode.PLAN + : ApprovalMode.AUTO_EDIT; + break; + case ApprovalMode.PLAN: nextApprovalMode = ApprovalMode.AUTO_EDIT; break; case ApprovalMode.AUTO_EDIT: - nextApprovalMode = config.isPlanEnabled() - ? ApprovalMode.PLAN - : ApprovalMode.DEFAULT; - break; - case ApprovalMode.PLAN: nextApprovalMode = ApprovalMode.DEFAULT; break; case ApprovalMode.YOLO: