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 8db2948361
commit 9556a1d620
84 changed files with 521 additions and 1057 deletions
+1 -2
View File
@@ -355,7 +355,6 @@ describe('GeminiAgent', () => {
name: 'Auto Edit',
description: 'Auto-approves edit tools',
},
{ id: 'yolo', name: 'YOLO', description: 'Auto-approves all tools' },
],
currentModeId: 'default',
});
@@ -413,7 +412,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
@@ -1577,11 +1577,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
@@ -199,11 +199,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
@@ -1387,7 +1387,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)',
);
});
@@ -2559,7 +2559,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 () => {
@@ -2570,7 +2570,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 () => {
@@ -2603,7 +2603,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 () => {
@@ -2629,7 +2629,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 experimental.plan is enabled', async () => {
@@ -2780,7 +2780,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 experimental.plan is enabled', async () => {
+15 -10
View File
@@ -561,10 +561,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;
@@ -584,33 +587,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.
@@ -646,8 +653,6 @@ export async function loadCliConfig(
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
!argv.isCommand);
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
// In non-interactive mode, exclude tools that require a prompt.
const extraExcludes: string[] = [];
if (!interactive) {
+2 -25
View File
@@ -436,7 +436,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,
@@ -452,20 +452,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'),
@@ -476,24 +462,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);
@@ -222,7 +222,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' }),
});
@@ -250,7 +252,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/,
@@ -107,9 +107,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');
expect(content).toContain(
'**DENY** tool: `dangerousTool` [Priority: 10]',
@@ -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;
@@ -82,9 +82,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),
);
@@ -95,10 +92,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', uniquePlan);
context.ui.addItem(
@@ -39,7 +39,10 @@ describe('ApprovalModeIndicator', () => {
it('renders correctly for YOLO mode', async () => {
const { lastFrame, waitUntilReady } = render(
<ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
<ApprovalModeIndicator
approvalMode={ApprovalMode.DEFAULT}
isYoloMode={true}
/>,
);
await waitUntilReady();
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 (
@@ -232,6 +232,7 @@ const createMockConfig = (overrides = {}): Config =>
getAccessibility: vi.fn(() => ({})),
getMcpServers: vi.fn(() => ({})),
isPlanEnabled: vi.fn(() => true),
getAllowedTools: vi.fn(() => []),
getToolRegistry: () => ({
getTool: vi.fn(),
}),
@@ -621,7 +622,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) => {
@@ -636,6 +636,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,
@@ -667,7 +681,6 @@ describe('Composer', () => {
});
it.each([
[ApprovalMode.YOLO, 'YOLO'],
[ApprovalMode.PLAN, 'plan'],
[ApprovalMode.AUTO_EDIT, 'auto edit'],
])(
@@ -683,6 +696,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,
@@ -945,7 +971,7 @@ describe('Composer', () => {
const uiState = createMockUIState({
cleanUiDetailsVisible: true,
showApprovalModeIndicator: ApprovalMode.YOLO,
showApprovalModeIndicator: ApprovalMode.AUTO_EDIT,
});
const { lastFrame } = await renderComposer(uiState);
+21 -17
View File
@@ -115,24 +115,26 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const showApprovalIndicator =
!uiState.shellModeActive && !hideUiDetailsForSuggestions;
const showRawMarkdownIndicator = !uiState.renderMarkdown;
const isYoloMode = config.getAllowedTools()?.includes('*');
let modeBleedThrough: { text: string; color: string } | null = null;
switch (showApprovalModeIndicator) {
case ApprovalMode.YOLO:
modeBleedThrough = { text: 'YOLO', color: theme.status.error };
break;
case ApprovalMode.PLAN:
modeBleedThrough = { text: 'plan', color: theme.status.success };
break;
case ApprovalMode.AUTO_EDIT:
modeBleedThrough = { text: 'auto edit', color: theme.status.warning };
break;
case ApprovalMode.DEFAULT:
modeBleedThrough = null;
break;
default:
checkExhaustive(showApprovalModeIndicator);
modeBleedThrough = null;
break;
if (isYoloMode) {
modeBleedThrough = { text: 'YOLO', color: theme.status.error };
} else {
switch (showApprovalModeIndicator) {
case ApprovalMode.PLAN:
modeBleedThrough = { text: 'plan', color: theme.status.success };
break;
case ApprovalMode.AUTO_EDIT:
modeBleedThrough = { text: 'auto edit', color: theme.status.warning };
break;
case ApprovalMode.DEFAULT:
modeBleedThrough = null;
break;
default:
checkExhaustive(showApprovalModeIndicator);
modeBleedThrough = null;
break;
}
}
const hideMinimalModeHintWhileBusy =
@@ -365,6 +367,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
<ApprovalModeIndicator
approvalMode={showApprovalModeIndicator}
allowPlanMode={uiState.allowPlanMode}
isYoloMode={isYoloMode}
/>
)}
{!showLoadingIndicator && (
@@ -449,6 +452,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
shellModeActive={uiState.shellModeActive}
setShellModeActive={uiActions.setShellModeActive}
approvalMode={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)}
@@ -3836,15 +3836,6 @@ describe('InputPrompt', () => {
unmount();
});
it('should render correctly in yolo mode', async () => {
props.approvalMode = ApprovalMode.YOLO;
const { stdout, unmount } = 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;
@@ -203,6 +204,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
shellModeActive,
setShellModeActive,
approvalMode,
isYoloMode,
onEscapePromptChange,
onSuggestionsVisibilityChange,
vimHandleInput,
@@ -1455,8 +1457,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
"
`;
@@ -18,20 +18,8 @@ Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more
"
`;
exports[`ConfigInitDisplay > truncates list of waiting servers if too many 2`] = `
"
Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more
"
`;
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = `
"
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
"
`;
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 2`] = `
"
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
"
`;
@@ -92,13 +92,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
"
`;
+1 -1
View File
@@ -87,7 +87,7 @@ export const INFORMATIVE_TIPS = [
'Toggle the debug console display with F12…',
'Toggle the todo list display with Ctrl+T…',
'See full, untruncated responses with Ctrl+O…',
'Toggle auto-approval (YOLO mode) for all tools with Ctrl+Y…',
'Cycle through approval modes (Default, Auto-Edit, Plan) with Shift+Tab…',
'Toggle Markdown rendering (raw markdown mode) with Alt+M…',
'Toggle shell mode by typing ! in an empty prompt…',
@@ -162,19 +162,7 @@ describe('useApprovalModeIndicator', () => {
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
});
it('should initialize with ApprovalMode.YOLO if config.getApprovalMode returns ApprovalMode.YOLO', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
const { result } = renderHook(() =>
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: vi.fn(),
}),
);
expect(result.current).toBe(ApprovalMode.YOLO);
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(1);
});
it('should cycle the indicator and update config when Shift+Tab or Ctrl+Y is pressed', () => {
it('should cycle the indicator and update config when Shift+Tab is pressed', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const { result } = renderHook(() =>
useApprovalModeIndicator({
@@ -195,47 +183,6 @@ describe('useApprovalModeIndicator', () => {
ApprovalMode.AUTO_EDIT,
);
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
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 AUTO_EDIT (from YOLO)
act(() => {
capturedUseKeypressHandler({
name: 'tab',
shift: true,
} as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
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(() => {
capturedUseKeypressHandler({
name: 'tab',
shift: true,
} as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.AUTO_EDIT,
);
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
});
it('should not toggle if only one key or other keys combinations are pressed', () => {
@@ -326,36 +273,6 @@ describe('useApprovalModeIndicator', () => {
mockConfigInstance.isTrustedFolder.mockReturnValue(false);
});
it('should not enable YOLO mode when Ctrl+Y is pressed', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
mockConfigInstance.setApprovalMode.mockImplementation(() => {
throw new Error(
'Cannot enable privileged approval modes in an untrusted folder.',
);
});
const mockAddItem = vi.fn();
const { result } = renderHook(() =>
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
);
expect(result.current).toBe(ApprovalMode.DEFAULT);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
// We expect setApprovalMode to be called, and the error to be caught.
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.YOLO,
);
expect(mockAddItem).toHaveBeenCalled();
// Verify the underlying config value was not changed
expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should not enable AUTO_EDIT mode when Shift+Tab is pressed', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
mockConfigInstance.setApprovalMode.mockImplementation(() => {
@@ -389,26 +306,6 @@ describe('useApprovalModeIndicator', () => {
expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should disable YOLO mode when Ctrl+Y is pressed', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
const mockAddItem = vi.fn();
renderHook(() =>
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.DEFAULT,
);
expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should disable AUTO_EDIT mode when Shift+Tab is pressed', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(
ApprovalMode.AUTO_EDIT,
@@ -450,19 +347,6 @@ describe('useApprovalModeIndicator', () => {
}),
);
// Try to enable YOLO mode
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: errorMessage,
},
expect.any(Number),
);
// Try to enable AUTO_EDIT mode
act(() => {
capturedUseKeypressHandler({
@@ -479,126 +363,10 @@ describe('useApprovalModeIndicator', () => {
expect.any(Number),
);
expect(mockAddItem).toHaveBeenCalledTimes(2);
expect(mockAddItem).toHaveBeenCalledTimes(1);
});
});
describe('when YOLO mode is disabled by settings', () => {
beforeEach(() => {
// Ensure isYoloModeDisabled returns true for these tests
if (mockConfigInstance && mockConfigInstance.isYoloModeDisabled) {
mockConfigInstance.isYoloModeDisabled.mockReturnValue(true);
}
});
it('should not enable YOLO mode when Ctrl+Y is pressed and add an info message', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
mockConfigInstance.getRemoteAdminSettings.mockReturnValue({
strictModeDisabled: true,
});
const mockAddItem = vi.fn();
const { result } = renderHook(() =>
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
);
expect(result.current).toBe(ApprovalMode.DEFAULT);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
// setApprovalMode should not be called because the check should return early
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
// An info message should be added
expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.WARNING,
text: 'You cannot enter YOLO mode since it is disabled in your settings.',
},
expect.any(Number),
);
// The mode should not change
expect(result.current).toBe(ApprovalMode.DEFAULT);
});
it('should show admin error message when YOLO mode is disabled by admin', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
mockConfigInstance.getRemoteAdminSettings.mockReturnValue({
mcpEnabled: true,
});
const mockAddItem = vi.fn();
renderHook(() =>
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.WARNING,
text: '[Mock] YOLO mode is disabled',
},
expect.any(Number),
);
});
it('should show default error message when admin settings are empty', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
mockConfigInstance.getRemoteAdminSettings.mockReturnValue({});
const mockAddItem = vi.fn();
renderHook(() =>
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.WARNING,
text: 'You cannot enter YOLO mode since it is disabled in your settings.',
},
expect.any(Number),
);
});
});
it('should call onApprovalModeChange when switching to YOLO mode', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const mockOnApprovalModeChange = vi.fn();
renderHook(() =>
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange,
}),
);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.YOLO,
);
expect(mockOnApprovalModeChange).toHaveBeenCalledWith(ApprovalMode.YOLO);
});
it('should call onApprovalModeChange when switching to AUTO_EDIT mode', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
@@ -623,28 +391,6 @@ describe('useApprovalModeIndicator', () => {
);
});
it('should call onApprovalModeChange when switching to DEFAULT mode', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
const mockOnApprovalModeChange = vi.fn();
renderHook(() =>
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange,
}),
);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key); // This should toggle from YOLO to DEFAULT
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.DEFAULT,
);
expect(mockOnApprovalModeChange).toHaveBeenCalledWith(ApprovalMode.DEFAULT);
});
it('should not call onApprovalModeChange when callback is not provided', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
@@ -654,47 +400,14 @@ describe('useApprovalModeIndicator', () => {
}),
);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.YOLO,
);
// Should not throw an error when callback is not provided
});
it('should handle multiple mode changes correctly', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const mockOnApprovalModeChange = vi.fn();
renderHook(() =>
useApprovalModeIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
onApprovalModeChange: mockOnApprovalModeChange,
}),
);
// Switch to YOLO
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
// Switch to AUTO_EDIT
act(() => {
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
});
expect(mockOnApprovalModeChange).toHaveBeenCalledTimes(2);
expect(mockOnApprovalModeChange).toHaveBeenNthCalledWith(
1,
ApprovalMode.YOLO,
);
expect(mockOnApprovalModeChange).toHaveBeenNthCalledWith(
2,
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.AUTO_EDIT,
);
// Should not throw an error when callback is not provided
});
it('should cycle to PLAN when allowPlanMode is true', () => {
@@ -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:
}
}
@@ -40,8 +40,6 @@ import {
AuthType,
GeminiEventType as ServerGeminiEventType,
ToolErrorType,
ToolConfirmationOutcome,
MessageBusType,
tokenLimit,
debugLogger,
coreEvents,
@@ -2072,35 +2070,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 } = 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[] = [
createMockToolCall('replace', 'call1', 'edit'),
@@ -2157,7 +2126,7 @@ describe('useGeminiStream', () => {
const { result } = 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
@@ -2200,7 +2169,7 @@ describe('useGeminiStream', () => {
// Should not throw an error
await act(async () => {
await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
await result.current.handleApprovalModeChange(ApprovalMode.AUTO_EDIT);
});
});
@@ -2242,7 +2211,7 @@ describe('useGeminiStream', () => {
const { result } = 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,
@@ -604,27 +602,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);
@@ -1652,10 +1629,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',
+1 -4
View File
@@ -84,7 +84,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',
@@ -379,7 +378,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')]],
@@ -500,7 +498,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,
@@ -603,7 +600,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 })],
@@ -202,12 +202,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, '/');