qwen-code/packages/cli/src/config/settingsSchema.ts
DennisYu07 b5115e731e
feat(hooks): Add HTTP Hook, Function Hook and Async Hook support (#2827)
* add http/async/function type

* fix url error

* resolve comment

* align cc non blocking error

* fix hookRunner for async

* fix(hooks): update hook type validation to support http and function types

- Change validated hook types from ['command', 'plugin'] to ['command', 'http', 'function']
- Add validation for HTTP hooks requiring url field
- Add validation for function hooks requiring callback field
- Add comprehensive test coverage for all hook type validations

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(hooks): align SSRF protection with Claude Code behavior

- Allow 127.0.0.0/8 (loopback) for local dev hooks
- Allow localhost hostname for local dev hooks
- Allow ::1 (IPv6 loopback) for local dev hooks
- Add 100.64.0.0/10 (CGNAT) to blocked ranges (RFC 6598)
- Update tests to match Claude Code's ssrfGuard.ts behavior

This fixes HTTP hooks failing to connect to local dev servers.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(hooks): align HTTP hook security with Claude Code behavior

- Add CRLF/NUL sanitization for env var interpolation (header injection)
- Implement combined abort signal (external signal + timeout)
- Upgrade SSRF protection to DNS-level with ssrfGuard
  - Allow loopback (127.0.0.0/8, ::1) for local dev hooks
  - Block CGNAT (100.64.0.0/10) and IPv6 private ranges
- Increase default HTTP hook timeout to 10 minutes
- Fix VS Code hooks schema to support http type
  - Add url, headers, allowedEnvVars, async, once, statusMessage, shell fields
  - Note: "function" type is SDK-only (callback cannot be serialized to JSON)

* feat(hooks): enhance Function Hook with messages, skillRoot, shell, and matcher support

- Add MessagesProvider for automatic conversation history passing to function hooks
- Add FunctionHookContext with messages, toolUseID, and signal
- Add skillRoot support for skill-scoped session hooks
- Add shell parameter support for command hooks (bash/powershell)
- Add regex matcher support for hook pattern matching
- Add statusMessage to CommandHookConfig
- Change default function hook timeout from 60s to 5s
- Add comprehensive unit tests for all new features

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* add session hook for skill

* fix function hook parsing

* refactor ui for http hook/async hook/function hook

* update doc and add integration test

* change telemetryn type and refactor SSRF

* fix project level bug

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-16 10:10:33 +08:00

1793 lines
56 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {
MCPServerConfig,
BugCommandSettings,
TelemetrySettings,
AuthType,
ChatCompressionSettings,
ModelProvidersConfig,
} from '@qwen-code/qwen-code-core';
import {
ApprovalMode,
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
} from '@qwen-code/qwen-code-core';
import type { CustomTheme } from '../ui/themes/theme.js';
import { getLanguageSettingsOptions } from '../i18n/languages.js';
export type SettingsType =
| 'boolean'
| 'string'
| 'number'
| 'array'
| 'object'
| 'enum';
export type SettingsValue =
| boolean
| string
| number
| string[]
| object
| undefined;
/**
* Setting datatypes that "toggle" through a fixed list of options
* (e.g. an enum or true/false) rather than allowing for free form input
* (like a number or string).
*/
export const TOGGLE_TYPES: ReadonlySet<SettingsType | undefined> = new Set([
'boolean',
'enum',
]);
export interface SettingEnumOption {
value: string | number;
label: string;
}
export enum MergeStrategy {
// Replace the old value with the new value. This is the default.
REPLACE = 'replace',
// Concatenate arrays.
CONCAT = 'concat',
// Merge arrays, ensuring unique values.
UNION = 'union',
// Shallow merge objects.
SHALLOW_MERGE = 'shallow_merge',
}
export interface SettingDefinition {
type: SettingsType;
label: string;
category: string;
requiresRestart: boolean;
default: SettingsValue;
description?: string;
parentKey?: string;
key?: string;
properties?: SettingsSchema;
showInDialog?: boolean;
mergeStrategy?: MergeStrategy;
/** Enum type options */
options?: readonly SettingEnumOption[];
/** Schema for array items when type is 'array' */
items?: SettingItemDefinition;
}
/**
* Schema definition for array item types.
* Supports simple types (string, number, boolean) and complex object types.
*/
export interface SettingItemDefinition {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
properties?: Record<
string,
SettingItemDefinition & {
required?: boolean;
enum?: string[];
additionalProperties?: SettingItemDefinition;
}
>;
items?: SettingItemDefinition;
required?: boolean;
enum?: string[];
description?: string;
additionalProperties?: boolean | SettingItemDefinition;
}
export interface SettingsSchema {
[key: string]: SettingDefinition;
}
/**
* Common items schema for hook definitions.
* Used by all hook event types in the hooks configuration.
*/
const HOOK_DEFINITION_ITEMS: SettingItemDefinition = {
type: 'object',
description:
'A hook definition with an optional matcher and a list of hook configurations.',
properties: {
matcher: {
type: 'string',
description:
'An optional matcher pattern to filter when this hook definition applies.',
},
sequential: {
type: 'boolean',
description:
'Whether the hooks should be executed sequentially instead of in parallel.',
},
hooks: {
type: 'array',
description: 'The list of hook configurations to execute.',
required: true,
items: {
type: 'object',
description:
'A hook configuration entry that defines a hook to execute.',
properties: {
type: {
type: 'string',
description:
'The type of hook. Note: "function" type is only available via SDK registration, not settings.json.',
enum: ['command', 'http'],
required: true,
},
command: {
type: 'string',
description:
'The command to execute when the hook is triggered. Required for "command" type.',
},
url: {
type: 'string',
description:
'The URL to send the POST request to. Required for "http" type.',
},
headers: {
type: 'object',
description:
'HTTP headers to include in the request. Supports env var interpolation ($VAR, ${VAR}).',
additionalProperties: { type: 'string' },
},
allowedEnvVars: {
type: 'array',
description:
'List of environment variables allowed for interpolation in headers and URL.',
items: { type: 'string' },
},
name: {
type: 'string',
description: 'An optional name for the hook.',
},
description: {
type: 'string',
description: 'An optional description of what the hook does.',
},
timeout: {
type: 'number',
description: 'Timeout in seconds for the hook execution.',
},
env: {
type: 'object',
description:
'Environment variables to set when executing the hook command.',
additionalProperties: { type: 'string' },
},
async: {
type: 'boolean',
description:
'Whether to execute the hook asynchronously (non-blocking, for "command" type only).',
},
once: {
type: 'boolean',
description:
'Whether to execute the hook only once per session (for "http" type).',
},
statusMessage: {
type: 'string',
description: 'A message to display while the hook is executing.',
},
shell: {
type: 'string',
description: 'The shell to use for command execution.',
enum: ['bash', 'powershell'],
},
},
},
},
},
};
export type MemoryImportFormat = 'tree' | 'flat';
export type DnsResolutionOrder = 'ipv4first' | 'verbatim';
/**
* The canonical schema for all settings.
* The structure of this object defines the structure of the `Settings` type.
* `as const` is crucial for TypeScript to infer the most specific types possible.
*/
const SETTINGS_SCHEMA = {
// Maintained for compatibility/criticality
mcpServers: {
type: 'object',
label: 'MCP Servers',
category: 'Advanced',
requiresRestart: true,
default: {} as Record<string, MCPServerConfig>,
description: 'Configuration for MCP servers.',
showInDialog: false,
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
},
// Channels configuration (Telegram, Discord, etc.)
channels: {
type: 'object',
label: 'Channels',
category: 'Advanced',
requiresRestart: true,
default: {} as Record<string, Record<string, unknown>>,
description: 'Configuration for messaging channels.',
showInDialog: false,
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
},
// Model providers configuration grouped by authType
modelProviders: {
type: 'object',
label: 'Model Providers',
category: 'Model',
requiresRestart: false,
default: {} as ModelProvidersConfig,
description:
'Model providers configuration grouped by authType. Each authType contains an array of model configurations.',
showInDialog: false,
mergeStrategy: MergeStrategy.REPLACE,
},
// Coding Plan configuration
codingPlan: {
type: 'object',
label: 'Coding Plan',
category: 'Model',
requiresRestart: false,
default: {},
description: 'Coding Plan template version tracking and configuration.',
showInDialog: false,
properties: {
version: {
type: 'string',
label: 'Coding Plan Template Version',
category: 'Model',
requiresRestart: false,
default: undefined as string | undefined,
description:
'SHA256 hash of the Coding Plan template. Used to detect template updates.',
showInDialog: false,
},
},
},
// Environment variables fallback
env: {
type: 'object',
label: 'Environment Variables',
category: 'Advanced',
requiresRestart: true,
default: {} as Record<string, string>,
description:
'Environment variables to set as fallback defaults. These are loaded with the lowest priority: system environment variables > .env files > settings.json env field.',
showInDialog: false,
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
},
general: {
type: 'object',
label: 'General',
category: 'General',
requiresRestart: false,
default: {},
description: 'General application settings.',
showInDialog: false,
properties: {
preferredEditor: {
type: 'string',
label: 'Preferred Editor',
category: 'General',
requiresRestart: false,
default: undefined as string | undefined,
description: 'The preferred editor to open files in.',
showInDialog: true,
},
vimMode: {
type: 'boolean',
label: 'Vim Mode',
category: 'General',
requiresRestart: false,
default: false,
description: 'Enable Vim keybindings',
showInDialog: true,
},
enableAutoUpdate: {
type: 'boolean',
label: 'Enable Auto Update',
category: 'General',
requiresRestart: false,
default: true,
description:
'Enable automatic update checks and installations on startup.',
showInDialog: true,
},
gitCoAuthor: {
type: 'boolean',
label: 'Attribution: commit',
category: 'General',
requiresRestart: false,
default: true,
description:
'Automatically add a Co-authored-by trailer to git commit messages when commits are made through Qwen Code.',
showInDialog: true,
},
checkpointing: {
type: 'object',
label: 'Checkpointing',
category: 'General',
requiresRestart: true,
default: {},
description: 'Session checkpointing settings.',
showInDialog: false,
properties: {
enabled: {
type: 'boolean',
label: 'Enable Checkpointing',
category: 'General',
requiresRestart: true,
default: false,
description: 'Enable session checkpointing for recovery',
showInDialog: false,
},
},
},
debugKeystrokeLogging: {
type: 'boolean',
label: 'Debug Keystroke Logging',
category: 'General',
requiresRestart: false,
default: false,
description: 'Enable debug logging of keystrokes to the console.',
showInDialog: false,
},
language: {
type: 'enum',
label: 'Language: UI',
category: 'General',
requiresRestart: true,
default: 'auto',
description:
'The language for the user interface. Use "auto" to detect from system settings. ' +
'You can also use custom language codes (e.g., "es", "fr") by placing JS language files ' +
'in ~/.qwen/locales/ (e.g., ~/.qwen/locales/es.js).',
showInDialog: true,
options: [] as readonly SettingEnumOption[],
},
outputLanguage: {
type: 'string',
label: 'Language: Model',
category: 'General',
requiresRestart: true,
default: 'auto',
description:
'The language for LLM output. Use "auto" to detect from system settings, ' +
'or set a specific language.',
showInDialog: true,
},
terminalBell: {
type: 'boolean',
label: 'Terminal Bell Notification',
category: 'General',
requiresRestart: false,
default: true,
description:
'Play terminal bell sound when response completes or needs approval.',
showInDialog: true,
},
chatRecording: {
type: 'boolean',
label: 'Chat Recording',
category: 'General',
requiresRestart: true,
default: true,
description:
'Enable saving chat history to disk. Disabling this will also prevent --continue and --resume from working.',
showInDialog: false,
},
defaultFileEncoding: {
type: 'enum',
label: 'Default File Encoding',
category: 'General',
requiresRestart: false,
default: 'utf-8',
description:
'Default encoding for new files. Use "utf-8" (default) for UTF-8 without BOM, or "utf-8-bom" for UTF-8 with BOM. Only change this if your project specifically requires BOM.',
showInDialog: false,
options: [
{ value: 'utf-8', label: 'UTF-8 (without BOM)' },
{ value: 'utf-8-bom', label: 'UTF-8 with BOM' },
],
},
},
},
output: {
type: 'object',
label: 'Output',
category: 'General',
requiresRestart: false,
default: {},
description: 'Settings for the CLI output.',
showInDialog: false,
properties: {
format: {
type: 'enum',
label: 'Output Format',
category: 'General',
requiresRestart: false,
default: 'text',
description: 'The format of the CLI output.',
showInDialog: false,
options: [
{ value: 'text', label: 'Text' },
{ value: 'json', label: 'JSON' },
],
},
},
},
ui: {
type: 'object',
label: 'UI',
category: 'UI',
requiresRestart: false,
default: {},
description: 'User interface settings.',
showInDialog: false,
properties: {
theme: {
type: 'string',
label: 'Theme',
category: 'UI',
requiresRestart: false,
default: 'Qwen Dark' as string,
description: 'The color theme for the UI.',
showInDialog: true,
},
statusLine: {
type: 'object',
label: 'Status Line',
category: 'UI',
requiresRestart: false,
default: undefined as { type: 'command'; command: string } | undefined,
description: 'Custom status line display configuration.',
showInDialog: false,
},
customThemes: {
type: 'object',
label: 'Custom Themes',
category: 'UI',
requiresRestart: false,
default: {} as Record<string, CustomTheme>,
description: 'Custom theme definitions.',
showInDialog: false,
},
hideWindowTitle: {
type: 'boolean',
label: 'Hide Window Title',
category: 'UI',
requiresRestart: true,
default: false,
description: 'Hide the window title bar',
showInDialog: false,
},
showStatusInTitle: {
type: 'boolean',
label: 'Show Status in Title',
category: 'UI',
requiresRestart: false,
default: false,
description:
'Show Qwen Code status and thoughts in the terminal window title',
showInDialog: false,
},
hideTips: {
type: 'boolean',
label: 'Hide Tips',
category: 'UI',
requiresRestart: false,
default: false,
description: 'Hide helpful tips in the UI',
showInDialog: true,
},
showLineNumbers: {
type: 'boolean',
label: 'Show Line Numbers in Code',
category: 'UI',
requiresRestart: false,
default: true,
description: 'Show line numbers in the code output.',
showInDialog: true,
},
showCitations: {
type: 'boolean',
label: 'Show Citations',
category: 'UI',
requiresRestart: false,
default: false,
description: 'Show citations for generated text in the chat.',
showInDialog: false,
},
customWittyPhrases: {
type: 'array',
label: 'Custom Witty Phrases',
category: 'UI',
requiresRestart: false,
default: [] as string[],
description: 'Custom witty phrases to display during loading.',
showInDialog: false,
},
enableWelcomeBack: {
type: 'boolean',
label: 'Show Welcome Back Dialog',
category: 'UI',
requiresRestart: false,
default: true,
description:
'Show welcome back dialog when returning to a project with conversation history.',
showInDialog: true,
},
enableUserFeedback: {
type: 'boolean',
label: 'Enable User Feedback',
category: 'UI',
requiresRestart: false,
default: true,
description:
'Show optional feedback dialog after conversations to help improve Qwen performance.',
showInDialog: true,
},
enableFollowupSuggestions: {
type: 'boolean',
label: 'Enable Follow-up Suggestions',
category: 'UI',
requiresRestart: false,
default: false,
description:
'Show context-aware follow-up suggestions after task completion. Press Tab or Right Arrow to accept, Enter to accept and submit.',
showInDialog: true,
},
enableCacheSharing: {
type: 'boolean',
label: 'Enable Cache Sharing for Suggestions',
category: 'UI',
requiresRestart: false,
default: true,
description:
'Use cache-aware forked queries for suggestion generation. Reduces cost on providers that support prefix caching (experimental).',
showInDialog: false,
},
enableSpeculation: {
type: 'boolean',
label: 'Enable Speculative Execution',
category: 'UI',
requiresRestart: false,
default: false,
description:
'Speculatively execute accepted suggestions before submission. Results appear instantly when you accept (experimental).',
showInDialog: false,
},
accessibility: {
type: 'object',
label: 'Accessibility',
category: 'UI',
requiresRestart: true,
default: {},
description: 'Accessibility settings.',
showInDialog: false,
properties: {
enableLoadingPhrases: {
type: 'boolean',
label: 'Enable Loading Phrases',
category: 'UI',
requiresRestart: true,
default: true,
description: 'Enable loading phrases (disable for accessibility)',
showInDialog: true,
},
screenReader: {
type: 'boolean',
label: 'Screen Reader Mode',
category: 'UI',
requiresRestart: true,
default: undefined as boolean | undefined,
description:
'Render output in plain-text to be more screen reader accessible',
showInDialog: false,
},
},
},
feedbackLastShownTimestamp: {
type: 'number',
label: 'Feedback Last Shown Timestamp',
category: 'UI',
requiresRestart: false,
default: 0,
description: 'The last time the feedback dialog was shown.',
showInDialog: false,
},
compactMode: {
type: 'boolean',
label: 'Compact Mode',
category: 'UI',
requiresRestart: false,
default: false,
description:
'Hide tool output and thinking for a cleaner view (toggle with Ctrl+O).',
showInDialog: true,
},
},
},
ide: {
type: 'object',
label: 'IDE',
category: 'IDE',
requiresRestart: true,
default: {},
description: 'IDE integration settings.',
showInDialog: false,
properties: {
enabled: {
type: 'boolean',
label: 'Auto-connect to IDE',
category: 'IDE',
requiresRestart: true,
default: false,
description: 'Enable IDE integration mode',
showInDialog: true,
},
hasSeenNudge: {
type: 'boolean',
label: 'Has Seen IDE Integration Nudge',
category: 'IDE',
requiresRestart: false,
default: false,
description: 'Whether the user has seen the IDE integration nudge.',
showInDialog: false,
},
},
},
privacy: {
type: 'object',
label: 'Privacy',
category: 'Privacy',
requiresRestart: true,
default: {},
description: 'Privacy-related settings.',
showInDialog: false,
properties: {
usageStatisticsEnabled: {
type: 'boolean',
label: 'Enable Usage Statistics',
category: 'Privacy',
requiresRestart: true,
default: true,
description: 'Enable collection of usage statistics',
showInDialog: true,
},
},
},
telemetry: {
type: 'object',
label: 'Telemetry',
category: 'Advanced',
requiresRestart: true,
default: undefined as TelemetrySettings | undefined,
description: 'Telemetry configuration.',
showInDialog: false,
},
fastModel: {
type: 'string',
label: 'Fast Model',
category: 'Model',
requiresRestart: false,
default: '',
description:
'Model used for generating prompt suggestions and speculative execution. Leave empty to use the main model. A smaller/faster model (e.g., qwen3-coder-flash) reduces latency and cost.',
showInDialog: true,
},
model: {
type: 'object',
label: 'Model',
category: 'Model',
requiresRestart: false,
default: {},
description: 'Settings related to the generative model.',
showInDialog: false,
properties: {
name: {
type: 'string',
label: 'Model',
category: 'Model',
requiresRestart: false,
default: undefined as string | undefined,
description: 'The model to use for conversations.',
showInDialog: false,
},
maxSessionTurns: {
type: 'number',
label: 'Max Session Turns',
category: 'Model',
requiresRestart: false,
default: -1,
description:
'Maximum number of user/model/tool turns to keep in a session. -1 means unlimited.',
showInDialog: false,
},
chatCompression: {
type: 'object',
label: 'Chat Compression',
category: 'Model',
requiresRestart: false,
default: undefined as ChatCompressionSettings | undefined,
description: 'Chat compression settings.',
showInDialog: false,
},
sessionTokenLimit: {
type: 'number',
label: 'Session Token Limit',
category: 'Model',
requiresRestart: false,
default: undefined as number | undefined,
description: 'The maximum number of tokens allowed in a session.',
showInDialog: false,
},
skipNextSpeakerCheck: {
type: 'boolean',
label: 'Skip Next Speaker Check',
category: 'Model',
requiresRestart: false,
default: true,
description: 'Skip the next speaker check.',
showInDialog: false,
},
skipLoopDetection: {
type: 'boolean',
label: 'Skip Loop Detection',
category: 'Model',
requiresRestart: false,
default: true,
description: 'Disable all loop detection checks (streaming and LLM).',
showInDialog: false,
},
skipStartupContext: {
type: 'boolean',
label: 'Skip Startup Context',
category: 'Model',
requiresRestart: true,
default: false,
description:
'Avoid sending the workspace startup context at the beginning of each session.',
showInDialog: false,
},
enableOpenAILogging: {
type: 'boolean',
label: 'Enable OpenAI Logging',
category: 'Model',
requiresRestart: false,
default: false,
description: 'Enable OpenAI logging.',
showInDialog: false,
},
openAILoggingDir: {
type: 'string',
label: 'OpenAI Logging Directory',
category: 'Model',
requiresRestart: false,
default: undefined as string | undefined,
description:
'Custom directory path for OpenAI API logs. If not specified, defaults to logs/openai in the current working directory.',
showInDialog: false,
},
generationConfig: {
type: 'object',
label: 'Generation Configuration',
category: 'Model',
requiresRestart: false,
default: undefined as Record<string, unknown> | undefined,
description: 'Generation configuration settings.',
showInDialog: false,
properties: {
timeout: {
type: 'number',
label: 'Timeout',
category: 'Generation Configuration',
requiresRestart: false,
default: undefined as number | undefined,
description: 'Request timeout in milliseconds.',
parentKey: 'generationConfig',
showInDialog: false,
},
maxRetries: {
type: 'number',
label: 'Max Retries',
category: 'Generation Configuration',
requiresRestart: false,
default: undefined as number | undefined,
description: 'Maximum number of retries for failed requests.',
parentKey: 'generationConfig',
showInDialog: false,
},
enableCacheControl: {
type: 'boolean',
label: 'Enable Cache Control',
category: 'Generation Configuration',
requiresRestart: false,
default: true,
description: 'Enable cache control for DashScope providers.',
parentKey: 'generationConfig',
showInDialog: false,
},
schemaCompliance: {
type: 'enum',
label: 'Tool Schema Compliance',
category: 'Generation Configuration',
requiresRestart: false,
default: 'auto',
description:
'The compliance mode for tool schemas sent to the model. Use "openapi_30" for strict OpenAPI 3.0 compatibility (e.g., for Gemini).',
parentKey: 'generationConfig',
showInDialog: false,
options: [
{ value: 'auto', label: 'Auto (Default)' },
{ value: 'openapi_30', label: 'OpenAPI 3.0 Strict' },
],
},
contextWindowSize: {
type: 'number',
label: 'Context Window Size',
category: 'Generation Configuration',
requiresRestart: false,
default: undefined,
description:
"Overrides the default context window size for the selected model. Use this setting when a provider's effective context limit differs from Qwen Code's default. This value defines the model's assumed maximum context capacity, not a per-request token limit.",
parentKey: 'generationConfig',
showInDialog: false,
},
},
},
},
},
context: {
type: 'object',
label: 'Context',
category: 'Context',
requiresRestart: false,
default: {},
description: 'Settings for managing context provided to the model.',
showInDialog: false,
properties: {
fileName: {
type: 'object',
label: 'Context File Name',
category: 'Context',
requiresRestart: false,
default: undefined as string | string[] | undefined,
description: 'The name of the context file.',
showInDialog: false,
},
importFormat: {
type: 'string',
label: 'Memory Import Format',
category: 'Context',
requiresRestart: false,
default: undefined as MemoryImportFormat | undefined,
description: 'The format to use when importing memory.',
showInDialog: false,
},
includeDirectories: {
type: 'array',
label: 'Include Directories',
category: 'Context',
requiresRestart: false,
default: [] as string[],
description:
'Additional directories to include in the workspace context. Missing directories will be skipped with a warning.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
},
loadFromIncludeDirectories: {
type: 'boolean',
label: 'Load Memory From Include Directories',
category: 'Context',
requiresRestart: false,
default: false,
description: 'Whether to load memory files from include directories.',
showInDialog: false,
},
clearContextOnIdle: {
type: 'object',
label: 'Clear Context On Idle',
category: 'Context',
requiresRestart: false,
default: {},
description:
'Settings for clearing stale context after idle periods. Use -1 to disable a threshold.',
showInDialog: false,
properties: {
thinkingThresholdMinutes: {
type: 'number',
label: 'Thinking Idle Threshold (minutes)',
category: 'Context',
requiresRestart: false,
default: 5 as number,
description:
'Minutes of inactivity before clearing old thinking blocks. Use -1 to disable.',
showInDialog: false,
},
toolResultsThresholdMinutes: {
type: 'number',
label: 'Tool Results Idle Threshold (minutes)',
category: 'Context',
requiresRestart: false,
default: 60 as number,
description:
'Minutes of inactivity before clearing old tool result content. Use -1 to disable.',
showInDialog: false,
},
toolResultsNumToKeep: {
type: 'number',
label: 'Tool Results Number To Keep',
category: 'Context',
requiresRestart: false,
default: 5 as number,
description:
'Number of most-recent compactable tool results to preserve when clearing. Floor at 1.',
showInDialog: false,
},
},
},
fileFiltering: {
type: 'object',
label: 'File Filtering',
category: 'Context',
requiresRestart: true,
default: {},
description: 'Settings for git-aware file filtering.',
showInDialog: false,
properties: {
respectGitIgnore: {
type: 'boolean',
label: 'Respect .gitignore',
category: 'Context',
requiresRestart: true,
default: true,
description: 'Respect .gitignore files when searching',
showInDialog: true,
},
respectQwenIgnore: {
type: 'boolean',
label: 'Respect .qwenignore',
category: 'Context',
requiresRestart: true,
default: true,
description: 'Respect .qwenignore files when searching',
showInDialog: true,
},
enableRecursiveFileSearch: {
type: 'boolean',
label: 'Enable Recursive File Search',
category: 'Context',
requiresRestart: true,
default: true,
description: 'Enable recursive file search functionality',
showInDialog: false,
},
enableFuzzySearch: {
type: 'boolean',
label: 'Enable Fuzzy Search',
category: 'Context',
requiresRestart: true,
default: true,
description: 'Enable fuzzy search when searching for files.',
showInDialog: true,
},
},
},
},
},
permissions: {
type: 'object',
label: 'Permissions',
category: 'Tools',
requiresRestart: true,
default: {},
description:
'Permission rules controlling tool usage. Rules are evaluated in priority order: deny > ask > allow.',
showInDialog: false,
properties: {
allow: {
type: 'array',
label: 'Allow Rules',
category: 'Tools',
requiresRestart: true,
default: undefined as string[] | undefined,
description:
'Tools or commands that are auto-approved without confirmation. ' +
'Examples: "ShellTool", "Bash(git *)", "ReadFileTool".',
showInDialog: false,
mergeStrategy: MergeStrategy.UNION,
},
ask: {
type: 'array',
label: 'Ask Rules',
category: 'Tools',
requiresRestart: true,
default: undefined as string[] | undefined,
description:
'Tools or commands that always require user confirmation. ' +
'Takes precedence over allow rules.',
showInDialog: false,
mergeStrategy: MergeStrategy.UNION,
},
deny: {
type: 'array',
label: 'Deny Rules',
category: 'Tools',
requiresRestart: true,
default: undefined as string[] | undefined,
description:
'Tools or commands that are always blocked. Highest priority rule. ' +
'Examples: "ShellTool", "Bash(rm -rf *)".',
showInDialog: false,
mergeStrategy: MergeStrategy.UNION,
},
},
},
tools: {
type: 'object',
label: 'Tools',
category: 'Tools',
requiresRestart: true,
default: {},
description: 'Settings for built-in and custom tools.',
showInDialog: false,
properties: {
sandbox: {
type: 'object',
label: 'Sandbox',
category: 'Tools',
requiresRestart: true,
default: undefined as boolean | string | undefined,
description:
'Sandbox execution environment (can be a boolean or a path string).',
showInDialog: false,
},
sandboxImage: {
type: 'string',
label: 'Sandbox Image',
category: 'Tools',
requiresRestart: true,
default: undefined as string | undefined,
description:
'Sandbox image URI used by Docker/Podman when --sandbox-image and QWEN_SANDBOX_IMAGE are not set.',
showInDialog: false,
},
shell: {
type: 'object',
label: 'Shell',
category: 'Tools',
requiresRestart: false,
default: {},
description: 'Settings for shell execution.',
showInDialog: false,
properties: {
enableInteractiveShell: {
type: 'boolean',
label: 'Interactive Shell (PTY)',
category: 'Tools',
requiresRestart: true,
default: true,
description:
'Use node-pty for an interactive shell experience. Falls back to child_process if PTY is unavailable.',
showInDialog: true,
},
pager: {
type: 'string',
label: 'Pager',
category: 'Tools',
requiresRestart: false,
default: 'cat' as string | undefined,
description:
'The pager command to use for shell output. Defaults to `cat`.',
showInDialog: false,
},
showColor: {
type: 'boolean',
label: 'Show Color',
category: 'Tools',
requiresRestart: false,
default: false,
description: 'Show color in shell output.',
showInDialog: false,
},
},
},
// Legacy tool permission fields kept for backward compatibility.
// Use permissions.{allow,ask,deny} instead.
core: {
type: 'array',
label: 'Core Tools (deprecated)',
category: 'Tools',
requiresRestart: true,
default: undefined as string[] | undefined,
description: 'Deprecated. Use permissions.allow instead.',
showInDialog: false,
},
allowed: {
type: 'array',
label: 'Allowed Tools (deprecated)',
category: 'Advanced',
requiresRestart: true,
default: undefined as string[] | undefined,
description: 'Deprecated. Use permissions.allow instead.',
showInDialog: false,
},
exclude: {
type: 'array',
label: 'Exclude Tools (deprecated)',
category: 'Tools',
requiresRestart: true,
default: undefined as string[] | undefined,
description: 'Deprecated. Use permissions.deny instead.',
showInDialog: false,
mergeStrategy: MergeStrategy.UNION,
},
approvalMode: {
type: 'enum',
label: 'Tool Approval Mode',
category: 'Tools',
requiresRestart: false,
default: ApprovalMode.DEFAULT,
description:
'Approval mode for tool usage. Controls how tools are approved before execution.',
showInDialog: true,
options: [
{ value: ApprovalMode.PLAN, label: 'Plan' },
{ value: ApprovalMode.DEFAULT, label: 'Default' },
{ value: ApprovalMode.AUTO_EDIT, label: 'Auto Edit' },
{ value: ApprovalMode.YOLO, label: 'YOLO' },
],
},
autoAccept: {
type: 'boolean',
label: 'Auto Accept',
category: 'Tools',
requiresRestart: false,
default: false,
description:
'Automatically accept and execute tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation.',
showInDialog: false,
},
discoveryCommand: {
type: 'string',
label: 'Tool Discovery Command',
category: 'Tools',
requiresRestart: true,
default: undefined as string | undefined,
description: 'Command to run for tool discovery.',
showInDialog: false,
},
callCommand: {
type: 'string',
label: 'Tool Call Command',
category: 'Tools',
requiresRestart: true,
default: undefined as string | undefined,
description: 'Command to run for tool calls.',
showInDialog: false,
},
useRipgrep: {
type: 'boolean',
label: 'Use Ripgrep',
category: 'Tools',
requiresRestart: false,
default: true,
description:
'Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.',
showInDialog: false,
},
useBuiltinRipgrep: {
type: 'boolean',
label: 'Use Builtin Ripgrep',
category: 'Tools',
requiresRestart: false,
default: true,
description:
'Use the bundled ripgrep binary. When set to false, the system-level "rg" command will be used instead. This setting is only effective when useRipgrep is true.',
showInDialog: false,
},
truncateToolOutputThreshold: {
type: 'number',
label: 'Tool Output Truncation Threshold',
category: 'General',
requiresRestart: true,
default: DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
description:
'Truncate tool output if it is larger than this many characters. Set to -1 to disable.',
showInDialog: false,
},
truncateToolOutputLines: {
type: 'number',
label: 'Tool Output Truncation Lines',
category: 'General',
requiresRestart: true,
default: DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
description: 'The number of lines to keep when truncating tool output.',
showInDialog: false,
},
},
},
mcp: {
type: 'object',
label: 'MCP',
category: 'MCP',
requiresRestart: true,
default: {},
description: 'Settings for Model Context Protocol (MCP) servers.',
showInDialog: false,
properties: {
serverCommand: {
type: 'string',
label: 'MCP Server Command',
category: 'MCP',
requiresRestart: true,
default: undefined as string | undefined,
description: 'Command to start an MCP server.',
showInDialog: false,
},
allowed: {
type: 'array',
label: 'Allow MCP Servers',
category: 'MCP',
requiresRestart: true,
default: undefined as string[] | undefined,
description: 'A list of MCP servers to allow.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
},
excluded: {
type: 'array',
label: 'Exclude MCP Servers',
category: 'MCP',
requiresRestart: true,
default: undefined as string[] | undefined,
description: 'A list of MCP servers to exclude.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
},
},
},
security: {
type: 'object',
label: 'Security',
category: 'Security',
requiresRestart: true,
default: {},
description: 'Security-related settings.',
showInDialog: false,
properties: {
folderTrust: {
type: 'object',
label: 'Folder Trust',
category: 'Security',
requiresRestart: false,
default: {},
description: 'Settings for folder trust.',
showInDialog: false,
properties: {
enabled: {
type: 'boolean',
label: 'Folder Trust',
category: 'Security',
requiresRestart: true,
default: false,
description: 'Setting to track whether Folder trust is enabled.',
showInDialog: false,
},
},
},
auth: {
type: 'object',
label: 'Authentication',
category: 'Security',
requiresRestart: true,
default: {},
description: 'Authentication settings.',
showInDialog: false,
properties: {
selectedType: {
type: 'string',
label: 'Selected Auth Type',
category: 'Security',
requiresRestart: true,
default: undefined as AuthType | undefined,
description: 'The currently selected authentication type.',
showInDialog: false,
},
enforcedType: {
type: 'string',
label: 'Enforced Auth Type',
category: 'Advanced',
requiresRestart: true,
default: undefined as AuthType | undefined,
description:
'The required auth type. If this does not match the selected auth type, the user will be prompted to re-authenticate.',
showInDialog: false,
},
useExternal: {
type: 'boolean',
label: 'Use External Auth',
category: 'Security',
requiresRestart: true,
default: undefined as boolean | undefined,
description: 'Whether to use an external authentication flow.',
showInDialog: false,
},
apiKey: {
type: 'string',
label: 'API Key',
category: 'Security',
requiresRestart: true,
default: undefined as string | undefined,
description: 'API key for OpenAI compatible authentication.',
showInDialog: false,
},
baseUrl: {
type: 'string',
label: 'Base URL',
category: 'Security',
requiresRestart: true,
default: undefined as string | undefined,
description: 'Base URL for OpenAI compatible API.',
showInDialog: false,
},
},
},
allowedHttpHookUrls: {
type: 'array',
label: 'Allowed HTTP Hook URLs',
category: 'Security',
requiresRestart: false,
default: [] as string[],
description:
'Whitelist of URL patterns for HTTP hooks. Supports * wildcard. If empty, all URLs are allowed (subject to SSRF protection).',
showInDialog: false,
items: {
type: 'string',
description: 'URL pattern (supports * wildcard)',
},
},
},
},
advanced: {
type: 'object',
label: 'Advanced',
category: 'Advanced',
requiresRestart: true,
default: {},
description: 'Advanced settings for power users.',
showInDialog: false,
properties: {
autoConfigureMemory: {
type: 'boolean',
label: 'Auto Configure Max Old Space Size',
category: 'Advanced',
requiresRestart: true,
default: false,
description: 'Automatically configure Node.js memory limits',
showInDialog: false,
},
dnsResolutionOrder: {
type: 'string',
label: 'DNS Resolution Order',
category: 'Advanced',
requiresRestart: true,
default: undefined as DnsResolutionOrder | undefined,
description: 'The DNS resolution order.',
showInDialog: false,
},
excludedEnvVars: {
type: 'array',
label: 'Excluded Project Environment Variables',
category: 'Advanced',
requiresRestart: false,
default: ['DEBUG', 'DEBUG_MODE'] as string[],
description: 'Environment variables to exclude from project context.',
showInDialog: false,
mergeStrategy: MergeStrategy.UNION,
},
bugCommand: {
type: 'object',
label: 'Bug Command',
category: 'Advanced',
requiresRestart: false,
default: undefined as BugCommandSettings | undefined,
description: 'Configuration for the bug report command.',
showInDialog: false,
},
runtimeOutputDir: {
type: 'string',
label: 'Runtime Output Directory',
category: 'Advanced',
requiresRestart: true,
default: undefined as string | undefined,
description:
'Custom directory for runtime output (temp files, debug logs, session data, todos, etc.). ' +
'Config files remain at ~/.qwen. Env var QWEN_RUNTIME_DIR takes priority.',
showInDialog: false,
},
tavilyApiKey: {
type: 'string',
label: 'Tavily API Key (Deprecated)',
category: 'Advanced',
requiresRestart: false,
default: undefined as string | undefined,
description:
'⚠️ DEPRECATED: Please use webSearch.provider configuration instead. Legacy API key for the Tavily API.',
showInDialog: false,
},
},
},
webSearch: {
type: 'object',
label: 'Web Search',
category: 'Advanced',
requiresRestart: true,
default: undefined as
| {
provider: Array<{
type: 'tavily' | 'google' | 'dashscope';
apiKey?: string;
searchEngineId?: string;
}>;
default: string;
}
| undefined,
description: 'Configuration for web search providers.',
showInDialog: false,
},
agents: {
type: 'object',
label: 'Agents',
category: 'Advanced',
requiresRestart: false,
default: {},
description:
'Settings for multi-agent collaboration features (Arena, Team, Swarm).',
showInDialog: false,
properties: {
displayMode: {
type: 'enum',
label: 'Display Mode',
category: 'Advanced',
requiresRestart: false,
default: undefined as string | undefined,
description:
'Display mode for multi-agent sessions. Currently only "in-process" is supported.',
showInDialog: false,
options: [
{ value: 'in-process', label: 'In-process' },
// { value: 'tmux', label: 'tmux' },
// { value: 'iterm2', label: 'iTerm2' },
],
},
arena: {
type: 'object',
label: 'Arena',
category: 'Advanced',
requiresRestart: false,
default: {},
description: 'Settings for Arena (multi-model competitive execution).',
showInDialog: false,
properties: {
worktreeBaseDir: {
type: 'string',
label: 'Worktree Base Directory',
category: 'Advanced',
requiresRestart: true,
default: undefined as string | undefined,
description:
'Custom base directory for Arena worktrees. Defaults to ~/.qwen/arena.',
showInDialog: false,
},
preserveArtifacts: {
type: 'boolean',
label: 'Preserve Arena Artifacts',
category: 'Advanced',
requiresRestart: false,
default: false,
description:
'When enabled, Arena worktrees and session state files are preserved after the session ends or the main agent exits.',
showInDialog: true,
},
maxRoundsPerAgent: {
type: 'number',
label: 'Max Rounds Per Agent',
category: 'Advanced',
requiresRestart: false,
default: undefined as number | undefined,
description:
'Maximum number of rounds (turns) each agent can execute. No limit if unset.',
showInDialog: false,
},
timeoutSeconds: {
type: 'number',
label: 'Timeout (seconds)',
category: 'Advanced',
requiresRestart: false,
default: undefined as number | undefined,
description:
'Total timeout in seconds for the Arena session. No limit if unset.',
showInDialog: false,
},
},
},
team: {
type: 'object',
label: 'Team',
category: 'Advanced',
requiresRestart: false,
default: {},
description:
'Settings for Agent Team (role-based collaborative execution). Reserved for future use.',
showInDialog: false,
},
swarm: {
type: 'object',
label: 'Swarm',
category: 'Advanced',
requiresRestart: false,
default: {},
description:
'Settings for Agent Swarm (parallel sub-agent execution). Reserved for future use.',
showInDialog: false,
},
},
},
disableAllHooks: {
type: 'boolean',
label: 'Disable All Hooks',
category: 'Advanced',
requiresRestart: true, // Future enhancement: consider supporting mid-session toggle for better UX
default: false,
description:
'Temporarily disable all hooks without deleting configurations. Default is false (hooks enabled).',
showInDialog: false,
},
hooks: {
type: 'object',
label: 'Hooks',
category: 'Advanced',
requiresRestart: false,
default: {},
description:
'Hook event configurations for extending CLI behavior at various lifecycle points.',
showInDialog: false,
properties: {
UserPromptSubmit: {
type: 'array',
label: 'Before Agent Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute before agent processing. Can modify prompts or inject context.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
Stop: {
type: 'array',
label: 'After Agent Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute after agent processing. Can post-process responses or log interactions.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
Notification: {
type: 'array',
label: 'Notification Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description: 'Hooks that execute when notifications are sent.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
PreToolUse: {
type: 'array',
label: 'Pre Tool Use Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description: 'Hooks that execute before tool execution.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
PostToolUse: {
type: 'array',
label: 'Post Tool Use Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description: 'Hooks that execute after successful tool execution.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
PostToolUseFailure: {
type: 'array',
label: 'Post Tool Use Failure Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description: 'Hooks that execute when tool execution fails. ',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
SessionStart: {
type: 'array',
label: 'Session Start Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description: 'Hooks that execute when a new session starts or resumes.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
SessionEnd: {
type: 'array',
label: 'Session End Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description: 'Hooks that execute when a session ends.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
PreCompact: {
type: 'array',
label: 'Pre Compact Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description: 'Hooks that execute before conversation compaction.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
SubagentStart: {
type: 'array',
label: 'Subagent Start Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute when a subagent (Task tool call) is started.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
SubagentStop: {
type: 'array',
label: 'Subagent Stop Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute right before a subagent (Task tool call) concludes its response.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
PermissionRequest: {
type: 'array',
label: 'Permission Request Hooks',
category: 'Advanced',
requiresRestart: false,
default: [],
description:
'Hooks that execute when a permission dialog is displayed.',
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
items: HOOK_DEFINITION_ITEMS,
},
},
},
experimental: {
type: 'object',
label: 'Experimental',
category: 'Experimental',
requiresRestart: true,
default: {},
description: 'Settings to enable experimental features.',
showInDialog: false,
properties: {
cron: {
type: 'boolean',
label: 'Enable Cron/Loop Tools',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enable in-session cron/loop tools (experimental). When enabled, the model can create recurring prompts using cron_create, cron_list, and cron_delete tools. Can also be enabled via QWEN_CODE_ENABLE_CRON=1 environment variable.',
showInDialog: true,
},
},
},
} as const satisfies SettingsSchema;
export type SettingsSchemaType = typeof SETTINGS_SCHEMA;
export function getSettingsSchema(): SettingsSchemaType {
// Inject dynamic language options
const schema = SETTINGS_SCHEMA as unknown as SettingsSchema;
if (schema['general']?.properties?.['language']) {
(
schema['general'].properties['language'] as {
options?: SettingEnumOption[];
}
).options = getLanguageSettingsOptions();
}
return SETTINGS_SCHEMA;
}
type InferSettings<T extends SettingsSchema> = {
-readonly [K in keyof T]?: T[K] extends { properties: SettingsSchema }
? InferSettings<T[K]['properties']>
: T[K]['type'] extends 'enum'
? T[K]['options'] extends readonly SettingEnumOption[]
? T[K]['options'][number]['value']
: T[K]['default']
: T[K]['default'] extends boolean
? boolean
: T[K]['default'];
};
export type Settings = InferSettings<SettingsSchemaType>;