feat(plan): implement persistent approvalMode setting (#17350)

This commit is contained in:
Adib234
2026-01-23 18:14:11 -05:00
committed by GitHub
parent 00b5b2045f
commit 6fae28197e
6 changed files with 129 additions and 17 deletions

View File

@@ -90,16 +90,17 @@ they appear in the UI.
### Tools
| UI Label | Setting | Description | Default |
| -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
| Auto Accept | `tools.autoAccept` | Automatically accept and execute tool calls that are considered safe (e.g., read-only operations). | `false` |
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` |
| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Truncate tool output if it is larger than this many characters. Set to -1 to disable. | `4000000` |
| Tool Output Truncation Lines | `tools.truncateToolOutputLines` | The number of lines to keep when truncating tool output. | `1000` |
| Disable LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `true` |
| UI Label | Setting | Description | Default |
| -------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
| Auto Accept | `tools.autoAccept` | Automatically accept and execute tool calls that are considered safe (e.g., read-only operations). | `false` |
| Approval Mode | `tools.approvalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` |
| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Truncate tool output if it is larger than this many characters. Set to -1 to disable. | `4000000` |
| Tool Output Truncation Lines | `tools.truncateToolOutputLines` | The number of lines to keep when truncating tool output. | `1000` |
| Disable LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `true` |
### Security

View File

@@ -650,6 +650,13 @@ their corresponding top-level category object in your `settings.json` file.
considered safe (e.g., read-only operations).
- **Default:** `false`
- **`tools.approvalMode`** (enum):
- **Description:** The default approval mode for tool execution. 'default'
prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is
read-only mode. 'yolo' is not supported yet.
- **Default:** `"default"`
- **Values:** `"default"`, `"auto_edit"`, `"plan"`
- **`tools.core`** (array):
- **Description:** Restrict the set of built-in tools with an allowlist. Match
semantics mirror tools.allowed; see the built-in tools documentation for

View File

@@ -2233,6 +2233,19 @@ describe('loadCliConfig approval mode', () => {
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
});
it('should ignore "yolo" in settings.tools.approvalMode and fall back to DEFAULT', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
tools: {
// @ts-expect-error: testing invalid value
approvalMode: 'yolo',
},
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
});
it('should throw error when --approval-mode=plan is used but experimental.plan is disabled', async () => {
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
const argv = await parseArguments(createTestMergedSettings());
@@ -2310,6 +2323,67 @@ describe('loadCliConfig approval mode', () => {
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
});
});
describe('Persistent approvalMode setting', () => {
it('should use approvalMode from settings when no CLI flags are set', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
tools: { approvalMode: 'auto_edit' },
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(
ServerConfig.ApprovalMode.AUTO_EDIT,
);
});
it('should prioritize --approval-mode flag over settings', async () => {
process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit'];
const settings = createTestMergedSettings({
tools: { approvalMode: 'default' },
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(
ServerConfig.ApprovalMode.AUTO_EDIT,
);
});
it('should prioritize --yolo flag over settings', async () => {
process.argv = ['node', 'script.js', '--yolo'];
const settings = createTestMergedSettings({
tools: { approvalMode: 'auto_edit' },
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
});
it('should respect plan mode from settings when experimental.plan is enabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
tools: { approvalMode: 'plan' },
experimental: { plan: true },
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
});
it('should throw error if plan mode is in settings but experimental.plan is disabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
tools: { approvalMode: 'plan' },
experimental: { plan: false },
});
const argv = await parseArguments(settings);
await expect(
loadCliConfig(settings, 'test-session', argv),
).rejects.toThrow(
'Approval mode "plan" is only available when experimental.plan is enabled.',
);
});
});
});
describe('loadCliConfig fileFiltering', () => {

View File

@@ -502,9 +502,15 @@ export async function loadCliConfig(
// Determine approval mode with backward compatibility
let approvalMode: ApprovalMode;
if (argv.approvalMode) {
// New --approval-mode flag takes precedence
switch (argv.approvalMode) {
const rawApprovalMode =
argv.approvalMode ||
(argv.yolo ? 'yolo' : undefined) ||
((settings.tools?.approvalMode as string) !== 'yolo'
? settings.tools.approvalMode
: undefined);
if (rawApprovalMode) {
switch (rawApprovalMode) {
case 'yolo':
approvalMode = ApprovalMode.YOLO;
break;
@@ -524,13 +530,11 @@ export async function loadCliConfig(
break;
default:
throw new Error(
`Invalid approval mode: ${argv.approvalMode}. Valid values are: yolo, auto_edit, plan, default`,
`Invalid approval mode: ${rawApprovalMode}. Valid values are: yolo, auto_edit, plan, default`,
);
}
} else {
// Fallback to legacy --yolo flag behavior
approvalMode =
argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT;
approvalMode = ApprovalMode.DEFAULT;
}
// Override approval mode if disableYoloMode is set.

View File

@@ -1023,6 +1023,24 @@ const SETTINGS_SCHEMA = {
`,
showInDialog: true,
},
approvalMode: {
type: 'enum',
label: 'Approval Mode',
category: 'Tools',
requiresRestart: false,
default: 'default',
description: oneLine`
The default approval mode for tool execution.
'default' prompts for approval, 'auto_edit' auto-approves edit tools,
and 'plan' is read-only mode. 'yolo' is not supported yet.
`,
showInDialog: true,
options: [
{ value: 'default', label: 'Default' },
{ value: 'auto_edit', label: 'Auto Edit' },
{ value: 'plan', label: 'Plan' },
],
},
core: {
type: 'array',
label: 'Core Tools',

View File

@@ -1095,6 +1095,14 @@
"default": false,
"type": "boolean"
},
"approvalMode": {
"title": "Approval Mode",
"description": "The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet.",
"markdownDescription": "The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet.\n\n- Category: `Tools`\n- Requires restart: `no`\n- Default: `default`",
"default": "default",
"type": "string",
"enum": ["default", "auto_edit", "plan"]
},
"core": {
"title": "Core Tools",
"description": "Restrict the set of built-in tools with an allowlist. Match semantics mirror tools.allowed; see the built-in tools documentation for available names.",