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
+2 -2
View File
@@ -94,14 +94,14 @@ available combinations.
#### App Controls #### App Controls
| Action | Keys | | Action | Keys |
| ------------------------------------------------------------------------------------------------ | ---------------- | | ----------------------------------------------------------------------------------------------------- | ---------------- |
| Toggle detailed error information. | `F12` | | Toggle detailed error information. | `F12` |
| Toggle the full TODO list. | `Ctrl + T` | | Toggle the full TODO list. | `Ctrl + T` |
| Show IDE context details. | `Ctrl + G` | | Show IDE context details. | `Ctrl + G` |
| Toggle Markdown rendering. | `Cmd + M` | | Toggle Markdown rendering. | `Cmd + M` |
| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` | | Toggle copy mode when in alternate buffer mode. | `Ctrl + S` |
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` | | Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Toggle Auto Edit (auto-accept edits) mode. | `Shift + Tab` | | 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` | | 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 shell input from the gemini input. | `Tab (no Shift)` |
| Focus the Gemini input from the shell input. | `Tab` | | Focus the Gemini input from the shell input. | `Tab` |
+5 -4
View File
@@ -76,7 +76,7 @@ export enum Command {
TOGGLE_MARKDOWN = 'app.toggleMarkdown', TOGGLE_MARKDOWN = 'app.toggleMarkdown',
TOGGLE_COPY_MODE = 'app.toggleCopyMode', TOGGLE_COPY_MODE = 'app.toggleCopyMode',
TOGGLE_YOLO = 'app.toggleYolo', TOGGLE_YOLO = 'app.toggleYolo',
TOGGLE_AUTO_EDIT = 'app.toggleAutoEdit', CYCLE_APPROVAL_MODE = 'app.cycleApprovalMode',
SHOW_MORE_LINES = 'app.showMoreLines', SHOW_MORE_LINES = 'app.showMoreLines',
FOCUS_SHELL_INPUT = 'app.focusShellInput', FOCUS_SHELL_INPUT = 'app.focusShellInput',
UNFOCUS_SHELL_INPUT = 'app.unfocusShellInput', UNFOCUS_SHELL_INPUT = 'app.unfocusShellInput',
@@ -246,7 +246,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.TOGGLE_MARKDOWN]: [{ key: 'm', command: true }], [Command.TOGGLE_MARKDOWN]: [{ key: 'm', command: true }],
[Command.TOGGLE_COPY_MODE]: [{ key: 's', ctrl: true }], [Command.TOGGLE_COPY_MODE]: [{ key: 's', ctrl: true }],
[Command.TOGGLE_YOLO]: [{ key: 'y', 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.SHOW_MORE_LINES]: [{ key: 's', ctrl: true }],
[Command.FOCUS_SHELL_INPUT]: [{ key: 'tab', shift: false }], [Command.FOCUS_SHELL_INPUT]: [{ key: 'tab', shift: false }],
[Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab' }], [Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab' }],
@@ -352,7 +352,7 @@ export const commandCategories: readonly CommandCategory[] = [
Command.TOGGLE_MARKDOWN, Command.TOGGLE_MARKDOWN,
Command.TOGGLE_COPY_MODE, Command.TOGGLE_COPY_MODE,
Command.TOGGLE_YOLO, Command.TOGGLE_YOLO,
Command.TOGGLE_AUTO_EDIT, Command.CYCLE_APPROVAL_MODE,
Command.SHOW_MORE_LINES, Command.SHOW_MORE_LINES,
Command.FOCUS_SHELL_INPUT, Command.FOCUS_SHELL_INPUT,
Command.UNFOCUS_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_MARKDOWN]: 'Toggle Markdown rendering.',
[Command.TOGGLE_COPY_MODE]: 'Toggle copy mode when in alternate buffer mode.', [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_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]: [Command.SHOW_MORE_LINES]:
'Expand a height-constrained response to show additional lines when not in alternate buffer mode.', '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.', [Command.FOCUS_SHELL_INPUT]: 'Focus the shell input from the gemini input.',
+4 -4
View File
@@ -136,7 +136,7 @@ vi.mock('./hooks/useLoadingIndicator.js');
vi.mock('./hooks/useFolderTrust.js'); vi.mock('./hooks/useFolderTrust.js');
vi.mock('./hooks/useIdeTrustListener.js'); vi.mock('./hooks/useIdeTrustListener.js');
vi.mock('./hooks/useMessageQueue.js'); vi.mock('./hooks/useMessageQueue.js');
vi.mock('./hooks/useAutoAcceptIndicator.js'); vi.mock('./hooks/useApprovalModeIndicator.js');
vi.mock('./hooks/useGitBranchName.js'); vi.mock('./hooks/useGitBranchName.js');
vi.mock('./contexts/VimModeContext.js'); vi.mock('./contexts/VimModeContext.js');
vi.mock('./contexts/SessionContext.js'); vi.mock('./contexts/SessionContext.js');
@@ -164,7 +164,7 @@ import { useVim } from './hooks/vim.js';
import { useFolderTrust } from './hooks/useFolderTrust.js'; import { useFolderTrust } from './hooks/useFolderTrust.js';
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js'; import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
import { useMessageQueue } from './hooks/useMessageQueue.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 { useGitBranchName } from './hooks/useGitBranchName.js';
import { useVimMode } from './contexts/VimModeContext.js'; import { useVimMode } from './contexts/VimModeContext.js';
import { useSessionStats } from './contexts/SessionContext.js'; import { useSessionStats } from './contexts/SessionContext.js';
@@ -236,7 +236,7 @@ describe('AppContainer State Management', () => {
const mockedUseFolderTrust = useFolderTrust as Mock; const mockedUseFolderTrust = useFolderTrust as Mock;
const mockedUseIdeTrustListener = useIdeTrustListener as Mock; const mockedUseIdeTrustListener = useIdeTrustListener as Mock;
const mockedUseMessageQueue = useMessageQueue as Mock; const mockedUseMessageQueue = useMessageQueue as Mock;
const mockedUseAutoAcceptIndicator = useAutoAcceptIndicator as Mock; const mockedUseApprovalModeIndicator = useApprovalModeIndicator as Mock;
const mockedUseGitBranchName = useGitBranchName as Mock; const mockedUseGitBranchName = useGitBranchName as Mock;
const mockedUseVimMode = useVimMode as Mock; const mockedUseVimMode = useVimMode as Mock;
const mockedUseSessionStats = useSessionStats as Mock; const mockedUseSessionStats = useSessionStats as Mock;
@@ -335,7 +335,7 @@ describe('AppContainer State Management', () => {
clearQueue: vi.fn(), clearQueue: vi.fn(),
getQueuedMessagesText: vi.fn().mockReturnValue(''), getQueuedMessagesText: vi.fn().mockReturnValue(''),
}); });
mockedUseAutoAcceptIndicator.mockReturnValue(false); mockedUseApprovalModeIndicator.mockReturnValue(false);
mockedUseGitBranchName.mockReturnValue('main'); mockedUseGitBranchName.mockReturnValue('main');
mockedUseVimMode.mockReturnValue({ mockedUseVimMode.mockReturnValue({
isVimEnabled: false, isVimEnabled: false,
+4 -4
View File
@@ -102,7 +102,7 @@ import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js'; import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
import type { SessionInfo } from '../utils/sessionUtils.js'; import type { SessionInfo } from '../utils/sessionUtils.js';
import { useMessageQueue } from './hooks/useMessageQueue.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 { useSessionStats } from './contexts/SessionContext.js';
import { useGitBranchName } from './hooks/useGitBranchName.js'; import { useGitBranchName } from './hooks/useGitBranchName.js';
import { import {
@@ -854,7 +854,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
); );
// Auto-accept indicator // Auto-accept indicator
const showAutoAcceptIndicator = useAutoAcceptIndicator({ const showApprovalModeIndicator = useApprovalModeIndicator({
config, config,
addItem: historyManager.addItem, addItem: historyManager.addItem,
onApprovalModeChange: handleApprovalModeChange, onApprovalModeChange: handleApprovalModeChange,
@@ -1590,7 +1590,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
activeHooks, activeHooks,
messageQueue, messageQueue,
queueErrorMessage, queueErrorMessage,
showAutoAcceptIndicator, showApprovalModeIndicator,
currentModel, currentModel,
userTier, userTier,
proQuotaRequest, proQuotaRequest,
@@ -1682,7 +1682,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
activeHooks, activeHooks,
messageQueue, messageQueue,
queueErrorMessage, queueErrorMessage,
showAutoAcceptIndicator, showApprovalModeIndicator,
userTier, userTier,
proQuotaRequest, proQuotaRequest,
validationRequest, validationRequest,
@@ -5,23 +5,32 @@
*/ */
import { render } from '../../test-utils/render.js'; import { render } from '../../test-utils/render.js';
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js'; import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { ApprovalMode } from '@google/gemini-cli-core'; import { ApprovalMode } from '@google/gemini-cli-core';
describe('AutoAcceptIndicator', () => { describe('ApprovalModeIndicator', () => {
it('renders correctly for AUTO_EDIT mode', () => { it('renders correctly for AUTO_EDIT mode', () => {
const { lastFrame } = render( const { lastFrame } = render(
<AutoAcceptIndicator approvalMode={ApprovalMode.AUTO_EDIT} />, <ApprovalModeIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
); );
const output = lastFrame(); const output = lastFrame();
expect(output).toContain('accepting edits'); 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', () => { it('renders correctly for YOLO mode', () => {
const { lastFrame } = render( const { lastFrame } = render(
<AutoAcceptIndicator approvalMode={ApprovalMode.YOLO} />, <ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
); );
const output = lastFrame(); const output = lastFrame();
expect(output).toContain('YOLO mode'); expect(output).toContain('YOLO mode');
@@ -30,7 +39,7 @@ describe('AutoAcceptIndicator', () => {
it('renders nothing for DEFAULT mode', () => { it('renders nothing for DEFAULT mode', () => {
const { lastFrame } = render( const { lastFrame } = render(
<AutoAcceptIndicator approvalMode={ApprovalMode.DEFAULT} />, <ApprovalModeIndicator approvalMode={ApprovalMode.DEFAULT} />,
); );
const output = lastFrame(); const output = lastFrame();
expect(output).not.toContain('accepting edits'); expect(output).not.toContain('accepting edits');
@@ -9,11 +9,11 @@ import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { ApprovalMode } from '@google/gemini-cli-core'; import { ApprovalMode } from '@google/gemini-cli-core';
interface AutoAcceptIndicatorProps { interface ApprovalModeIndicatorProps {
approvalMode: ApprovalMode; approvalMode: ApprovalMode;
} }
export const AutoAcceptIndicator: React.FC<AutoAcceptIndicatorProps> = ({ export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
approvalMode, approvalMode,
}) => { }) => {
let textColor = ''; let textColor = '';
@@ -24,7 +24,12 @@ export const AutoAcceptIndicator: React.FC<AutoAcceptIndicatorProps> = ({
case ApprovalMode.AUTO_EDIT: case ApprovalMode.AUTO_EDIT:
textColor = theme.status.warning; textColor = theme.status.warning;
textContent = 'accepting edits'; 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; break;
case ApprovalMode.YOLO: case ApprovalMode.YOLO:
textColor = theme.status.error; textColor = theme.status.error;
@@ -41,8 +41,8 @@ vi.mock('./HookStatusDisplay.js', () => ({
HookStatusDisplay: () => <Text>HookStatusDisplay</Text>, HookStatusDisplay: () => <Text>HookStatusDisplay</Text>,
})); }));
vi.mock('./AutoAcceptIndicator.js', () => ({ vi.mock('./ApprovalModeIndicator.js', () => ({
AutoAcceptIndicator: () => <Text>AutoAcceptIndicator</Text>, ApprovalModeIndicator: () => <Text>ApprovalModeIndicator</Text>,
})); }));
vi.mock('./ShellModeIndicator.js', () => ({ vi.mock('./ShellModeIndicator.js', () => ({
@@ -95,7 +95,7 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
({ ({
streamingState: null, streamingState: null,
contextFileNames: [], contextFileNames: [],
showAutoAcceptIndicator: ApprovalMode.DEFAULT, showApprovalModeIndicator: ApprovalMode.DEFAULT,
messageQueue: [], messageQueue: [],
showErrorDetails: false, showErrorDetails: false,
constrainHeight: false, constrainHeight: false,
@@ -419,15 +419,15 @@ describe('Composer', () => {
expect(lastFrame()).not.toContain('InputPrompt'); 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({ const uiState = createMockUIState({
showAutoAcceptIndicator: ApprovalMode.YOLO, showApprovalModeIndicator: ApprovalMode.YOLO,
shellModeActive: false, shellModeActive: false,
}); });
const { lastFrame } = renderComposer(uiState); const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).toContain('AutoAcceptIndicator'); expect(lastFrame()).toContain('ApprovalModeIndicator');
}); });
it('shows ShellModeIndicator when shell mode is active', () => { it('shows ShellModeIndicator when shell mode is active', () => {
+5 -5
View File
@@ -8,7 +8,7 @@ import { useState } from 'react';
import { Box, useIsScreenReaderEnabled } from 'ink'; import { Box, useIsScreenReaderEnabled } from 'ink';
import { LoadingIndicator } from './LoadingIndicator.js'; import { LoadingIndicator } from './LoadingIndicator.js';
import { StatusDisplay } from './StatusDisplay.js'; import { StatusDisplay } from './StatusDisplay.js';
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js'; import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
import { ShellModeIndicator } from './ShellModeIndicator.js'; import { ShellModeIndicator } from './ShellModeIndicator.js';
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js'; import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js'; import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
@@ -42,7 +42,7 @@ export const Composer = () => {
const [suggestionsVisible, setSuggestionsVisible] = useState(false); const [suggestionsVisible, setSuggestionsVisible] = useState(false);
const isAlternateBuffer = useAlternateBuffer(); const isAlternateBuffer = useAlternateBuffer();
const { showAutoAcceptIndicator } = uiState; const { showApprovalModeIndicator } = uiState;
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below'; const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
const hideContextSummary = const hideContextSummary =
suggestionsVisible && suggestionsPosition === 'above'; suggestionsVisible && suggestionsPosition === 'above';
@@ -92,9 +92,9 @@ export const Composer = () => {
<StatusDisplay hideContextSummary={hideContextSummary} /> <StatusDisplay hideContextSummary={hideContextSummary} />
</Box> </Box>
<Box paddingTop={isNarrow ? 1 : 0}> <Box paddingTop={isNarrow ? 1 : 0}>
{showAutoAcceptIndicator !== ApprovalMode.DEFAULT && {showApprovalModeIndicator !== ApprovalMode.DEFAULT &&
!uiState.shellModeActive && ( !uiState.shellModeActive && (
<AutoAcceptIndicator approvalMode={showAutoAcceptIndicator} /> <ApprovalModeIndicator approvalMode={showApprovalModeIndicator} />
)} )}
{uiState.shellModeActive && <ShellModeIndicator />} {uiState.shellModeActive && <ShellModeIndicator />}
{!uiState.renderMarkdown && <RawMarkdownIndicator />} {!uiState.renderMarkdown && <RawMarkdownIndicator />}
@@ -131,7 +131,7 @@ export const Composer = () => {
commandContext={uiState.commandContext} commandContext={uiState.commandContext}
shellModeActive={uiState.shellModeActive} shellModeActive={uiState.shellModeActive}
setShellModeActive={uiActions.setShellModeActive} setShellModeActive={uiActions.setShellModeActive}
approvalMode={showAutoAcceptIndicator} approvalMode={showApprovalModeIndicator}
onEscapePromptChange={uiActions.onEscapePromptChange} onEscapePromptChange={uiActions.onEscapePromptChange}
focus={true} focus={true}
vimHandleInput={uiActions.vimHandleInput} vimHandleInput={uiActions.vimHandleInput}
+1 -1
View File
@@ -90,7 +90,7 @@ export const INFORMATIVE_TIPS = [
'Toggle the todo list display with Ctrl+T…', 'Toggle the todo list display with Ctrl+T…',
'See full, untruncated responses with Ctrl+S…', 'See full, untruncated responses with Ctrl+S…',
'Toggle auto-approval (YOLO mode) for all tools with Ctrl+Y…', '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 Markdown rendering (raw markdown mode) with Option+M…',
'Toggle shell mode by typing ! in an empty prompt…', 'Toggle shell mode by typing ! in an empty prompt…',
'Insert a newline with a backslash (\\) followed by Enter…', 'Insert a newline with a backslash (\\) followed by Enter…',
@@ -106,7 +106,7 @@ export interface UIState {
activeHooks: ActiveHook[]; activeHooks: ActiveHook[];
messageQueue: string[]; messageQueue: string[];
queueErrorMessage: string | null; queueErrorMessage: string | null;
showAutoAcceptIndicator: ApprovalMode; showApprovalModeIndicator: ApprovalMode;
// Quota-related state // Quota-related state
userTier: UserTierId | undefined; userTier: UserTierId | undefined;
proQuotaRequest: ProQuotaDialogRequest | null; proQuotaRequest: ProQuotaDialogRequest | null;
@@ -15,7 +15,7 @@ import {
} from 'vitest'; } from 'vitest';
import { act } from 'react'; import { act } from 'react';
import { renderHook } from '../../test-utils/render.js'; 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 { Config, ApprovalMode } from '@google/gemini-cli-core';
import type { Config as ActualConfigType } from '@google/gemini-cli-core'; import type { Config as ActualConfigType } from '@google/gemini-cli-core';
@@ -37,6 +37,7 @@ interface MockConfigInstanceShape {
getApprovalMode: Mock<() => ApprovalMode>; getApprovalMode: Mock<() => ApprovalMode>;
setApprovalMode: Mock<(value: ApprovalMode) => void>; setApprovalMode: Mock<(value: ApprovalMode) => void>;
isYoloModeDisabled: Mock<() => boolean>; isYoloModeDisabled: Mock<() => boolean>;
isPlanEnabled: Mock<() => boolean>;
isTrustedFolder: Mock<() => boolean>; isTrustedFolder: Mock<() => boolean>;
getCoreTools: Mock<() => string[]>; getCoreTools: Mock<() => string[]>;
getToolDiscoveryCommand: Mock<() => string | undefined>; getToolDiscoveryCommand: Mock<() => string | undefined>;
@@ -55,7 +56,7 @@ interface MockConfigInstanceShape {
type UseKeypressHandler = (key: Key) => void; type UseKeypressHandler = (key: Key) => void;
describe('useAutoAcceptIndicator', () => { describe('useApprovalModeIndicator', () => {
let mockConfigInstance: MockConfigInstanceShape; let mockConfigInstance: MockConfigInstanceShape;
let capturedUseKeypressHandler: UseKeypressHandler; let capturedUseKeypressHandler: UseKeypressHandler;
let mockedUseKeypress: MockedFunction<typeof useKeypress>; let mockedUseKeypress: MockedFunction<typeof useKeypress>;
@@ -66,7 +67,9 @@ describe('useAutoAcceptIndicator', () => {
( (
Config as unknown as MockedFunction<() => MockConfigInstanceShape> Config as unknown as MockedFunction<() => MockConfigInstanceShape>
).mockImplementation(() => { ).mockImplementation(() => {
const instanceGetApprovalModeMock = vi.fn(); const instanceGetApprovalModeMock = vi
.fn()
.mockReturnValue(ApprovalMode.DEFAULT);
const instanceSetApprovalModeMock = vi.fn(); const instanceSetApprovalModeMock = vi.fn();
const instance: MockConfigInstanceShape = { const instance: MockConfigInstanceShape = {
@@ -77,6 +80,7 @@ describe('useAutoAcceptIndicator', () => {
(value: ApprovalMode) => void (value: ApprovalMode) => void
>, >,
isYoloModeDisabled: vi.fn().mockReturnValue(false), isYoloModeDisabled: vi.fn().mockReturnValue(false),
isPlanEnabled: vi.fn().mockReturnValue(false),
isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>, isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>,
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>, getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock< 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', () => { it('should initialize with ApprovalMode.AUTO_EDIT if config.getApprovalMode returns ApprovalMode.AUTO_EDIT', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT); mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
const { result } = renderHook(() => const { result } = renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(), addItem: vi.fn(),
}), }),
@@ -138,7 +142,7 @@ describe('useAutoAcceptIndicator', () => {
it('should initialize with ApprovalMode.DEFAULT if config.getApprovalMode returns ApprovalMode.DEFAULT', () => { it('should initialize with ApprovalMode.DEFAULT if config.getApprovalMode returns ApprovalMode.DEFAULT', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const { result } = renderHook(() => const { result } = renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(), addItem: vi.fn(),
}), }),
@@ -150,7 +154,7 @@ describe('useAutoAcceptIndicator', () => {
it('should initialize with ApprovalMode.YOLO if config.getApprovalMode returns ApprovalMode.YOLO', () => { it('should initialize with ApprovalMode.YOLO if config.getApprovalMode returns ApprovalMode.YOLO', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO); mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
const { result } = renderHook(() => const { result } = renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(), addItem: vi.fn(),
}), }),
@@ -159,16 +163,17 @@ describe('useAutoAcceptIndicator', () => {
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1); 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); mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const { result } = renderHook(() => const { result } = renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(), addItem: vi.fn(),
}), }),
); );
expect(result.current).toBe(ApprovalMode.DEFAULT); expect(result.current).toBe(ApprovalMode.DEFAULT);
// Shift+Tab cycles to AUTO_EDIT
act(() => { act(() => {
capturedUseKeypressHandler({ capturedUseKeypressHandler({
name: 'tab', name: 'tab',
@@ -188,22 +193,7 @@ describe('useAutoAcceptIndicator', () => {
); );
expect(result.current).toBe(ApprovalMode.YOLO); expect(result.current).toBe(ApprovalMode.YOLO);
act(() => { // Shift+Tab cycles back to DEFAULT (since PLAN is disabled by default in mock)
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);
act(() => { act(() => {
capturedUseKeypressHandler({ capturedUseKeypressHandler({
name: 'tab', name: 'tab',
@@ -215,22 +205,67 @@ describe('useAutoAcceptIndicator', () => {
); );
expect(result.current).toBe(ApprovalMode.AUTO_EDIT); 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(() => { act(() => {
capturedUseKeypressHandler({ capturedUseKeypressHandler({
name: 'tab', name: 'tab',
shift: true, shift: true,
} as Key); } 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( expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.DEFAULT, ApprovalMode.DEFAULT,
); );
expect(result.current).toBe(ApprovalMode.DEFAULT);
}); });
it('should not toggle if only one key or other keys combinations are pressed', () => { it('should not toggle if only one key or other keys combinations are pressed', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
renderHook(() => renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(), addItem: vi.fn(),
}), }),
@@ -290,7 +325,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const { result, rerender } = renderHook( const { result, rerender } = renderHook(
(props: { config: ActualConfigType; addItem: () => void }) => (props: { config: ActualConfigType; addItem: () => void }) =>
useAutoAcceptIndicator(props), useApprovalModeIndicator(props),
{ {
initialProps: { initialProps: {
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
@@ -324,7 +359,7 @@ describe('useAutoAcceptIndicator', () => {
}); });
const mockAddItem = vi.fn(); const mockAddItem = vi.fn();
const { result } = renderHook(() => const { result } = renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem, addItem: mockAddItem,
}), }),
@@ -354,7 +389,7 @@ describe('useAutoAcceptIndicator', () => {
}); });
const mockAddItem = vi.fn(); const mockAddItem = vi.fn();
const { result } = renderHook(() => const { result } = renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem, addItem: mockAddItem,
}), }),
@@ -382,7 +417,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO); mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
const mockAddItem = vi.fn(); const mockAddItem = vi.fn();
renderHook(() => renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem, addItem: mockAddItem,
}), }),
@@ -404,7 +439,7 @@ describe('useAutoAcceptIndicator', () => {
); );
const mockAddItem = vi.fn(); const mockAddItem = vi.fn();
renderHook(() => renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem, addItem: mockAddItem,
}), }),
@@ -433,7 +468,7 @@ describe('useAutoAcceptIndicator', () => {
const mockAddItem = vi.fn(); const mockAddItem = vi.fn();
renderHook(() => renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem, addItem: mockAddItem,
}), }),
@@ -484,7 +519,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const mockAddItem = vi.fn(); const mockAddItem = vi.fn();
const { result } = renderHook(() => const { result } = renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem, addItem: mockAddItem,
}), }),
@@ -517,7 +552,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn(); const mockOnApprovalModeChange = vi.fn();
renderHook(() => renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange, onApprovalModeChange: mockOnApprovalModeChange,
}), }),
@@ -539,7 +574,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn(); const mockOnApprovalModeChange = vi.fn();
renderHook(() => renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange, onApprovalModeChange: mockOnApprovalModeChange,
}), }),
@@ -563,7 +598,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn(); const mockOnApprovalModeChange = vi.fn();
renderHook(() => renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange, onApprovalModeChange: mockOnApprovalModeChange,
}), }),
@@ -583,7 +618,7 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
renderHook(() => renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
}), }),
); );
@@ -604,7 +639,7 @@ describe('useAutoAcceptIndicator', () => {
const mockOnApprovalModeChange = vi.fn(); const mockOnApprovalModeChange = vi.fn();
renderHook(() => renderHook(() =>
useAutoAcceptIndicator({ useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType, config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange, onApprovalModeChange: mockOnApprovalModeChange,
}), }),
@@ -11,25 +11,24 @@ import { keyMatchers, Command } from '../keyMatchers.js';
import type { HistoryItemWithoutId } from '../types.js'; import type { HistoryItemWithoutId } from '../types.js';
import { MessageType } from '../types.js'; import { MessageType } from '../types.js';
export interface UseAutoAcceptIndicatorArgs { export interface UseApprovalModeIndicatorArgs {
config: Config; config: Config;
addItem?: (item: HistoryItemWithoutId, timestamp: number) => void; addItem?: (item: HistoryItemWithoutId, timestamp: number) => void;
onApprovalModeChange?: (mode: ApprovalMode) => void; onApprovalModeChange?: (mode: ApprovalMode) => void;
isActive?: boolean; isActive?: boolean;
} }
export function useAutoAcceptIndicator({ export function useApprovalModeIndicator({
config, config,
addItem, addItem,
onApprovalModeChange, onApprovalModeChange,
isActive = true, isActive = true,
}: UseAutoAcceptIndicatorArgs): ApprovalMode { }: UseApprovalModeIndicatorArgs): ApprovalMode {
const currentConfigValue = config.getApprovalMode(); const currentConfigValue = config.getApprovalMode();
const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] = const [showApprovalMode, setApprovalMode] = useState(currentConfigValue);
useState(currentConfigValue);
useEffect(() => { useEffect(() => {
setShowAutoAcceptIndicator(currentConfigValue); setApprovalMode(currentConfigValue);
}, [currentConfigValue]); }, [currentConfigValue]);
useKeypress( useKeypress(
@@ -56,18 +55,32 @@ export function useAutoAcceptIndicator({
config.getApprovalMode() === ApprovalMode.YOLO config.getApprovalMode() === ApprovalMode.YOLO
? ApprovalMode.DEFAULT ? ApprovalMode.DEFAULT
: ApprovalMode.YOLO; : ApprovalMode.YOLO;
} else if (keyMatchers[Command.TOGGLE_AUTO_EDIT](key)) { } else if (keyMatchers[Command.CYCLE_APPROVAL_MODE](key)) {
nextApprovalMode = const currentMode = config.getApprovalMode();
config.getApprovalMode() === ApprovalMode.AUTO_EDIT switch (currentMode) {
? ApprovalMode.DEFAULT case ApprovalMode.DEFAULT:
nextApprovalMode = config.isPlanEnabled()
? ApprovalMode.PLAN
: ApprovalMode.AUTO_EDIT; : 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) { if (nextApprovalMode) {
try { try {
config.setApprovalMode(nextApprovalMode); config.setApprovalMode(nextApprovalMode);
// Update local state immediately for responsiveness // Update local state immediately for responsiveness
setShowAutoAcceptIndicator(nextApprovalMode); setApprovalMode(nextApprovalMode);
// Notify the central handler about the approval mode change // Notify the central handler about the approval mode change
onApprovalModeChange?.(nextApprovalMode); onApprovalModeChange?.(nextApprovalMode);
@@ -87,5 +100,5 @@ export function useAutoAcceptIndicator({
{ isActive }, { isActive },
); );
return showAutoAcceptIndicator; return showApprovalMode;
} }
+1 -1
View File
@@ -336,7 +336,7 @@ describe('keyMatchers', () => {
negative: [createKey('y'), createKey('y', { meta: true })], negative: [createKey('y'), createKey('y', { meta: true })],
}, },
{ {
command: Command.TOGGLE_AUTO_EDIT, command: Command.CYCLE_APPROVAL_MODE,
positive: [createKey('tab', { shift: true })], positive: [createKey('tab', { shift: true })],
negative: [createKey('tab')], negative: [createKey('tab')],
}, },