feat(policy): map --yolo to allowedTools wildcard policy

This PR maps the `--yolo` flag natively into a wildcard policy array
(`allowedTools: ["*"]`) and removes the concept of `ApprovalMode.YOLO` as a
distinct state in the application, fulfilling issue #11303.

This removes the hardcoded `ApprovalMode.YOLO` state and its associated
UI/bypasses. The `PolicyEngine` now evaluates YOLO purely via data-driven rules.

- Removes `ApprovalMode.YOLO`
- Removes UI toggle (`Ctrl+Y`) and indicators for YOLO
- Removes `yolo.toml`
- Updates A2A server and CLI config logic to translate YOLO into a wildcard tool
- Rewrites policy engine tests to evaluate the wildcard
- Enforces enterprise `disableYoloMode` and `secureModeEnabled` controls
  by actively preventing manual `--allowed-tools=*` bypasses.

Fixes #11303
This commit is contained in:
Spencer
2026-03-19 02:43:14 +00:00
parent 08b926796c
commit 822e098d32
77 changed files with 541 additions and 746 deletions
+1 -2
View File
@@ -374,7 +374,6 @@ describe('GeminiAgent', () => {
name: 'Auto Edit',
description: 'Auto-approves edit tools',
},
{ id: 'yolo', name: 'YOLO', description: 'Auto-approves all tools' },
],
currentModeId: 'default',
});
@@ -452,7 +451,7 @@ describe('GeminiAgent', () => {
name: 'Auto Edit',
description: 'Auto-approves edit tools',
},
{ id: 'yolo', name: 'YOLO', description: 'Auto-approves all tools' },
{ id: 'plan', name: 'Plan', description: 'Read-only mode' },
],
currentModeId: 'plan',
-5
View File
@@ -1986,11 +1986,6 @@ function buildAvailableModes(isPlanEnabled: boolean): acp.SessionMode[] {
name: 'Auto Edit',
description: 'Auto-approves edit tools',
},
{
id: ApprovalMode.YOLO,
name: 'YOLO',
description: 'Auto-approves all tools',
},
];
if (isPlanEnabled) {
-5
View File
@@ -211,11 +211,6 @@ describe('GeminiAgent Session Resume', () => {
name: 'Auto Edit',
description: 'Auto-approves edit tools',
},
{
id: ApprovalMode.YOLO,
name: 'YOLO',
description: 'Auto-approves all tools',
},
{
id: ApprovalMode.PLAN,
name: 'Plan',
+6 -6
View File
@@ -1472,7 +1472,7 @@ describe('Approval mode tool exclusion logic', () => {
await expect(
loadCliConfig(settings, 'test-session', invalidArgv as CliArgs),
).rejects.toThrow(
'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, plan, default',
'Invalid approval mode: invalid_mode. Valid values are: auto_edit, plan, default (yolo is mapped to allowed-tools)',
);
});
@@ -2750,7 +2750,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
});
it('should set YOLO approval mode when -y flag is used', async () => {
@@ -2761,7 +2761,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
});
it('should set DEFAULT approval mode when --approval-mode=default', async () => {
@@ -2794,7 +2794,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
});
it('should prioritize --approval-mode over --yolo when both would be valid (but validation prevents this)', async () => {
@@ -2820,7 +2820,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
});
it('should set Plan approval mode when --approval-mode=plan is used and plan is enabled', async () => {
@@ -2971,7 +2971,7 @@ describe('loadCliConfig approval mode', () => {
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
});
it('should respect plan mode from settings when plan is enabled', async () => {
+15 -11
View File
@@ -661,10 +661,13 @@ export async function loadCliConfig(
? settings.general?.defaultApprovalMode
: undefined);
let isYoloRequested = false;
if (rawApprovalMode) {
switch (rawApprovalMode) {
case 'yolo':
approvalMode = ApprovalMode.YOLO;
approvalMode = ApprovalMode.DEFAULT;
isYoloRequested = true;
break;
case 'auto_edit':
approvalMode = ApprovalMode.AUTO_EDIT;
@@ -684,33 +687,37 @@ export async function loadCliConfig(
break;
default:
throw new Error(
`Invalid approval mode: ${rawApprovalMode}. Valid values are: yolo, auto_edit, plan, default`,
`Invalid approval mode: ${rawApprovalMode}. Valid values are: auto_edit, plan, default (yolo is mapped to allowed-tools)`,
);
}
} else {
approvalMode = ApprovalMode.DEFAULT;
}
// Override approval mode if disableYoloMode is set.
let allowedTools = argv.allowedTools || settings.tools?.allowed || [];
if (settings.security?.disableYoloMode || settings.admin?.secureModeEnabled) {
if (approvalMode === ApprovalMode.YOLO) {
if (isYoloRequested || allowedTools.includes('*')) {
if (settings.admin?.secureModeEnabled) {
debugLogger.error(
'YOLO mode is disabled by "secureModeEnabled" setting.',
'YOLO mode (wildcard policies) are disabled by "secureModeEnabled" setting.',
);
} else {
debugLogger.error(
'YOLO mode is disabled by the "disableYolo" setting.',
'YOLO mode (wildcard policies) are disabled by the "disableYolo" setting.',
);
}
throw new FatalConfigError(
getAdminErrorMessage('YOLO mode', undefined /* config */),
);
}
} else if (approvalMode === ApprovalMode.YOLO) {
} else if (isYoloRequested) {
debugLogger.warn(
'YOLO mode is enabled. All tool calls will be automatically approved.',
'YOLO mode is enabled via flag or setting. All tool calls will be automatically approved by a wildcard policy.',
);
if (!allowedTools.includes('*')) {
allowedTools = [...allowedTools, '*'];
}
}
// Force approval mode to default if the folder is not trusted.
@@ -746,10 +753,7 @@ export async function loadCliConfig(
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
!argv.isCommand);
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
const isAcpMode = !!argv.acp || !!argv.experimentalAcp;
// In non-interactive mode, exclude tools that require a prompt.
const extraExcludes: string[] = [];
if (!interactive || isAcpMode) {
+2 -25
View File
@@ -438,7 +438,7 @@ priority = 100
);
});
it('should ignore ALLOW rules and YOLO mode from extension policies for security', async () => {
it('should ignore ALLOW rules from extension policies for security', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const extDir = createExtension({
extensionsDir: userExtensionsDir,
@@ -454,20 +454,6 @@ priority = 100
toolName = "allow_tool"
decision = "allow"
priority = 100
[[rule]]
toolName = "yolo_tool"
decision = "ask_user"
priority = 100
modes = ["yolo"]
[[safety_checker]]
toolName = "yolo_check"
priority = 100
modes = ["yolo"]
[safety_checker.checker]
type = "external"
name = "yolo-checker"
`;
fs.writeFileSync(
path.join(policiesDir, 'policies.toml'),
@@ -478,24 +464,15 @@ name = "yolo-checker"
expect(extensions).toHaveLength(1);
const extension = extensions[0];
// ALLOW rules and YOLO rules/checkers should be filtered out
// ALLOW rules should be filtered out
expect(extension.rules).toBeDefined();
expect(extension.rules).toHaveLength(0);
expect(extension.checkers).toBeDefined();
expect(extension.checkers).toHaveLength(0);
// Should have logged warnings
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('attempted to contribute an ALLOW rule'),
);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('attempted to contribute a rule for YOLO mode'),
);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'attempted to contribute a safety checker for YOLO mode',
),
);
consoleSpy.mockRestore();
});
@@ -274,20 +274,21 @@ describe('Policy Engine Integration Tests', () => {
).toBe(PolicyDecision.ASK_USER);
});
it('should handle YOLO mode correctly', async () => {
it('should handle wildcard policy (YOLO mode) correctly', async () => {
const settings: Settings = {
tools: {
exclude: ['dangerous-tool'], // Even in YOLO, excludes should be respected
allowed: ['*'],
exclude: ['dangerous-tool'], // Even in wildcard, excludes should be respected
},
};
const config = await createPolicyEngineConfig(
settings,
ApprovalMode.YOLO,
ApprovalMode.DEFAULT,
);
const engine = new PolicyEngine(config);
// Most tools should be allowed in YOLO mode
// Most tools should be allowed in wildcard mode
expect(
(await engine.check({ name: 'run_shell_command' }, undefined)).decision,
).toBe(PolicyDecision.ALLOW);
@@ -225,7 +225,9 @@ describe('ShellProcessor', () => {
decision: PolicyDecision.ALLOW,
});
// Override the approval mode for this test (though PolicyEngine mock handles the decision)
(mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO);
(mockConfig.getApprovalMode as Mock).mockReturnValue(
ApprovalMode.AUTO_EDIT,
);
mockShellExecute.mockReturnValue({
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'deleted' }),
});
@@ -253,7 +255,9 @@ describe('ShellProcessor', () => {
decision: PolicyDecision.DENY,
});
// Set approval mode to YOLO
(mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO);
(mockConfig.getApprovalMode as Mock).mockReturnValue(
ApprovalMode.AUTO_EDIT,
);
await expect(processor.process(prompt, context)).rejects.toThrow(
/Blocked command: "reboot". Reason: Blocked by policy/,
@@ -113,9 +113,6 @@ describe('policiesCommand', () => {
expect(content).toContain(
'### Auto Edit Mode Policies (combined with normal mode policies)',
);
expect(content).toContain(
'### Yolo Mode Policies (combined with normal mode policies)',
);
expect(content).toContain(
'### Plan Mode Policies (combined with normal mode policies)',
);
@@ -11,7 +11,7 @@ import { MessageType } from '../types.js';
interface CategorizedRules {
normal: PolicyRule[];
autoEdit: PolicyRule[];
yolo: PolicyRule[];
plan: PolicyRule[];
}
@@ -21,7 +21,7 @@ const categorizeRulesByMode = (
const result: CategorizedRules = {
normal: [],
autoEdit: [],
yolo: [],
plan: [],
};
const ALL_MODES = Object.values(ApprovalMode);
@@ -30,7 +30,7 @@ const categorizeRulesByMode = (
const modeSet = new Set(modes);
if (modeSet.has(ApprovalMode.DEFAULT)) result.normal.push(rule);
if (modeSet.has(ApprovalMode.AUTO_EDIT)) result.autoEdit.push(rule);
if (modeSet.has(ApprovalMode.YOLO)) result.yolo.push(rule);
if (modeSet.has(ApprovalMode.PLAN)) result.plan.push(rule);
});
return result;
@@ -83,9 +83,6 @@ const listPoliciesCommand: SlashCommand = {
const uniqueAutoEdit = categorized.autoEdit.filter(
(rule) => !normalRulesSet.has(rule),
);
const uniqueYolo = categorized.yolo.filter(
(rule) => !normalRulesSet.has(rule),
);
const uniquePlan = categorized.plan.filter(
(rule) => !normalRulesSet.has(rule),
);
@@ -96,10 +93,6 @@ const listPoliciesCommand: SlashCommand = {
'Auto Edit Mode Policies (combined with normal mode policies)',
uniqueAutoEdit,
);
content += formatSection(
'Yolo Mode Policies (combined with normal mode policies)',
uniqueYolo,
);
content += formatSection(
'Plan Mode Policies (combined with normal mode policies)',
uniquePlan,
@@ -35,8 +35,11 @@ describe('ApprovalModeIndicator', () => {
});
it('renders correctly for YOLO mode', async () => {
const { lastFrame } = await render(
<ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
const { lastFrame, waitUntilReady } = await render(
<ApprovalModeIndicator
approvalMode={ApprovalMode.DEFAULT}
isYoloMode={true}
/>,
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -14,43 +14,45 @@ import { Command } from '../key/keyBindings.js';
interface ApprovalModeIndicatorProps {
approvalMode: ApprovalMode;
allowPlanMode?: boolean;
isYoloMode?: boolean;
}
export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
approvalMode,
allowPlanMode,
isYoloMode,
}) => {
let textColor = '';
let textContent = '';
let subText = '';
const cycleHint = formatCommand(Command.CYCLE_APPROVAL_MODE);
const yoloHint = formatCommand(Command.TOGGLE_YOLO);
switch (approvalMode) {
case ApprovalMode.AUTO_EDIT:
textColor = theme.status.warning;
textContent = 'auto-accept edits';
subText = allowPlanMode
? `${cycleHint} to plan`
: `${cycleHint} to manual`;
break;
case ApprovalMode.PLAN:
textColor = theme.status.success;
textContent = 'plan';
subText = `${cycleHint} to manual`;
break;
case ApprovalMode.YOLO:
textColor = theme.status.error;
textContent = 'YOLO';
subText = yoloHint;
break;
case ApprovalMode.DEFAULT:
default:
textColor = theme.text.accent;
textContent = '';
subText = `${cycleHint} to accept edits`;
break;
if (isYoloMode) {
textColor = theme.status.error;
textContent = 'YOLO';
subText = '';
} else {
switch (approvalMode) {
case ApprovalMode.AUTO_EDIT:
textColor = theme.status.warning;
textContent = 'auto-accept edits';
subText = allowPlanMode
? `${cycleHint} to plan`
: `${cycleHint} to manual`;
break;
case ApprovalMode.PLAN:
textColor = theme.status.success;
textContent = 'plan';
subText = `${cycleHint} to manual`;
break;
case ApprovalMode.DEFAULT:
default:
textColor = theme.text.accent;
textContent = '';
subText = `${cycleHint} to accept edits`;
break;
}
}
return (
@@ -231,6 +231,7 @@ const createMockConfig = (overrides = {}): Config =>
getAccessibility: vi.fn(() => ({})),
getMcpServers: vi.fn(() => ({})),
isPlanEnabled: vi.fn(() => true),
getAllowedTools: vi.fn(() => []),
getToolRegistry: () => ({
getTool: vi.fn(),
}),
@@ -625,7 +626,6 @@ describe('Composer', () => {
[ApprovalMode.DEFAULT],
[ApprovalMode.AUTO_EDIT],
[ApprovalMode.PLAN],
[ApprovalMode.YOLO],
])(
'shows ApprovalModeIndicator when approval mode is %s and shell mode is inactive',
async (mode) => {
@@ -640,6 +640,20 @@ describe('Composer', () => {
},
);
it('shows ApprovalModeIndicator when YOLO mode is active and shell mode is inactive', async () => {
const config = createMockConfig({
getAllowedTools: vi.fn(() => ['*']),
});
const uiState = createMockUIState({
showApprovalModeIndicator: ApprovalMode.DEFAULT,
shellModeActive: false,
});
const { lastFrame } = await renderComposer(uiState, undefined, config);
expect(lastFrame()).toMatch(/ApprovalModeIndic[\s\S]*ator/);
});
it('shows ShellModeIndicator when shell mode is active', async () => {
const uiState = createMockUIState({
shellModeActive: true,
@@ -671,7 +685,6 @@ describe('Composer', () => {
});
it.each([
{ mode: ApprovalMode.YOLO, label: '● YOLO' },
{ mode: ApprovalMode.PLAN, label: '● plan' },
{
mode: ApprovalMode.AUTO_EDIT,
@@ -690,6 +703,19 @@ describe('Composer', () => {
},
);
it('shows minimal mode badge "YOLO" when clean UI details are hidden and YOLO mode is active', async () => {
const config = createMockConfig({
getAllowedTools: vi.fn(() => ['*']),
});
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
showApprovalModeIndicator: ApprovalMode.DEFAULT,
});
const { lastFrame } = await renderComposer(uiState, undefined, config);
expect(lastFrame()).toContain('YOLO');
});
it('hides minimal mode badge while loading in clean mode', async () => {
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
@@ -983,7 +1009,7 @@ describe('Composer', () => {
const uiState = createMockUIState({
cleanUiDetailsVisible: true,
showApprovalModeIndicator: ApprovalMode.YOLO,
showApprovalModeIndicator: ApprovalMode.AUTO_EDIT,
});
const { lastFrame } = await renderComposer(uiState);
@@ -77,6 +77,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const hasToast = shouldShowToast(uiState);
const hideUiDetailsForSuggestions =
suggestionsVisible && suggestionsPosition === 'above';
const isYoloMode = config.getAllowedTools()?.includes('*');
// Mini Mode VIP Flags (Pure Content Triggers)
const showMinimalToast = hasToast;
@@ -147,6 +148,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
shellModeActive={uiState.shellModeActive}
setShellModeActive={uiActions.setShellModeActive}
approvalMode={uiState.showApprovalModeIndicator}
isYoloMode={isYoloMode}
onEscapePromptChange={uiActions.onEscapePromptChange}
focus={isFocused}
vimHandleInput={uiActions.vimHandleInput}
-6
View File
@@ -153,12 +153,6 @@ export const Help: React.FC<Help> = ({ commands }) => (
</Text>{' '}
- Open input in external editor
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
{formatCommand(Command.TOGGLE_YOLO)}
</Text>{' '}
- Toggle YOLO mode
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
{formatCommand(Command.SUBMIT)}
@@ -4033,15 +4033,7 @@ describe('InputPrompt', () => {
unmount();
});
it('should render correctly in yolo mode', async () => {
props.approvalMode = ApprovalMode.YOLO;
const { stdout, unmount } = await renderWithProviders(
<InputPrompt {...props} />,
);
await waitFor(() => expect(stdout.lastFrame()).toContain('*'));
expect(stdout.lastFrame()).toMatchSnapshot();
unmount();
});
it('should not show inverted cursor when shell is focused', async () => {
props.isEmbeddedShellFocused = true;
props.focus = false;
@@ -110,6 +110,7 @@ export interface InputPromptProps {
shellModeActive: boolean;
setShellModeActive: (value: boolean) => void;
approvalMode: ApprovalMode;
isYoloMode?: boolean;
onEscapePromptChange?: (showPrompt: boolean) => void;
onSuggestionsVisibilityChange?: (visible: boolean) => void;
vimHandleInput?: (key: Key) => boolean;
@@ -205,6 +206,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
shellModeActive,
setShellModeActive,
approvalMode,
isYoloMode,
onEscapePromptChange,
onSuggestionsVisibilityChange,
vimHandleInput,
@@ -1489,8 +1491,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const showAutoAcceptStyling =
!shellModeActive && approvalMode === ApprovalMode.AUTO_EDIT;
const showYoloStyling =
!shellModeActive && approvalMode === ApprovalMode.YOLO;
const showYoloStyling = !shellModeActive && isYoloMode;
const showPlanStyling =
!shellModeActive && approvalMode === ApprovalMode.PLAN;
@@ -23,7 +23,6 @@ const buildShortcutItems = (): ShortcutItem[] => [
{ key: '@', description: 'select file or folder' },
{ key: 'Double Esc', description: 'clear & rewind' },
{ key: formatCommand(Command.FOCUS_SHELL_INPUT), description: 'focus UI' },
{ key: formatCommand(Command.TOGGLE_YOLO), description: 'YOLO mode' },
{
key: formatCommand(Command.CYCLE_APPROVAL_MODE),
description: 'cycle mode',
@@ -64,16 +63,14 @@ export const ShortcutsHelp: React.FC = () => {
const itemsForDisplay = isNarrow
? items
: [
// Keep first column stable: !, @, Esc Esc, Tab Tab.
items[0],
items[5],
items[6],
items[1],
items[4],
items[7],
items[5],
items[1],
items[6],
items[2],
items[7],
items[8],
items[9],
items[3],
];
@@ -26,6 +26,6 @@ exports[`ApprovalModeIndicator > renders correctly for PLAN mode 1`] = `
`;
exports[`ApprovalModeIndicator > renders correctly for YOLO mode 1`] = `
"YOLO Ctrl+Y
"YOLO
"
`;
@@ -189,13 +189,6 @@ exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = `
"
`;
exports[`InputPrompt > snapshots > should render correctly in yolo mode 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
* Type your message or @path/to/file
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > snapshots > should render correctly when accepting edits 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Type your message or @path/to/file
@@ -7,7 +7,6 @@ exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'linux' 1`] = `
@ select file or folder
Double Esc clear & rewind
Tab focus UI
Ctrl+Y YOLO mode
Shift+Tab cycle mode
Ctrl+V paste images
Alt+M raw markdown mode
@@ -23,7 +22,6 @@ exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'mac' 1`] = `
@ select file or folder
Double Esc clear & rewind
Tab focus UI
Ctrl+Y YOLO mode
Shift+Tab cycle mode
Ctrl+V paste images
Option+M raw markdown mode
@@ -36,9 +34,8 @@ exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'linux' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
Shortcuts See /help for more
! shell mode Shift+Tab cycle mode Ctrl+V paste images
@ select file or folder Ctrl+Y YOLO mode Alt+M raw markdown mode
Double Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
Tab focus UI
@ select file or folder Alt+M raw markdown mode Double Esc clear & rewind
Ctrl+R reverse-search history Ctrl+X open external editor Tab focus UI
"
`;
@@ -46,8 +43,7 @@ exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'mac' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
Shortcuts See /help for more
! shell mode Shift+Tab cycle mode Ctrl+V paste images
@ select file or folder Ctrl+Y YOLO mode Option+M raw markdown mode
Double Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
Tab focus UI
@ select file or folder Option+M raw markdown mode Double Esc clear & rewind
Ctrl+R reverse-search history Ctrl+X open external editor Tab focus UI
"
`;
@@ -5,11 +5,7 @@
*/
import { useState, useEffect } from 'react';
import {
ApprovalMode,
type Config,
getAdminErrorMessage,
} from '@google/gemini-cli-core';
import { ApprovalMode, type Config } from '@google/gemini-cli-core';
import { useKeypress } from './useKeypress.js';
import { Command } from '../key/keyMatchers.js';
import { useKeyMatchers } from './useKeyMatchers.js';
@@ -42,36 +38,7 @@ export function useApprovalModeIndicator({
(key) => {
let nextApprovalMode: ApprovalMode | undefined;
if (keyMatchers[Command.TOGGLE_YOLO](key)) {
if (
config.isYoloModeDisabled() &&
config.getApprovalMode() !== ApprovalMode.YOLO
) {
if (addItem) {
let text =
'You cannot enter YOLO mode since it is disabled in your settings.';
const adminSettings = config.getRemoteAdminSettings();
const hasSettings =
adminSettings && Object.keys(adminSettings).length > 0;
if (hasSettings && !adminSettings.strictModeDisabled) {
text = getAdminErrorMessage('YOLO mode', config);
}
addItem(
{
type: MessageType.WARNING,
text,
},
Date.now(),
);
}
return;
}
nextApprovalMode =
config.getApprovalMode() === ApprovalMode.YOLO
? ApprovalMode.DEFAULT
: ApprovalMode.YOLO;
} else if (keyMatchers[Command.CYCLE_APPROVAL_MODE](key)) {
if (keyMatchers[Command.CYCLE_APPROVAL_MODE](key)) {
const currentMode = config.getApprovalMode();
switch (currentMode) {
case ApprovalMode.DEFAULT:
@@ -85,9 +52,7 @@ export function useApprovalModeIndicator({
case ApprovalMode.PLAN:
nextApprovalMode = ApprovalMode.DEFAULT;
break;
case ApprovalMode.YOLO:
nextApprovalMode = ApprovalMode.AUTO_EDIT;
break;
default:
}
}
@@ -43,8 +43,6 @@ import {
AuthType,
GeminiEventType as ServerGeminiEventType,
ToolErrorType,
ToolConfirmationOutcome,
MessageBusType,
tokenLimit,
debugLogger,
coreEvents,
@@ -2325,34 +2323,6 @@ describe('useGeminiStream', () => {
});
describe('handleApprovalModeChange', () => {
it('should auto-approve all pending tool calls when switching to YOLO mode', async () => {
const awaitingApprovalToolCalls: TrackedToolCall[] = [
createMockToolCall('replace', 'call1', 'edit'),
createMockToolCall('read_file', 'call2', 'info'),
];
const { result } = await renderTestHook(awaitingApprovalToolCalls);
await act(async () => {
await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
});
// Both tool calls should be auto-approved
expect(mockMessageBus.publish).toHaveBeenCalledTimes(2);
expect(mockMessageBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId: 'corr-call1',
outcome: ToolConfirmationOutcome.ProceedOnce,
}),
);
expect(mockMessageBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
correlationId: 'corr-call2',
outcome: ToolConfirmationOutcome.ProceedOnce,
}),
);
});
it('should only auto-approve edit tools when switching to AUTO_EDIT mode', async () => {
const awaitingApprovalToolCalls: TrackedToolCall[] = [
@@ -2410,7 +2380,7 @@ describe('useGeminiStream', () => {
const { result } = await renderTestHook(awaitingApprovalToolCalls);
await act(async () => {
await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
await result.current.handleApprovalModeChange(ApprovalMode.AUTO_EDIT);
});
// Both should be attempted despite first error
@@ -2453,7 +2423,7 @@ describe('useGeminiStream', () => {
// Should not throw an error
await act(async () => {
await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
await result.current.handleApprovalModeChange(ApprovalMode.AUTO_EDIT);
});
});
@@ -2495,7 +2465,7 @@ describe('useGeminiStream', () => {
const { result } = await renderTestHook(mixedStatusToolCalls);
await act(async () => {
await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
await result.current.handleApprovalModeChange(ApprovalMode.AUTO_EDIT);
});
// Only the awaiting_approval tool should be processed.
+1 -27
View File
@@ -15,8 +15,6 @@ import {
UnauthorizedError,
UserPromptEvent,
DEFAULT_GEMINI_FLASH_MODEL,
logConversationFinishedEvent,
ConversationFinishedEvent,
ApprovalMode,
parseAndFormatApiError,
ToolConfirmationOutcome,
@@ -753,27 +751,6 @@ export const useGeminiStream = (
prevActiveShellPtyIdRef.current = activeShellPtyId;
}, [activeShellPtyId, addItem, setIsResponding]);
useEffect(() => {
if (
config.getApprovalMode() === ApprovalMode.YOLO &&
streamingState === StreamingState.Idle
) {
const lastUserMessageIndex = history.findLastIndex(
(item: HistoryItem) => item.type === MessageType.USER,
);
const turnCount =
lastUserMessageIndex === -1 ? 0 : history.length - lastUserMessageIndex;
if (turnCount > 0) {
logConversationFinishedEvent(
config,
new ConversationFinishedEvent(config.getApprovalMode(), turnCount),
);
}
}
}, [streamingState, config, history]);
useEffect(() => {
if (!isResponding) {
setRetryStatus(null);
@@ -1801,10 +1778,7 @@ export const useGeminiStream = (
previousApprovalModeRef.current = newApprovalMode;
// Auto-approve pending tool calls when switching to auto-approval modes
if (
newApprovalMode === ApprovalMode.YOLO ||
newApprovalMode === ApprovalMode.AUTO_EDIT
) {
if (newApprovalMode === ApprovalMode.AUTO_EDIT) {
let awaitingApprovalCalls = toolCalls.filter(
(call): call is TrackedWaitingToolCall =>
call.status === 'awaiting_approval' && !call.request.forcedAsk,
+1 -4
View File
@@ -85,7 +85,6 @@ export enum Command {
SHOW_IDE_CONTEXT_DETAIL = 'app.showIdeContextDetail',
TOGGLE_MARKDOWN = 'app.toggleMarkdown',
TOGGLE_COPY_MODE = 'app.toggleCopyMode',
TOGGLE_YOLO = 'app.toggleYolo',
CYCLE_APPROVAL_MODE = 'app.cycleApprovalMode',
SHOW_MORE_LINES = 'app.showMoreLines',
EXPAND_PASTE = 'app.expandPaste',
@@ -386,7 +385,6 @@ export const defaultKeyBindingConfig: KeyBindingConfig = new Map([
[Command.SHOW_IDE_CONTEXT_DETAIL, [new KeyBinding('ctrl+g')]],
[Command.TOGGLE_MARKDOWN, [new KeyBinding('alt+m')]],
[Command.TOGGLE_COPY_MODE, [new KeyBinding('ctrl+s')]],
[Command.TOGGLE_YOLO, [new KeyBinding('ctrl+y')]],
[Command.CYCLE_APPROVAL_MODE, [new KeyBinding('shift+tab')]],
[Command.SHOW_MORE_LINES, [new KeyBinding('ctrl+o')]],
[Command.EXPAND_PASTE, [new KeyBinding('ctrl+o')]],
@@ -512,7 +510,6 @@ export const commandCategories: readonly CommandCategory[] = [
Command.SHOW_IDE_CONTEXT_DETAIL,
Command.TOGGLE_MARKDOWN,
Command.TOGGLE_COPY_MODE,
Command.TOGGLE_YOLO,
Command.CYCLE_APPROVAL_MODE,
Command.SHOW_MORE_LINES,
Command.EXPAND_PASTE,
@@ -621,7 +618,7 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
[Command.SHOW_IDE_CONTEXT_DETAIL]: 'Show IDE context details.',
[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.CYCLE_APPROVAL_MODE]:
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy.',
[Command.SHOW_MORE_LINES]:
+1 -5
View File
@@ -403,11 +403,7 @@ describe('keyMatchers', () => {
positive: [createKey('tab')],
negative: [createKey('f6'), createKey('f', { ctrl: true })],
},
{
command: Command.TOGGLE_YOLO,
positive: [createKey('y', { ctrl: true })],
negative: [createKey('y'), createKey('y', { alt: true })],
},
{
command: Command.CYCLE_APPROVAL_MODE,
positive: [createKey('tab', { shift: true })],
@@ -204,12 +204,7 @@ describe('handleAutoUpdate', () => {
expect(mockSpawn).not.toHaveBeenCalled();
});
it.each([
PackageManager.NPX,
PackageManager.PNPX,
PackageManager.BUNX,
PackageManager.BINARY,
])(
it.each([PackageManager.NPX, PackageManager.PNPX, PackageManager.BUNX])(
'should suppress update notifications when running via %s',
(packageManager) => {
mockGetInstallationInfo.mockReturnValue({
+3 -6
View File
@@ -87,12 +87,9 @@ export function handleAutoUpdate(
);
if (
[
PackageManager.NPX,
PackageManager.PNPX,
PackageManager.BUNX,
PackageManager.BINARY,
].includes(installationInfo.packageManager)
[PackageManager.NPX, PackageManager.PNPX, PackageManager.BUNX].includes(
installationInfo.packageManager,
)
) {
return;
}
@@ -58,19 +58,6 @@ describe('getInstallationInfo', () => {
process.argv = originalArgv;
});
it('should detect running as a standalone binary', () => {
vi.stubEnv('IS_BINARY', 'true');
process.argv[1] = '/path/to/binary';
const info = getInstallationInfo(projectRoot, true);
expect(info.packageManager).toBe(PackageManager.BINARY);
expect(info.isGlobal).toBe(true);
expect(info.updateMessage).toBe(
'Running as a standalone binary. Please update by downloading the latest version from GitHub.',
);
expect(info.updateCommand).toBeUndefined();
vi.unstubAllEnvs();
});
it('should return UNKNOWN when cliPath is not available', () => {
process.argv[1] = '';
const info = getInstallationInfo(projectRoot, true);
@@ -21,7 +21,6 @@ export enum PackageManager {
BUNX = 'bunx',
HOMEBREW = 'homebrew',
NPX = 'npx',
BINARY = 'binary',
UNKNOWN = 'unknown',
}
@@ -42,16 +41,6 @@ export function getInstallationInfo(
}
try {
// Check for standalone binary first
if (process.env['IS_BINARY'] === 'true') {
return {
packageManager: PackageManager.BINARY,
isGlobal: true,
updateMessage:
'Running as a standalone binary. Please update by downloading the latest version from GitHub.',
};
}
// Normalize path separators to forward slashes for consistent matching.
const realPath = fs.realpathSync(cliPath).replace(/\\/g, '/');
const normalizedProjectRoot = projectRoot?.replace(/\\/g, '/');