feat(plan): Extend Shift+Tab Mode Cycling to include Plan Mode (#17177)

This commit is contained in:
Adib234
2026-01-21 10:19:47 -05:00
committed by GitHub
parent b703c87a64
commit 0605e6e3e9
13 changed files with 163 additions and 100 deletions

View File

@@ -93,20 +93,20 @@ available combinations.
#### App Controls
| Action | Keys |
| ------------------------------------------------------------------------------------------------ | ---------------- |
| Toggle detailed error information. | `F12` |
| Toggle the full TODO list. | `Ctrl + T` |
| Show IDE context details. | `Ctrl + G` |
| Toggle Markdown rendering. | `Cmd + M` |
| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` |
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Toggle Auto Edit (auto-accept edits) mode. | `Shift + Tab` |
| Expand a height-constrained response to show additional lines when not in alternate buffer mode. | `Ctrl + S` |
| Focus the shell input from the gemini input. | `Tab (no Shift)` |
| Focus the Gemini input from the shell input. | `Tab` |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
| Restart the application. | `R` |
| Action | Keys |
| ----------------------------------------------------------------------------------------------------- | ---------------- |
| Toggle detailed error information. | `F12` |
| Toggle the full TODO list. | `Ctrl + T` |
| Show IDE context details. | `Ctrl + G` |
| Toggle Markdown rendering. | `Cmd + 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` |
| Expand a height-constrained response to show additional lines when not in alternate buffer mode. | `Ctrl + S` |
| Focus the shell input from the gemini input. | `Tab (no Shift)` |
| Focus the Gemini input from the shell input. | `Tab` |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
| Restart the application. | `R` |
<!-- KEYBINDINGS-AUTOGEN:END -->

View File

@@ -76,7 +76,7 @@ export enum Command {
TOGGLE_MARKDOWN = 'app.toggleMarkdown',
TOGGLE_COPY_MODE = 'app.toggleCopyMode',
TOGGLE_YOLO = 'app.toggleYolo',
TOGGLE_AUTO_EDIT = 'app.toggleAutoEdit',
CYCLE_APPROVAL_MODE = 'app.cycleApprovalMode',
SHOW_MORE_LINES = 'app.showMoreLines',
FOCUS_SHELL_INPUT = 'app.focusShellInput',
UNFOCUS_SHELL_INPUT = 'app.unfocusShellInput',
@@ -246,7 +246,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.TOGGLE_MARKDOWN]: [{ key: 'm', command: true }],
[Command.TOGGLE_COPY_MODE]: [{ key: 's', ctrl: true }],
[Command.TOGGLE_YOLO]: [{ key: 'y', ctrl: true }],
[Command.TOGGLE_AUTO_EDIT]: [{ key: 'tab', shift: true }],
[Command.CYCLE_APPROVAL_MODE]: [{ key: 'tab', shift: true }],
[Command.SHOW_MORE_LINES]: [{ key: 's', ctrl: true }],
[Command.FOCUS_SHELL_INPUT]: [{ key: 'tab', shift: false }],
[Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab' }],
@@ -352,7 +352,7 @@ export const commandCategories: readonly CommandCategory[] = [
Command.TOGGLE_MARKDOWN,
Command.TOGGLE_COPY_MODE,
Command.TOGGLE_YOLO,
Command.TOGGLE_AUTO_EDIT,
Command.CYCLE_APPROVAL_MODE,
Command.SHOW_MORE_LINES,
Command.FOCUS_SHELL_INPUT,
Command.UNFOCUS_SHELL_INPUT,
@@ -437,7 +437,8 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
[Command.TOGGLE_MARKDOWN]: 'Toggle Markdown rendering.',
[Command.TOGGLE_COPY_MODE]: 'Toggle copy mode when in alternate buffer mode.',
[Command.TOGGLE_YOLO]: 'Toggle YOLO (auto-approval) mode for tool calls.',
[Command.TOGGLE_AUTO_EDIT]: 'Toggle Auto Edit (auto-accept edits) mode.',
[Command.CYCLE_APPROVAL_MODE]:
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only).',
[Command.SHOW_MORE_LINES]:
'Expand a height-constrained response to show additional lines when not in alternate buffer mode.',
[Command.FOCUS_SHELL_INPUT]: 'Focus the shell input from the gemini input.',

View File

@@ -136,7 +136,7 @@ vi.mock('./hooks/useLoadingIndicator.js');
vi.mock('./hooks/useFolderTrust.js');
vi.mock('./hooks/useIdeTrustListener.js');
vi.mock('./hooks/useMessageQueue.js');
vi.mock('./hooks/useAutoAcceptIndicator.js');
vi.mock('./hooks/useApprovalModeIndicator.js');
vi.mock('./hooks/useGitBranchName.js');
vi.mock('./contexts/VimModeContext.js');
vi.mock('./contexts/SessionContext.js');
@@ -164,7 +164,7 @@ import { useVim } from './hooks/vim.js';
import { useFolderTrust } from './hooks/useFolderTrust.js';
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
import { useApprovalModeIndicator } from './hooks/useApprovalModeIndicator.js';
import { useGitBranchName } from './hooks/useGitBranchName.js';
import { useVimMode } from './contexts/VimModeContext.js';
import { useSessionStats } from './contexts/SessionContext.js';
@@ -236,7 +236,7 @@ describe('AppContainer State Management', () => {
const mockedUseFolderTrust = useFolderTrust as Mock;
const mockedUseIdeTrustListener = useIdeTrustListener as Mock;
const mockedUseMessageQueue = useMessageQueue as Mock;
const mockedUseAutoAcceptIndicator = useAutoAcceptIndicator as Mock;
const mockedUseApprovalModeIndicator = useApprovalModeIndicator as Mock;
const mockedUseGitBranchName = useGitBranchName as Mock;
const mockedUseVimMode = useVimMode as Mock;
const mockedUseSessionStats = useSessionStats as Mock;
@@ -335,7 +335,7 @@ describe('AppContainer State Management', () => {
clearQueue: vi.fn(),
getQueuedMessagesText: vi.fn().mockReturnValue(''),
});
mockedUseAutoAcceptIndicator.mockReturnValue(false);
mockedUseApprovalModeIndicator.mockReturnValue(false);
mockedUseGitBranchName.mockReturnValue('main');
mockedUseVimMode.mockReturnValue({
isVimEnabled: false,

View File

@@ -102,7 +102,7 @@ import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
import type { SessionInfo } from '../utils/sessionUtils.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
import { useApprovalModeIndicator } from './hooks/useApprovalModeIndicator.js';
import { useSessionStats } from './contexts/SessionContext.js';
import { useGitBranchName } from './hooks/useGitBranchName.js';
import {
@@ -854,7 +854,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
);
// Auto-accept indicator
const showAutoAcceptIndicator = useAutoAcceptIndicator({
const showApprovalModeIndicator = useApprovalModeIndicator({
config,
addItem: historyManager.addItem,
onApprovalModeChange: handleApprovalModeChange,
@@ -1590,7 +1590,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
activeHooks,
messageQueue,
queueErrorMessage,
showAutoAcceptIndicator,
showApprovalModeIndicator,
currentModel,
userTier,
proQuotaRequest,
@@ -1682,7 +1682,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
activeHooks,
messageQueue,
queueErrorMessage,
showAutoAcceptIndicator,
showApprovalModeIndicator,
userTier,
proQuotaRequest,
validationRequest,

View File

@@ -5,23 +5,32 @@
*/
import { render } from '../../test-utils/render.js';
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
import { describe, it, expect } from 'vitest';
import { ApprovalMode } from '@google/gemini-cli-core';
describe('AutoAcceptIndicator', () => {
describe('ApprovalModeIndicator', () => {
it('renders correctly for AUTO_EDIT mode', () => {
const { lastFrame } = render(
<AutoAcceptIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
<ApprovalModeIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
);
const output = lastFrame();
expect(output).toContain('accepting edits');
expect(output).toContain('(shift + tab to toggle)');
expect(output).toContain('(shift + tab to cycle)');
});
it('renders correctly for PLAN mode', () => {
const { lastFrame } = render(
<ApprovalModeIndicator approvalMode={ApprovalMode.PLAN} />,
);
const output = lastFrame();
expect(output).toContain('plan mode');
expect(output).toContain('(shift + tab to cycle)');
});
it('renders correctly for YOLO mode', () => {
const { lastFrame } = render(
<AutoAcceptIndicator approvalMode={ApprovalMode.YOLO} />,
<ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
);
const output = lastFrame();
expect(output).toContain('YOLO mode');
@@ -30,7 +39,7 @@ describe('AutoAcceptIndicator', () => {
it('renders nothing for DEFAULT mode', () => {
const { lastFrame } = render(
<AutoAcceptIndicator approvalMode={ApprovalMode.DEFAULT} />,
<ApprovalModeIndicator approvalMode={ApprovalMode.DEFAULT} />,
);
const output = lastFrame();
expect(output).not.toContain('accepting edits');

View File

@@ -9,11 +9,11 @@ import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { ApprovalMode } from '@google/gemini-cli-core';
interface AutoAcceptIndicatorProps {
interface ApprovalModeIndicatorProps {
approvalMode: ApprovalMode;
}
export const AutoAcceptIndicator: React.FC<AutoAcceptIndicatorProps> = ({
export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
approvalMode,
}) => {
let textColor = '';
@@ -24,7 +24,12 @@ export const AutoAcceptIndicator: React.FC<AutoAcceptIndicatorProps> = ({
case ApprovalMode.AUTO_EDIT:
textColor = theme.status.warning;
textContent = 'accepting edits';
subText = ' (shift + tab to toggle)';
subText = ' (shift + tab to cycle)';
break;
case ApprovalMode.PLAN:
textColor = theme.status.success;
textContent = 'plan mode';
subText = ' (shift + tab to cycle)';
break;
case ApprovalMode.YOLO:
textColor = theme.status.error;

View File

@@ -41,8 +41,8 @@ vi.mock('./HookStatusDisplay.js', () => ({
HookStatusDisplay: () => <Text>HookStatusDisplay</Text>,
}));
vi.mock('./AutoAcceptIndicator.js', () => ({
AutoAcceptIndicator: () => <Text>AutoAcceptIndicator</Text>,
vi.mock('./ApprovalModeIndicator.js', () => ({
ApprovalModeIndicator: () => <Text>ApprovalModeIndicator</Text>,
}));
vi.mock('./ShellModeIndicator.js', () => ({
@@ -95,7 +95,7 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
({
streamingState: null,
contextFileNames: [],
showAutoAcceptIndicator: ApprovalMode.DEFAULT,
showApprovalModeIndicator: ApprovalMode.DEFAULT,
messageQueue: [],
showErrorDetails: false,
constrainHeight: false,
@@ -419,15 +419,15 @@ describe('Composer', () => {
expect(lastFrame()).not.toContain('InputPrompt');
});
it('shows AutoAcceptIndicator when approval mode is not default and shell mode is inactive', () => {
it('shows ApprovalModeIndicator when approval mode is not default and shell mode is inactive', () => {
const uiState = createMockUIState({
showAutoAcceptIndicator: ApprovalMode.YOLO,
showApprovalModeIndicator: ApprovalMode.YOLO,
shellModeActive: false,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).toContain('AutoAcceptIndicator');
expect(lastFrame()).toContain('ApprovalModeIndicator');
});
it('shows ShellModeIndicator when shell mode is active', () => {

View File

@@ -8,7 +8,7 @@ import { useState } from 'react';
import { Box, useIsScreenReaderEnabled } from 'ink';
import { LoadingIndicator } from './LoadingIndicator.js';
import { StatusDisplay } from './StatusDisplay.js';
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
import { ShellModeIndicator } from './ShellModeIndicator.js';
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
@@ -42,7 +42,7 @@ export const Composer = () => {
const [suggestionsVisible, setSuggestionsVisible] = useState(false);
const isAlternateBuffer = useAlternateBuffer();
const { showAutoAcceptIndicator } = uiState;
const { showApprovalModeIndicator } = uiState;
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
const hideContextSummary =
suggestionsVisible && suggestionsPosition === 'above';
@@ -92,9 +92,9 @@ export const Composer = () => {
<StatusDisplay hideContextSummary={hideContextSummary} />
</Box>
<Box paddingTop={isNarrow ? 1 : 0}>
{showAutoAcceptIndicator !== ApprovalMode.DEFAULT &&
{showApprovalModeIndicator !== ApprovalMode.DEFAULT &&
!uiState.shellModeActive && (
<AutoAcceptIndicator approvalMode={showAutoAcceptIndicator} />
<ApprovalModeIndicator approvalMode={showApprovalModeIndicator} />
)}
{uiState.shellModeActive && <ShellModeIndicator />}
{!uiState.renderMarkdown && <RawMarkdownIndicator />}
@@ -131,7 +131,7 @@ export const Composer = () => {
commandContext={uiState.commandContext}
shellModeActive={uiState.shellModeActive}
setShellModeActive={uiActions.setShellModeActive}
approvalMode={showAutoAcceptIndicator}
approvalMode={showApprovalModeIndicator}
onEscapePromptChange={uiActions.onEscapePromptChange}
focus={true}
vimHandleInput={uiActions.vimHandleInput}

View File

@@ -90,7 +90,7 @@ export const INFORMATIVE_TIPS = [
'Toggle the todo list display with Ctrl+T…',
'See full, untruncated responses with Ctrl+S…',
'Toggle auto-approval (YOLO mode) for all tools with Ctrl+Y…',
'Toggle auto-accepting edits approval mode with Shift+Tab…',
'Cycle through approval modes (Default, Plan, Auto-Edit) with Shift+Tab…',
'Toggle Markdown rendering (raw markdown mode) with Option+M…',
'Toggle shell mode by typing ! in an empty prompt…',
'Insert a newline with a backslash (\\) followed by Enter…',

View File

@@ -106,7 +106,7 @@ export interface UIState {
activeHooks: ActiveHook[];
messageQueue: string[];
queueErrorMessage: string | null;
showAutoAcceptIndicator: ApprovalMode;
showApprovalModeIndicator: ApprovalMode;
// Quota-related state
userTier: UserTierId | undefined;
proQuotaRequest: ProQuotaDialogRequest | null;

View File

@@ -15,7 +15,7 @@ import {
} from 'vitest';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useAutoAcceptIndicator } from './useAutoAcceptIndicator.js';
import { useApprovalModeIndicator } from './useApprovalModeIndicator.js';
import { Config, ApprovalMode } from '@google/gemini-cli-core';
import type { Config as ActualConfigType } from '@google/gemini-cli-core';
@@ -37,6 +37,7 @@ interface MockConfigInstanceShape {
getApprovalMode: Mock<() => ApprovalMode>;
setApprovalMode: Mock<(value: ApprovalMode) => void>;
isYoloModeDisabled: Mock<() => boolean>;
isPlanEnabled: Mock<() => boolean>;
isTrustedFolder: Mock<() => boolean>;
getCoreTools: Mock<() => string[]>;
getToolDiscoveryCommand: Mock<() => string | undefined>;
@@ -55,7 +56,7 @@ interface MockConfigInstanceShape {
type UseKeypressHandler = (key: Key) => void;
describe('useAutoAcceptIndicator', () => {
describe('useApprovalModeIndicator', () => {
let mockConfigInstance: MockConfigInstanceShape;
let capturedUseKeypressHandler: UseKeypressHandler;
let mockedUseKeypress: MockedFunction<typeof useKeypress>;
@@ -66,7 +67,9 @@ describe('useAutoAcceptIndicator', () => {
(
Config as unknown as MockedFunction<() => MockConfigInstanceShape>
).mockImplementation(() => {
const instanceGetApprovalModeMock = vi.fn();
const instanceGetApprovalModeMock = vi
.fn()
.mockReturnValue(ApprovalMode.DEFAULT);
const instanceSetApprovalModeMock = vi.fn();
const instance: MockConfigInstanceShape = {
@@ -77,6 +80,7 @@ describe('useAutoAcceptIndicator', () => {
(value: ApprovalMode) => void
>,
isYoloModeDisabled: vi.fn().mockReturnValue(false),
isPlanEnabled: vi.fn().mockReturnValue(false),
isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>,
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
@@ -126,7 +130,7 @@ describe('useAutoAcceptIndicator', () => {
it('should initialize with ApprovalMode.AUTO_EDIT if config.getApprovalMode returns ApprovalMode.AUTO_EDIT', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
@@ -138,7 +142,7 @@ describe('useAutoAcceptIndicator', () => {
it('should initialize with ApprovalMode.DEFAULT if config.getApprovalMode returns ApprovalMode.DEFAULT', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
@@ -150,7 +154,7 @@ describe('useAutoAcceptIndicator', () => {
it('should initialize with ApprovalMode.YOLO if config.getApprovalMode returns ApprovalMode.YOLO', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
@@ -159,16 +163,17 @@ describe('useAutoAcceptIndicator', () => {
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
});
it('should toggle the indicator and update config when Shift+Tab or Ctrl+Y is pressed', () => {
it('should cycle the indicator and update config when Shift+Tab or Ctrl+Y is pressed', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
);
expect(result.current).toBe(ApprovalMode.DEFAULT);
// Shift+Tab cycles to AUTO_EDIT
act(() => {
capturedUseKeypressHandler({
name: 'tab',
@@ -188,22 +193,7 @@ describe('useAutoAcceptIndicator', () => {
);
expect(result.current).toBe(ApprovalMode.YOLO);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.DEFAULT,
);
expect(result.current).toBe(ApprovalMode.DEFAULT);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.YOLO,
);
expect(result.current).toBe(ApprovalMode.YOLO);
// Shift+Tab cycles back to DEFAULT (since PLAN is disabled by default in mock)
act(() => {
capturedUseKeypressHandler({
name: 'tab',
@@ -215,22 +205,67 @@ describe('useAutoAcceptIndicator', () => {
);
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
// Ctrl+Y toggles YOLO
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.YOLO,
);
expect(result.current).toBe(ApprovalMode.YOLO);
// Shift+Tab from YOLO jumps to AUTO_EDIT
act(() => {
capturedUseKeypressHandler({
name: 'tab',
shift: true,
} as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.AUTO_EDIT,
);
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
});
it('should cycle through DEFAULT -> AUTO_EDIT -> PLAN -> DEFAULT when plan is enabled', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
mockConfigInstance.isPlanEnabled.mockReturnValue(true);
renderHook(() =>
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
);
// DEFAULT -> PLAN
act(() => {
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.PLAN,
);
// 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);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.DEFAULT,
);
expect(result.current).toBe(ApprovalMode.DEFAULT);
});
it('should not toggle if only one key or other keys combinations are pressed', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
@@ -290,7 +325,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const { result, rerender } = renderHook(
(props: { config: ActualConfigType; addItem: () => void }) =>
useAutoAcceptIndicator(props),
useApprovalModeIndicator(props),
{
initialProps: {
config: mockConfigInstance as unknown as ActualConfigType,
@@ -324,7 +359,7 @@ describe('useAutoAcceptIndicator', () => {
});
const mockAddItem = vi.fn();
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -354,7 +389,7 @@ describe('useAutoAcceptIndicator', () => {
});
const mockAddItem = vi.fn();
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -382,7 +417,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
const mockAddItem = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -404,7 +439,7 @@ describe('useAutoAcceptIndicator', () => {
);
const mockAddItem = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -433,7 +468,7 @@ describe('useAutoAcceptIndicator', () => {
const mockAddItem = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -484,7 +519,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const mockAddItem = vi.fn();
const { result } = renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
@@ -517,7 +552,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange,
}),
@@ -539,7 +574,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange,
}),
@@ -563,7 +598,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange,
}),
@@ -583,7 +618,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
}),
);
@@ -604,7 +639,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn();
renderHook(() =>
useAutoAcceptIndicator({
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange,
}),

View File

@@ -11,25 +11,24 @@ import { keyMatchers, Command } from '../keyMatchers.js';
import type { HistoryItemWithoutId } from '../types.js';
import { MessageType } from '../types.js';
export interface UseAutoAcceptIndicatorArgs {
export interface UseApprovalModeIndicatorArgs {
config: Config;
addItem?: (item: HistoryItemWithoutId, timestamp: number) => void;
onApprovalModeChange?: (mode: ApprovalMode) => void;
isActive?: boolean;
}
export function useAutoAcceptIndicator({
export function useApprovalModeIndicator({
config,
addItem,
onApprovalModeChange,
isActive = true,
}: UseAutoAcceptIndicatorArgs): ApprovalMode {
}: UseApprovalModeIndicatorArgs): ApprovalMode {
const currentConfigValue = config.getApprovalMode();
const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] =
useState(currentConfigValue);
const [showApprovalMode, setApprovalMode] = useState(currentConfigValue);
useEffect(() => {
setShowAutoAcceptIndicator(currentConfigValue);
setApprovalMode(currentConfigValue);
}, [currentConfigValue]);
useKeypress(
@@ -56,18 +55,32 @@ export function useAutoAcceptIndicator({
config.getApprovalMode() === ApprovalMode.YOLO
? ApprovalMode.DEFAULT
: ApprovalMode.YOLO;
} else if (keyMatchers[Command.TOGGLE_AUTO_EDIT](key)) {
nextApprovalMode =
config.getApprovalMode() === ApprovalMode.AUTO_EDIT
? ApprovalMode.DEFAULT
: ApprovalMode.AUTO_EDIT;
} else if (keyMatchers[Command.CYCLE_APPROVAL_MODE](key)) {
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 = ApprovalMode.DEFAULT;
break;
case ApprovalMode.YOLO:
nextApprovalMode = ApprovalMode.AUTO_EDIT;
break;
default:
}
}
if (nextApprovalMode) {
try {
config.setApprovalMode(nextApprovalMode);
// Update local state immediately for responsiveness
setShowAutoAcceptIndicator(nextApprovalMode);
setApprovalMode(nextApprovalMode);
// Notify the central handler about the approval mode change
onApprovalModeChange?.(nextApprovalMode);
@@ -87,5 +100,5 @@ export function useAutoAcceptIndicator({
{ isActive },
);
return showAutoAcceptIndicator;
return showApprovalMode;
}

View File

@@ -336,7 +336,7 @@ describe('keyMatchers', () => {
negative: [createKey('y'), createKey('y', { meta: true })],
},
{
command: Command.TOGGLE_AUTO_EDIT,
command: Command.CYCLE_APPROVAL_MODE,
positive: [createKey('tab', { shift: true })],
negative: [createKey('tab')],
},