mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
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:
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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, '/');
|
||||
|
||||
Reference in New Issue
Block a user