feat(cli): update approval modes UI (#18476)

This commit is contained in:
Jerop Kipruto
2026-02-06 19:23:59 -05:00
committed by GitHub
parent 7409ce5df6
commit 3b0649d408
6 changed files with 86 additions and 45 deletions

View File

@@ -15,8 +15,20 @@ describe('ApprovalModeIndicator', () => {
<ApprovalModeIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
);
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(
<ApprovalModeIndicator
approvalMode={ApprovalMode.AUTO_EDIT}
isPlanEnabled={true}
/>,
);
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', () => {
<ApprovalModeIndicator approvalMode={ApprovalMode.PLAN} />,
);
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', () => {
<ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
);
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(
<ApprovalModeIndicator approvalMode={ApprovalMode.DEFAULT} />,
);
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(
<ApprovalModeIndicator
approvalMode={ApprovalMode.DEFAULT}
isPlanEnabled={true}
/>,
);
const output = lastFrame();
expect(output).toContain('shift + tab to enter plan mode');
});
});

View File

@@ -11,10 +11,12 @@ import { ApprovalMode } from '@google/gemini-cli-core';
interface ApprovalModeIndicatorProps {
approvalMode: ApprovalMode;
isPlanEnabled?: boolean;
}
export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
approvalMode,
isPlanEnabled,
}) => {
let textColor = '';
let textContent = '';
@@ -23,29 +25,39 @@ export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
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 (
<Box>
<Text color={textColor}>
{textContent}
{subText && <Text color={theme.text.secondary}>{subText}</Text>}
{textContent ? textContent : null}
{subText ? (
<Text color={theme.text.secondary}>
{textContent ? ' ' : ''}
{subText}
</Text>
) : null}
</Text>
</Box>
);

View File

@@ -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({

View File

@@ -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 && (
<ApprovalModeIndicator
approvalMode={showApprovalModeIndicator}
isPlanEnabled={config.isPlanEnabled()}
/>
)}
{uiState.shellModeActive && (

View File

@@ -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);
});

View File

@@ -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: