/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type { MCPServerConfig, BugCommandSettings, TelemetrySettings, OutboundCorrelationSettings, AuthType, ChatCompressionSettings, ModelProvidersConfig, ProviderProtocolConfig, } from '@qwen-code/qwen-code-core'; import { ApprovalMode, DEFAULT_MAX_SUBAGENT_DEPTH, GOAL_MAX_ACTIVE_MINUTES_CAP, GOAL_MAX_TURNS_CAP, DEFAULT_WEB_SEARCH_MAX_PER_SESSION, DEFAULT_WEB_SEARCH_TIMEOUT_MS, MAX_WEB_SEARCH_MAX_PER_SESSION, MAX_WEB_SEARCH_TIMEOUT_MS, DEFAULT_MAX_TOOL_CALLS_PER_TURN, DEFAULT_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH, DEFAULT_QWEN_CUSTOM_IGNORE_FILE_NAMES, DEFAULT_STOP_HOOK_BLOCK_CAP, DEFAULT_TOOL_OUTPUT_BATCH_BUDGET, DEFAULT_TOOL_RESULTS_TOTAL_CHARS_THRESHOLD, DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, OutputFormat, REASONING_EFFORT_TIERS, SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH_LIMIT, } from '@qwen-code/qwen-code-core'; import type { CustomTheme } from '../ui/themes/theme.js'; import { getLanguageSettingsOptions } from '../i18n/languages.js'; import { MergeStrategy } from '../utils/deepMerge.js'; import type { Mem0Settings } from './mem0-settings.js'; export const DEFAULT_OPENAI_LOG_RETENTION_DAYS = 7; export type SettingsType = | 'boolean' | 'string' | 'number' | 'integer' | '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 = new Set([ 'boolean', 'enum', ]); /** * Setting types edited as a number in the settings dialogs. `integer` is a * number the write-time validator also requires to be whole; handling it as * anything else leaves the setting uneditable. */ export function isNumericSettingType(type: SettingsType | undefined): boolean { return type === 'number' || type === 'integer'; } export interface SettingEnumOption { value: string | number; label: string; } export interface SettingDefinition { type: SettingsType; label: string; category: string; requiresRestart: boolean; // Required even when there is no fixed default (use // `default: undefined as | undefined`): it is load-bearing for the // `satisfies SettingsSchema` constraint and `InferSettings` below, and at // runtime it feeds only display and reset paths, never the load path. 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; /** Minimum value for number/integer-type settings. */ minimum?: number; /** Maximum value for number/integer-type settings. */ maximum?: number; /** Values rejected even when they fall within the declared range. */ excludedValues?: ReadonlyArray; /** * Primitive shapes a field accepted before it was expanded to its current * type. The exported JSON Schema wraps the field in `anyOf` so values from * those older shapes don't trip the IDE validator while the runtime * migration is still pending. Has no runtime effect — it's purely a * compatibility hint for editors. * * Narrowed to the subset our generator can faithfully emit as a * one-liner `{ type: }` schema fragment. `'enum'` is * not a valid JSON Schema `type` value at all (enum constraints * use the `enum` keyword, not `type: 'enum'`), so allowing it here * would silently produce an invalid `settings.schema.json`. * `'object'` IS a valid JSON Schema type, but a bare * `{ type: 'object' }` legacy entry would accept ANY object value * — most likely not what the field's pre-expansion shape actually * permitted. Future legacy shapes that need `enum` / structured- * object compatibility should land their own branch in * `convertSettingToJsonSchema` (with proper `enum:` / `properties:` * companions) instead of widening this set. */ legacyTypes?: ReadonlyArray<'boolean' | 'string' | 'number' | 'array'>; /** * Escape hatch for the JSON Schema generator: when set, this object is * emitted verbatim under the setting's properties entry instead of the * shape derived from `type`/`properties`/etc. The `description` is still * carried forward from the SettingDefinition. * * Use sparingly — for most settings the generator's normal mapping is * preferable so the source schema stays the single source of truth. The * one valid case so far is settings whose accepted runtime shape is a * union (e.g. string | { path } | { small, large }) that the * SettingDefinition `type` field cannot express. */ jsonSchemaOverride?: Record; } /** * 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; } /** * Source for a single tier of custom ASCII art. Either an inline string * or a reference to a file on disk that contains the art. */ export type AsciiArtSource = string | { path: string }; /** * Setting value for `ui.customAsciiArt`. Accepts a bare source (treated as * both width tiers), or a width-aware `{small, large}` object. */ export type CustomAsciiArtSetting = | AsciiArtSource | { small?: AsciiArtSource; large?: AsciiArtSource }; /** * 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', 'prompt'], 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.', }, prompt: { type: 'string', description: 'The prompt to send to the model. Required for "prompt" type.', }, model: { type: 'string', description: 'The optional model to use for a "prompt" hook.', }, 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. Defaults to 60 for command hooks, 600 for http hooks and 30 for prompt hooks. For command hooks, a value of 1000 or more is read as legacy milliseconds.', }, 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', // Hot-reloadable (sub-task 3): a runtime edit is reconciled live by the // SettingsWatcher → reinitializeMcpServers path. Marking it // restart-required would make the watcher suppress MCP-only edits, so the // listener that drives the reconcile would never fire. requiresRestart: false, default: {} as Record, 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>, description: 'Configuration for messaging channels.', showInDialog: false, mergeStrategy: MergeStrategy.SHALLOW_MERGE, }, serve: { type: 'object', label: 'Serve', category: 'Advanced', requiresRestart: true, default: {}, description: 'Persistent qwen serve settings.', showInDialog: false, mergeStrategy: MergeStrategy.SHALLOW_MERGE, properties: { channels: { type: 'array', label: 'Startup Channels', category: 'Advanced', requiresRestart: true, default: [] as string[], description: 'Messaging channels to start automatically when the daemon boots.', showInDialog: false, items: { type: 'string' }, }, tokenQr: { type: 'boolean', label: 'Token QR', category: 'Advanced', requiresRestart: true, default: false, description: "Print the token-bearing QR even when stdout is captured and the bearer is an operator-supplied (stable) token. Honored from user, system, and system-defaults settings only — a workspace settings file must not be able to push the operator's credential into logs; qwen serve --no-token-qr suppresses the QR for that run. Same effect as qwen serve --token-qr.", showInDialog: false, }, maxConcurrentSubSessionsPerCaller: { type: 'integer', label: 'Max Concurrent Sub-Sessions Per Caller', category: 'Advanced', requiresRestart: true, default: 16, minimum: 1, description: 'Per-session ceiling on concurrent in-flight sub-sessions spawned via the create_sub_session tool.', showInDialog: false, }, maxConcurrentSubSessionsTotal: { type: 'integer', label: 'Max Concurrent Sub-Sessions Total', category: 'Advanced', requiresRestart: true, default: 24, minimum: 1, maximum: 1024, description: 'Workspace-wide ceiling on concurrent in-flight sub-sessions across all callers.', showInDialog: false, }, }, }, // Model providers configuration grouped by provider id modelProviders: { type: 'object', label: 'Model Providers', category: 'Model', requiresRestart: false, default: {} as ModelProvidersConfig, description: 'Model providers configuration keyed by provider id (a built-in provider protocol such as "openai" or "gemini", or a custom id mapped via providerProtocol). Each entry is an array of model configurations. OpenAI-compatible models can select wireApi: "chat-completions" or "responses"; omitting wireApi keeps the declared protocol (Chat Completions under openai). Released openai-responses declarations remain readable; new setup writes openai plus wireApi.', showInDialog: false, mergeStrategy: MergeStrategy.REPLACE, }, // Maps a custom modelProviders provider id to its SDK protocol (AuthType). providerProtocol: { type: 'object', label: 'Provider Protocols', category: 'Model', requiresRestart: true, default: {} as ProviderProtocolConfig, description: 'Maps a custom modelProviders provider id to the SDK protocol that routes its requests (e.g. {"idealab": "openai"}). Lets a custom provider id reuse a built-in protocol. Built-in provider ids (openai, gemini, anthropic, vertex-ai, qwen-oauth) are routed automatically and need no entry. New OpenAI configurations map to openai and select wireApi per model; released openai-responses mappings remain readable.', showInDialog: false, mergeStrategy: MergeStrategy.REPLACE, }, plansDirectory: { type: 'string', label: 'Plans Directory', category: 'Advanced', requiresRestart: true, default: undefined as string | undefined, description: 'Custom directory for approved Plan Mode files. Relative paths are resolved from the project root, and the resolved path must stay within the project root. Defaults to ~/.qwen/plans.', showInDialog: false, }, // Environment variables fallback env: { type: 'object', label: 'Environment Variables', category: 'Advanced', requiresRestart: true, default: {} as Record, 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, }, proxy: { type: 'string', label: 'Proxy', category: 'Advanced', requiresRestart: true, default: undefined as string | undefined, description: 'Proxy URL for CLI HTTP requests. Takes precedence over proxy environment variables when --proxy is not provided.', showInDialog: false, }, 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, }, outputStyle: { type: 'string', label: 'Output Style', category: 'General', // Generic settings edits do not rebuild the running system instruction; // `/output-style` owns the separate live-update path. requiresRestart: true, default: undefined as string | undefined, description: 'Name of the output style that shapes how responses are written: a built-in such as "Concise" or "Explanatory", or a custom style\'s name — its frontmatter `name`, defaulting to the file name without ".md" — from ~/.qwen/output-styles or .qwen/output-styles. Leave unset for the default style. Change it with /output-style. Only the built-ins are available in --bare and --safe-mode.', // The style list includes user/project-defined entries; the dedicated // /output-style picker owns selection rather than a static enum here. showInDialog: false, }, vimMode: { type: 'boolean', label: 'Vim Mode', category: 'General', requiresRestart: false, default: false, description: 'Enable Vim keybindings', showInDialog: true, }, voice: { type: 'object', label: 'Voice Dictation', category: 'General', requiresRestart: false, default: {}, description: 'Voice dictation settings.', showInDialog: false, properties: { enabled: { type: 'boolean', label: 'Voice Dictation', category: 'General', requiresRestart: false, default: false, description: 'Enable voice dictation in the prompt input.', showInDialog: false, }, mode: { type: 'enum', label: 'Voice Dictation Mode', category: 'General', requiresRestart: false, default: 'hold', description: 'How push-to-talk behaves: "hold" to talk while held, or "tap" to start and tap (or pause) to stop and submit.', showInDialog: false, options: [ { value: 'hold', label: 'Hold to talk' }, { value: 'tap', label: 'Tap to toggle' }, ], }, language: { type: 'string', label: 'Voice Dictation Language', category: 'General', requiresRestart: false, default: '', description: 'Preferred spoken language for voice transcription (e.g. "english", "chinese"). Leave empty to auto-detect.', showInDialog: false, }, keytermsFile: { type: 'string', label: 'Voice Dictation Keyterms File', category: 'General', requiresRestart: false, default: '', description: 'Path to a custom keyterms file (one term per line, "#" for comments) that biases voice transcription toward domain-specific terms. Relative paths resolve from the workspace root; defaults to ".qwen/voice-keyterms.txt" when present. The file contents are sent to the ASR provider and it is read only in trusted workspaces. Only applies to Qwen ASR models (qwen3-asr-*).', showInDialog: false, }, refineTranscript: { type: 'boolean', label: 'Refine Voice Transcript', category: 'General', requiresRestart: false, default: true, description: 'Clean up voice transcripts with the fast model before inserting them — removes filler words and fixes recognition errors while preserving meaning. Falls back to the raw transcript on failure, and is skipped when no fast model is configured.', showInDialog: false, }, }, }, enableAutoUpdate: { type: 'boolean', label: 'Enable Auto Update', category: 'General', requiresRestart: false, default: true, description: 'Enable automatic update checks and installations on startup.', showInDialog: true, }, batchAutoCollect: { type: 'boolean', label: 'Batch Auto Collect', category: 'General', requiresRestart: true, default: true, description: "Collect this project's /batch-api tasks in interactive sessions when their batch finishes (also at startup) and write the results. Polls the provider over HTTP; never calls the model and never retries failed items.", showInDialog: false, }, showSessionRecap: { type: 'boolean', label: 'Show Session Recap', category: 'General', requiresRestart: false, // Off by default — an ambient background LLM call isn't something // users should be opted into silently, especially when `fastModel` // is unset and the call would land on the main coding model. // Manual `/recap` works regardless. default: false, description: 'Auto-show a one-line "where you left off" recap when returning to the terminal after being away. Off by default. Use /recap to trigger manually regardless of this setting.', showInDialog: true, }, sessionRecapAwayThresholdMinutes: { type: 'number', label: 'Session Recap Away Threshold (minutes)', category: 'General', requiresRestart: false, default: 5, minimum: 1, description: "How many minutes the terminal must be blurred before an auto-recap fires on the next focus-in. Matches Claude Code's default of 5 minutes; raise if you briefly alt-tab and do not want recaps to pile up.", showInDialog: true, }, cleanupPeriodDays: { type: 'number', label: 'Cleanup Period (days)', category: 'General', // LoadedSettings._merged is cached without verified setValue→recompute // paths in all UI flows. Mark restart-required so users aren't // surprised when a mid-session edit doesn't take effect immediately. requiresRestart: true, default: 30, minimum: 0, description: 'Number of days to retain ~/.qwen/file-history/ session backups used by /rewind, background subagent transcripts under /subagents/, and session debug logs under the runtime debug/ directory. Data older than this is removed by a background housekeeping pass that runs at most once per day. Set to 0 for minimum retention (~1 hour) — protects sessions touched in the last hour, plus the currently active session.', showInDialog: true, }, gitCoAuthor: { type: 'object', label: 'Attribution', category: 'General', requiresRestart: false, // Match `normalizeGitCoAuthor`'s runtime defaults so the IDE // schema publishes the same "enabled by default" hint users see // at runtime. The empty-object form here would silently lose // editor-surfaced defaults. default: { commit: true, pr: true }, description: 'Attribution added to git commits and pull requests created through Qwen Code.', showInDialog: false, // Pre-V4 settings stored this as a single boolean. The V3→V4 // migration rewrites those on first launch, but the IDE schema // validator runs before that — accept the boolean shape so users // editing settings.json in VS Code don't see a spurious warning // until they run qwen once. Config.normalizeGitCoAuthor handles // the boolean at runtime. legacyTypes: ['boolean'], properties: { commit: { type: 'boolean', label: 'Attribution: commit', category: 'General', requiresRestart: false, default: true, description: 'Add a Co-authored-by trailer to git commit messages AND attach a per-file AI-attribution git note (`refs/notes/ai-attribution`) for commits made through Qwen Code. Disabling skips both.', showInDialog: true, }, pr: { type: 'boolean', label: 'Attribution: PR', category: 'General', requiresRestart: false, default: true, description: 'Append a Qwen Code attribution line to PR descriptions when running `gh pr create`.', showInDialog: true, }, }, }, 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 follow the user input language, ' + '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, }, notificationMode: { type: 'enum', label: 'Notification Mode', category: 'General', requiresRestart: false, default: 'all', description: 'Which unfocused-terminal events fire a bell/OS notification. ' + '"all" fires on every tool approval prompt AND on task completion (current behavior). ' + '"task-complete" suppresses the per-approval notification and only fires when a long task returns to idle. ' + 'Requires `terminalBell` to be enabled; otherwise no notifications fire regardless of mode.', showInDialog: true, options: [ { value: 'all', label: 'All (approvals + task completion)' }, { value: 'task-complete', label: 'Task completion only' }, ], }, preventSystemSleep: { type: 'boolean', label: 'Prevent System Sleep While Running', category: 'General', // Read once at startup via Config.preventSystemSleep (a readonly field // captured in loadCliConfig), so a runtime toggle only takes effect // after restart. requiresRestart: true, default: true, description: 'Prevent the system from sleeping while Qwen Code is streaming a model response or executing tools. Idle prompt time and permission prompts do not inhibit sleep.', 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' }, ], }, }, }, review: { type: 'object', label: 'Review', category: 'General', requiresRestart: false, default: {}, description: 'Settings for the /review skill.', showInDialog: false, properties: { attribution: { type: 'boolean', label: 'Attribution: review', category: 'General', requiresRestart: false, default: true, description: 'Append the attribution footer naming the model and CLI version (e.g. "_— qwen3-coder via Qwen Code /review (v0.21.2)_") to review bodies and inline comments posted to GitHub. Disable to post reviews without VISIBLE AI attribution: no footer, and no "**[Critical]**"/"**[Suggestion]**" severity markers on posted comments and body lists. Unattributed posts stay identifiable in the raw source: each posted comment carries an invisible severity marker ("") and the review body carries a ledger marker ("") — anything reading comment bodies (GitHub API automation, the workflows this setting couples to) still recognizes a /review artifact, and presubmit duplicate detection recognizes the reviewing account\'s earlier posts by the severity marker, though unattributed posts from other accounts escape it. Another consequence: qwen-autofix\'s Critical-only mode (engaged after round 5, or earlier when a counting window\'s diff-growth budget trips) no longer recognizes the posted findings as Critical and defers them. Only honored from User, System, and SystemDefaults settings scopes; values set in Workspace settings are ignored, so a repository cannot set review policy for its reviewers.', showInDialog: true, }, sandbox: { type: 'enum', label: 'Sandbox the reviewed code: review', category: 'General', requiresRestart: false, default: 'off', description: 'Run the REVIEWED repository\'s own commands — `npm ci` with its install scripts, the build, the test suite, and every mutation probe — inside a container instead of directly as you. A review executes the code it is reviewing, and today those commands inherit the review process\'s whole environment (on CI that includes the model and GitHub credentials). "auto" uses a container when docker or podman answers and runs directly when neither does; "required" refuses to run them unsandboxed, which makes the evidence that depends on execution (build/test findings, mutation verdicts, `Source: [probe]`) unavailable for that run rather than ending the review; "off" is today\'s behaviour and stays the default, because containerising a build by surprise changes what native modules compile against. Only honored from User, System, and SystemDefaults settings scopes; values set in Workspace settings are ignored, so a repository cannot switch off the containment that exists to contain it.', showInDialog: true, options: [ { value: 'off', label: 'Off (run the reviewed code directly)' }, { value: 'auto', label: 'Auto (container when one is available)' }, { value: 'required', label: 'Required (never run it unsandboxed)' }, ], }, effort: { type: 'enum', label: 'Default effort: review', category: 'General', requiresRestart: false, default: 'auto', description: 'Default effort for /review when neither --effort nor a project-remembered explicitly typed level applies. "auto" keeps the built-in rule (high for PRs, medium for local changes). An explicit or remembered level wins; an effective --comment still forces high and --fix still floors at medium. Only honored from User, System, and SystemDefaults settings scopes; values set in Workspace settings are ignored, so a repository cannot set review policy for its reviewers.', showInDialog: true, options: [ { value: 'auto', label: 'Auto (high for PRs, medium for local)' }, { value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }, ], }, comment: { type: 'boolean', label: 'Comment by default: review', category: 'General', requiresRestart: false, default: false, description: 'Treat every PR /review as if --comment was passed: findings are posted to the pull request without the flag. The post still binds to the PR named in the invocation. Enable only if you always want reviews published. Only honored from User, System, and SystemDefaults settings scopes; values set in Workspace settings are ignored, so a repository cannot set review policy for its reviewers.', showInDialog: true, }, severityFloor: { type: 'enum', label: 'Posting floor: review', category: 'General', requiresRestart: false, default: 'auto', description: 'The lowest severity a PR /review posts when --severity-floor is not given. "auto" keeps the round-adaptive default: Suggestions post through round 5, and from round 6 only Criticals post while otherwise-postable high-confidence Suggestions are recorded and deferred (low-confidence and Nice-to-have findings stay terminal-only as ever); under "auto", rounds 2-5 additionally defer new Suggestions on code unchanged since the previous round — the same discipline that stops review rounds from ballooning a PR. "critical" applies that posture from round 1; "suggestion" keeps Suggestions posting at every round. Non-PR targets have no rounds and ignore this. Only honored from User, System, and SystemDefaults settings scopes; values set in Workspace settings are ignored, so a repository cannot set review policy for its reviewers.', showInDialog: true, options: [ { value: 'auto', label: 'Auto (Critical-only from round 6)' }, { value: 'critical', label: 'Critical-only (every round)' }, { value: 'suggestion', label: 'Suggestions and Criticals' }, ], }, reverseAuditRounds: { type: 'number', label: 'Reverse-audit round ceiling: review', category: 'General', requiresRestart: false, default: 0, description: 'Lower the reverse-audit loop\'s round cap for every high-effort review. The cap is normally chosen from the diff topology (10 small / 5 chunked; a huge diff is 3 when the run has a review deadline and 5 when it does not, because that reduction answers a CI ceiling and applies only where one exists) because a round costs one agent on a small diff and ~90 minutes on a huge one; this setting can only LOWER whichever tier applies, never raise it — a value that is not a whole number above zero, or that is out of range (below 3, or above the plan\'s own tier), is ignored and leaves the tier alone — JSON Schema has no integer type here, so a fraction validates in an editor and is then discarded at runtime. Understand what it buys before enabling: the loop ends on two consecutive dry rounds, so cutting the cap does not make reviews converge sooner, it makes them stop before converging more often — and every such stop is disclosed as unreviewed scope and caps the verdict at Comment, so a cheaper review is also one that can no longer Approve. To spend LESS on reviews generally, prefer "effort". Nothing here makes a loop run LONGER: a review deadline bounds a run rather than extending it, and on a huge diff setting one lowers the cap from 5 to 3 rather than raising it. Only honored from User, System, and SystemDefaults settings scopes; values set in Workspace settings are ignored, so a repository cannot set review policy for its reviewers.', showInDialog: true, }, approachRounds: { type: 'number', label: 'Approach-signal round threshold: review', category: 'General', requiresRestart: false, default: 0, description: 'How many rounds a pull request must reach before the review may add one advisory paragraph saying the shape of the change, rather than the current patch, looks like the open question. It appears only when the diff has also grown several times over since the review first measured it, and never on an Approve. It is disclosure only: it adds no finding, changes no verdict, and blocks nothing — it exists because every finding is anchored to a line in the current diff, so the review can report where an approach leaks but never that a different approach would retire all of the leaks at once. Leave at 0 to keep the built-in threshold of 5 rounds; raise it to make the paragraph appear later, and set it very high to silence it. A value that is not a whole number above zero is ignored. Only honored from User, System, and SystemDefaults settings scopes; values set in Workspace settings are ignored, so a repository cannot set review policy for its reviewers.', showInDialog: true, }, }, }, 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. With `stream-json`, runs started ' + 'with a prompt behave as non-interactive (headless), matching ' + '`--output-format stream-json`. Flags validated at argv parse ' + 'time (`--include-partial-messages`, `--input-format ' + 'stream-json`) still require the explicit `--output-format ' + 'stream-json` flag.', showInDialog: false, // The values are the runtime's `OutputFormat` enum members, and the // settingsSchema test pins these options against // `Object.values(OutputFormat)`, so a format added in core fails // that test until this list and the regenerated JSON follow. options: [ { value: OutputFormat.TEXT, label: 'Text' }, { value: OutputFormat.JSON, label: 'JSON' }, { value: OutputFormat.STREAM_JSON, label: 'Stream JSON' }, ], }, showTimestamps: { type: 'boolean', label: 'Show Timestamps', category: 'General', requiresRestart: false, default: false, description: 'Show [HH:MM:SS] timestamp before each assistant response.', showInDialog: true, }, }, }, dualOutput: { type: 'object', label: 'Dual Output', category: 'Advanced', requiresRestart: true, default: {}, description: 'Dual-output sidecar mode: emit structured JSON events to a ' + 'second channel while the TUI renders normally on stdout. See ' + 'docs/users/features/dual-output.md. CLI flags take precedence ' + 'over these settings.', showInDialog: false, properties: { jsonFile: { type: 'string', label: 'JSON Event File', category: 'Advanced', requiresRestart: true, default: undefined as string | undefined, description: 'File path for structured JSON event output. Equivalent to ' + '--json-file. Ignored if --json-fd or --json-file is also set.', showInDialog: false, }, inputFile: { type: 'string', label: 'Remote Input File', category: 'Advanced', requiresRestart: true, default: undefined as string | undefined, description: 'File path for remote input commands (JSONL). Equivalent to ' + '--input-file. Ignored if --input-file is also set.', showInDialog: false, }, }, }, 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, }, brand: { type: 'object', label: 'Web Shell Brand', category: 'UI', requiresRestart: false, default: {}, description: 'Product name and logo the Web Shell presents. Read from operator scopes only (System Defaults, User, System) — a workspace settings file cannot rebrand the shell, because it commonly comes from a repository the person opening the shell did not write. Not editable from the in-browser Settings page; edit settings.json directly.', showInDialog: false, properties: { name: { type: 'string', label: 'Web Shell Brand Name', category: 'UI', requiresRestart: false, default: '' as string, description: "Product name shown in the Web Shell sidebar, welcome header, About panel and browser tab title. Sanitized to a single line; capped at 80 characters. A placeholder value that substitution would change ($VAR/${VAR} with the variable set) is refused with a warning on the daemon's stderr, because the substitution source is process-wide and a workspace could supply it; an unresolvable placeholder is kept verbatim. Leave empty to use the built-in name. The terminal banner has its own separate setting, `ui.customBannerTitle`.", showInDialog: false, }, logoPath: { type: 'string', label: 'Web Shell Brand Logo Path', category: 'UI', requiresRestart: false, default: '' as string, description: 'Path to an SVG file used as the Web Shell sidebar logo and browser favicon. A leading "~" is expanded, and a relative path resolves against the directory of the settings file that declares it. The file must be a regular file — not a symlink, and not reachable through more than one hard link — at most 32 KiB both on disk and once UTF-8-decoded, and its root element must be a namespaced — a default xmlns, or an xmlns:svg binding on a prefix-bound root, is what makes it renderable as an image. Environment variable placeholders that would resolve are refused, as with the brand name. Leave empty to use the built-in logo.', showInDialog: false, }, }, }, autoModeAcknowledged: { type: 'boolean', label: 'Auto Mode Acknowledged', category: 'UI', requiresRestart: false, default: false, description: 'True once the user has seen the first-time information message about the AUTO approval mode. Set automatically; not intended for manual configuration.', showInDialog: false, }, statusLine: { type: 'object', label: 'Status Line', category: 'UI', requiresRestart: false, default: undefined as | ( | { type: 'command'; command: string; refreshInterval?: number; respectUserColors?: boolean; hideContextIndicator?: boolean; } | { type: 'preset'; items: string[]; useThemeColors?: boolean; hideContextIndicator?: boolean; } ) | undefined, description: 'Status line display configuration. Use `type: "preset"` with built-in item ids, or `type: "command"` with a shell command. Optional command `refreshInterval` (seconds, >= 1) re-runs the command on a timer so external data stays fresh. Set `respectUserColors: true` to preserve ANSI color codes in command output instead of applying dim/theme styling. Set `hideContextIndicator: true` to hide the built-in context usage indicator in the footer right section, or `false` to always show it. When `hideContextIndicator` is unset, the footer indicator is hidden automatically for preset status lines that already include `context-used` or `context-remaining`, and shown otherwise (including for `command` status lines). When unset (default), the built-in default preset (project name, git branch, model, context usage) is shown automatically; set to `null` to explicitly disable the status line.', showInDialog: false, }, customThemes: { type: 'object', label: 'Custom Themes', category: 'UI', requiresRestart: false, default: {} as Record, description: 'Custom theme definitions.', showInDialog: false, }, hideBuiltinWorktreeIndicator: { type: 'boolean', label: 'Hide Built-in Worktree Indicator', category: 'UI', requiresRestart: false, default: false, description: 'When true, the built-in `⎇ worktree- ()` line in the Footer is hidden. The worktree state is still surfaced to custom statusline scripts via the stdin payload (`worktree.{name, path, branch, original_cwd, original_branch}`). Keep at the default `false` unless your custom statusline renders the worktree itself — otherwise an active worktree silently has no UI affordance.', showInDialog: false, }, hideWindowTitle: { type: 'boolean', label: 'Hide Window Title', category: 'UI', requiresRestart: true, default: false, description: 'Hide the window title bar', showInDialog: false, }, disableWorkflowKeywordTrigger: { type: 'boolean', label: 'Disable Workflow Keyword Trigger', category: 'UI', requiresRestart: false, default: false, description: 'When true, mentioning the word `workflow` in a prompt no longer softly steers the turn toward the Workflow tool (and the Footer `workflow active` indicator is suppressed). Only applies when workflows are enabled.', showInDialog: true, }, showStatusInTitle: { type: 'boolean', label: 'Show Status in Title', category: 'UI', requiresRestart: false, default: true, description: 'Show Qwen Code session name and status in the terminal window title', showInDialog: true, }, hideTips: { type: 'boolean', label: 'Hide Tips', category: 'UI', requiresRestart: false, default: false, description: 'Hide helpful tips in the UI', showInDialog: true, }, history: { type: 'object', label: 'History', category: 'UI', requiresRestart: false, default: {}, description: 'History display settings.', showInDialog: false, properties: { collapseOnResume: { type: 'boolean', label: 'Collapse On Resume', category: 'UI', requiresRestart: false, default: false, description: 'Whether to collapse history by default when resuming a session.', showInDialog: false, }, collapsePreviewCount: { type: 'number', label: 'Collapse Preview Count', category: 'UI', requiresRestart: false, default: 0, description: 'Number of most recent user turns to keep visible when collapsing history on resume. 0 collapses all restored history by default; -1 shows all restored history.', showInDialog: false, }, }, }, 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, }, renderMode: { type: 'enum', label: 'Markdown Render Mode', category: 'UI', requiresRestart: false, default: 'render', description: 'Default Markdown display mode. Use "render" for rich visual previews, or "raw" to show source-oriented Markdown by default. Toggle during a session with Alt/Option+M; on macOS the terminal must send Option as Meta.', showInDialog: true, options: [ { value: 'render', label: 'Render visual previews' }, { value: 'raw', label: 'Show raw source' }, ], }, 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, }, showResponseTokensPerSecond: { type: 'boolean', label: 'Show Response Tokens Per Second', category: 'UI', requiresRestart: true, default: false, description: 'Show a live tokens/sec estimate next to the response token counter while the model is streaming. Takes effect in the next session.', showInDialog: true, }, 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. Choosing "Start new chat session" suppresses the dialog for that project until the project summary changes.', 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: true, description: 'Show context-aware follow-up suggestions after task completion. Press Tab, Right Arrow, or Enter to accept into the input buffer.', 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: false, 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, // Retired everywhere: compact tool output is always on in the TUI // (Ctrl+O opens the transcript there), and the web shell now fixes // its compact view on too. Kept schema-registered (mirrors // `voiceModel`) so settings files that still carry the key load // without warnings; nothing reads the value anymore. description: 'Retired: compact view is always on in both the TUI and the web shell. The key is kept so existing settings files do not warn.', showInDialog: false, }, useTerminalBuffer: { type: 'boolean', label: 'Virtualized History (reduces flicker on long sessions)', category: 'UI', requiresRestart: true, default: true, description: 'Render conversation history in an in-app scrollable viewport instead of the terminal scrollback buffer. Enabled by default in compatible interactive terminals to avoid flicker, scroll-storm, and interface freeze on long sessions, after Ctrl+O, after Ctrl+E / Ctrl+F (expand), after window resize, or when alt-tabbing back. Screen reader mode and non-interactive output such as piped stdout or CI use append-only terminal output instead. Scroll with Shift+↑/↓ (line), PgUp/PgDn (page), Ctrl+Home/End (top/bottom), or the mouse wheel. Also enables mouse interactions: click an option in a menu/dialog to select it, hover to highlight it, and click in the prompt to position the cursor. Does NOT use the host terminal scrollback while enabled. Drag to select text in the viewport (double/triple click selects a word/line), copied on release. To use the terminal’s own selection instead, hold Shift (or Option on macOS) while dragging. A single click opens an http(s) hyperlink under the pointer (other link schemes are copied to the clipboard), and right-click over a link or a text selection opens an in-app context menu. These mouse interactions are controlled by ui.mouseTracking; disable that setting to hand the mouse fully back to the terminal.', showInDialog: true, }, showScrollbar: { type: 'boolean', label: 'Show Scrollbar (Virtualized History)', category: 'UI', requiresRestart: false, default: true, description: 'Show the auto-hiding scrollbar in the in-app scrollable viewport (Virtualized History). The bar appears while scrolling and fades out when idle. Disable to hide it entirely.', showInDialog: true, }, mouseTracking: { type: 'boolean', label: 'Mouse Tracking', category: 'UI', requiresRestart: true, default: true, description: 'Enable in-app SGR mouse tracking. While enabled, Qwen Code captures mouse events for text selection, click-to-position in text inputs, row hover, history-item toggling, and viewport scrolling. Because the terminal forwards all mouse events to the app, Qwen Code supplies its own equivalents for what the terminal can no longer do natively: a single click opens an http(s) hyperlink under the pointer (other link schemes are copied to the clipboard), and right-click over a link or a text selection opens an in-app context menu with Open Link / Copy Link Address / Copy Selection. Disable to hand the mouse fully back to the terminal (native right-click menu and link clicks); this turns off all in-app mouse interaction, and in Virtualized History the wheel no longer scrolls the transcript — use Shift+↑/↓, PgUp/PgDn, or Ctrl+Home/End instead (pair with ui.useTerminalBuffer: false to restore native terminal scrollback).', showInDialog: true, }, showToolCallDetails: { type: 'boolean', label: 'Show Tool Call Details', category: 'UI', requiresRestart: false, default: true, description: 'Show tool arguments and results inline. Disable to render ordinary tool calls as a one-line summary; click a summary in Virtualized History or press Ctrl+O to expand its details. Approval prompts, user-initiated shell commands, and focused interactive shells remain expanded.', showInDialog: true, }, showToolCallArgs: { type: 'boolean', label: 'Show Tool Call Arguments', category: 'UI', requiresRestart: false, default: false, description: 'Render tool calls on their own line with their raw arguments inline, instead of the type-based compact summary that folds read/search/list batches into "Read 3 files". Useful when debugging MCP integrations or tool schemas. Applies wherever the arguments are available: live, resumed, agent-view and speculated turns. The row is capped at 2 wrapped lines (and never more than 1000 characters) and truncated with a `+N chars` marker; press Ctrl+O for the complete payload. Groups of running parallel subagents keep their compact roster, and daemon-attached sessions carry no arguments, so both keep the compact view — press Ctrl+O there. Does not change result-output truncation.', showInDialog: true, }, shellOutputMaxLines: { type: 'number', label: 'Shell Output Max Lines', category: 'UI', requiresRestart: false, default: 5, description: 'Max number of shell output lines shown inline. Set to 0 to disable the cap and show full output. The hidden line count is still surfaced via the `+N lines` indicator.', showInDialog: true, }, hideBanner: { type: 'boolean', label: 'Hide Banner', category: 'UI', requiresRestart: false, default: false, description: 'Hide the startup ASCII banner and info panel.', showInDialog: true, }, customBannerTitle: { type: 'string', label: 'Custom Banner Title', category: 'UI', requiresRestart: false, default: '' as string, description: 'Replace the default ">_ Qwen Code" title shown in the banner info panel. The version suffix is always appended.', showInDialog: false, }, customBannerSubtitle: { type: 'string', label: 'Custom Banner Subtitle', category: 'UI', requiresRestart: false, default: '' as string, description: 'Optional subtitle line rendered between the banner title and the auth/model line. When unset, the info panel keeps its blank spacer row.', showInDialog: false, }, customAsciiArt: { type: 'object', label: 'Custom ASCII Art', category: 'UI', requiresRestart: false, default: undefined as CustomAsciiArtSetting | undefined, description: 'Replace the default QWEN ASCII art. Accepts an inline string, {"path": "..."}, or {"small": ..., "large": ...} for width-aware selection.', showInDialog: false, // The runtime accepts three shapes (inline string, {path}, or // {small,large} where each tier is itself string-or-{path}). The // SettingDefinition `type: 'object'` keeps the in-app dialog out of // the way (we don't want a multi-line ASCII editor in the TUI), but // the JSON Schema needs a real union so VS Code stops flagging the // documented bare-string form. // The `oneOf` here uses three *mutually exclusive* branches rather // than one permissive object branch, so VS Code rejects nonsense // like `{ path, small, large }` (which the runtime would also // reject — see `normalizeTiers` in `customBanner.ts`). jsonSchemaOverride: { oneOf: [ { type: 'string' }, // Bare `{path}` — no tier keys allowed. { type: 'object', properties: { path: { type: 'string' } }, required: ['path'], additionalProperties: false, }, // Width-aware `{small?, large?}` — `path` not allowed at this // level; each tier is itself string-or-`{path}`. { type: 'object', properties: { small: { oneOf: [ { type: 'string' }, { type: 'object', properties: { path: { type: 'string' } }, required: ['path'], additionalProperties: false, }, ], }, large: { oneOf: [ { type: 'string' }, { type: 'object', properties: { path: { type: 'string' } }, required: ['path'], additionalProperties: false, }, ], }, }, additionalProperties: false, }, ], }, }, }, }, 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, jsonSchemaOverride: { type: 'object', properties: { userId: { description: 'Stable end-user identifier written to GenAI spans as gen_ai.user.id for ARMS session analysis. This value is linkable personal data: prefer a pseudonymous ID, and configure it only when one process represents one user.', type: 'string', }, includeSensitiveSpanAttributes: { description: 'When enabled, user prompts, system prompts, tool inputs/outputs, and model responses are written to native OTel span attributes in addition to the log-to-span bridge. Warning: this may expose sensitive data (file contents, shell commands, conversation history) to your OTLP backend.', type: 'boolean', default: false, }, sensitiveSpanAttributeMaxLength: { description: 'Maximum JavaScript string length for each sensitive native OTel span attribute content payload. Default: 1048576 (1 MiB). Maximum: 104857600 (100 MiB). Set lower if your collector or backend rejects large span attributes.', type: 'integer', minimum: 1, maximum: SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH_LIMIT, default: DEFAULT_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH, }, resourceAttributes: { description: 'Static resource attributes attached to every span/log/metric the SDK exports (OTLP or file outfile — they share the same Resource). Merged with the OTEL_RESOURCE_ATTRIBUTES env var; settings win on key conflict. Reserved keys (service.version, session.id) are dropped with a warning.', type: 'object', additionalProperties: { type: 'string' }, default: {}, }, metrics: { description: 'Per-signal cardinality controls for exported metrics.', type: 'object', additionalProperties: false, properties: { includeSessionId: { description: 'Include session.id on every metric data point. WARNING: each CLI session creates a new value, causing unbounded metric time-series fan-out at the backend. Only enable for short-term debugging — spans and logs still carry session.id.', type: 'boolean', default: false, }, }, }, }, additionalProperties: true, }, }, outboundCorrelation: { type: 'object', label: 'Outbound Correlation', category: 'Advanced', requiresRestart: true, default: undefined as OutboundCorrelationSettings | undefined, description: "SECURITY-RELEVANT. Controls what client-side correlation data qwen-code writes into outbound LLM API requests (DashScope, OpenAI, Anthropic, etc.) — separate from `telemetry.*` which governs data flow into the operator's OWN OTLP collector. All values default to off. Opt in only when the LLM provider also reports into your OTel collector for cross-process trace stitching (e.g. ARMS Tracing + DashScope).", showInDialog: false, jsonSchemaOverride: { type: 'object', properties: { propagateTraceContext: { description: "Requires `telemetry.enabled: true`. Inject W3C `traceparent` on outbound `fetch` requests (LLM SDK calls, MCP StreamableHTTP, WebFetch, ...) AND as a `TRACEPARENT` environment variable in shell child processes (Bash tool, hooks, monitor). When enabled, any existing `TRACEPARENT` in the parent environment is overwritten with qwen-code's own trace context. Default: false — trace context stays internal to the operator's OTLP collector. Set true when you want cross-process trace stitching with an OTel-aware LLM provider (e.g. ARMS+DashScope) or need shell scripts / CLI tools to participate in distributed tracing.", type: 'boolean', default: false, }, allowDynamicHeaderValues: { description: 'SECURITY-RELEVANT. Allow `modelProviders[].generationConfig.customHeaders` values to contain runtime placeholders — currently `${session_id}` — expanded per request instead of frozen at client construction. Default false: a value containing a placeholder is dropped rather than sent. Enable when a gateway requires a stable per-conversation identifier (e.g. OpenCode Go requires `x-opencode-session`). Which hosts receive the value and what the header is called are decided by the provider entry you attach the header to; this switch only decides whether `${session_id}` may be expanded from live session state and does not identify which settings source supplied the header.', type: 'boolean', default: false, }, }, additionalProperties: 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, }, batch: { type: 'object', label: 'Batch', category: 'Model', requiresRestart: true, default: {}, description: 'Independent model selection for /batch-api and qwen batch. Unset to reuse the main model configuration.', showInDialog: false, properties: { model: { type: 'string', label: 'Batch Model', category: 'Model', requiresRestart: true, default: undefined as string | undefined, description: 'Model ID in modelProviders. The selected entry supplies the endpoint, envKey and generationConfig.', showInDialog: false, }, authType: { type: 'string', label: 'Batch Auth Type', category: 'Model', requiresRestart: true, default: undefined as string | undefined, description: 'Batch protocol. Defaults to openai; only OpenAI-compatible chat-completions is supported.', showInDialog: false, }, baseUrl: { type: 'string', label: 'Batch Model Base URL', category: 'Model', requiresRestart: true, default: undefined as string | undefined, description: 'Optional exact modelProviders baseUrl to distinguish entries with the same model ID.', showInDialog: false, }, }, }, advisorMaxUses: { type: 'integer', label: 'Advisor Session Call Limit', category: 'Model', requiresRestart: true, default: 0, minimum: 0, description: 'Maximum native Advisor requests per session, shared by the executor and its subagents. Failed requests count. 0 means unlimited. Each request sends the conversation to the selected provider and consumes additional tokens. Only user and system settings apply.', showInDialog: true, }, advisorModel: { type: 'string', label: 'Advisor Model', category: 'Model', requiresRestart: true, default: '' as string, description: 'Model selector for the Advisor tool. Leave empty to disable Advisor. Enabling it sends the active conversation to that model, even when it uses another provider.', showInDialog: false, }, visionModel: { type: 'string', label: 'Vision Model', category: 'Model', requiresRestart: false, default: '', description: 'Image-capable model used as the vision bridge: when a text-only main model receives an image, it is transcribed by this model first. Set with /model --vision. Leave empty to auto-pick a same-provider vision model.', showInDialog: true, }, compactionModel: { type: 'string', label: 'Compaction Model', category: 'Model', requiresRestart: false, default: '', description: 'Model used for chat compression (auto-compaction). Set with /model --compaction. Leave empty to fall back to the main model. A smaller/faster model reduces compression latency and cost.', showInDialog: false, }, imageModel: { type: 'string', label: 'Image Model', category: 'Model', requiresRestart: false, default: '', description: 'Model used by the built-in image_gen tool. Set with /model --image. The selected route must set supportsImageGeneration: true (or legacy imageOnly: true) in modelProviders.', showInDialog: false, }, visionBridgeTimeoutMs: { type: 'integer', label: 'Vision Bridge Timeout (ms)', category: 'Model', // Read once in the Config constructor with no setter, so a mid-session // change only takes effect on restart. requiresRestart: true, default: undefined as number | undefined, minimum: 1, maximum: 2_147_483_647, description: 'Per-attempt timeout in milliseconds for the vision bridge image transcription call (a positive integer up to 2147483647). Unset uses the built-in 30s. Raise for slow or proxied vision endpoints.', showInDialog: false, }, modelFallbacks: { type: 'string', label: 'Model Fallbacks', category: 'Model', requiresRestart: true, default: '', description: 'Ordered list of fallback model IDs (comma-separated, max 3) to try when the primary model hits capacity errors (429/503/529). Example: "qwen-plus,qwen-turbo". Set via CLI with --fallback-model.', showInDialog: true, }, voiceModel: { type: 'string', label: 'Voice Model', category: 'Model', requiresRestart: false, default: '', description: 'Model used for voice transcription. Set with /model --voice. Leave empty to keep voice dictation disabled until a voice model is selected.', showInDialog: false, }, 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, }, baseUrl: { type: 'string', label: 'Model Base URL', category: 'Model', requiresRestart: false, default: undefined as string | undefined, description: 'Base URL paired with model.name; disambiguates which provider to use when multiple modelProviders entries share the same model id.', showInDialog: false, }, reasoningEffort: { type: 'enum', label: 'Reasoning Effort', category: 'Model', requiresRestart: false, default: undefined as string | undefined, description: 'How hard reasoning-capable models think, applied across all providers. Set with /effort. Each provider maps and clamps this to what the active model supports (e.g. Gemini caps at "high"; Anthropic clamps tiers a model lacks). Leave unset to use the model/provider default.', showInDialog: true, options: [ { value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }, { value: 'xhigh', label: 'Extra High' }, { value: 'max', label: 'Max' }, ], // WebShell persists none; the TUI keeps its existing tier-only control. jsonSchemaOverride: { type: 'string', enum: ['none', ...REASONING_EFFORT_TIERS], }, }, maxSessionTurns: { type: 'integer', label: 'Max Session Turns', category: 'Model', requiresRestart: false, default: -1, description: 'Maximum number of user/model/tool turns to keep in a session. Must be an integer; -1 means unlimited.', showInDialog: false, }, maxWallTimeSeconds: { type: 'number', label: 'Max Wall-Clock Time (seconds)', category: 'Model', requiresRestart: false, default: -1, description: 'Run-level wall-clock budget for headless / unattended runs, in seconds. -1 means unlimited; otherwise must be in [1, ~2,147,483] (sub-second values and values above ~24 days are rejected as typos). Overridable per-invocation via --max-wall-time (which also accepts duration suffixes like 5m, 1.5h).', showInDialog: false, }, goalTokenBudget: { type: 'integer', label: 'Goal Token Budget', category: 'Model', requiresRestart: false, default: undefined as number | undefined, description: 'Autonomous spend window armed on each new Goal, in tokens as counted by the Goal meter (totalTokenCount summed over Goal-turn model calls, direct foreground subagents, and Goal verifier/checkpoint checks; nested/background agents, other side queries, cron and notification turns are excluded). When a Goal spends its window it gets one wind-down turn to hand off, then stops until you resume it, which arms another window. Unset uses the built-in default of 30,000,000; -1 means unlimited. Zero, values above 300,000,000 (10x the default, a typo guard), other negative, fractional, or non-number values are rejected at startup.', showInDialog: false, }, goalMaxTurns: { type: 'integer', label: 'Goal Max Turns', category: 'Model', requiresRestart: true, default: undefined as number | undefined, description: 'Goal-turn window armed on each new Goal. Every finished Goal turn counts, including user-driven turns; user turns are still admitted at the ceiling, but they can make the next autonomous continuation a wind-down. A Goal that reaches the ceiling gets one wind-down turn to hand off, then stops until you resume it, which authorizes another window on top of the turns already finished. Unset runs Goals with no turn ceiling, and -1 says so explicitly -- but the opt-out only takes a ceiling off a Goal that has already spent it, on the resume or edit that follows; a Goal still under its ceiling keeps it. A ceiling is armed only on a Goal created after the change, so bounding a Goal already on the record means replacing it with /goal set, which starts a new Goal at revision 1 with its meters reset and its earlier evidence no longer citable, or clearing it and starting again. Zero, values above 10,000, other negative, fractional, or non-number values are rejected at startup. Changes take effect after restart.', showInDialog: false, minimum: -1, maximum: GOAL_MAX_TURNS_CAP, excludedValues: [0], }, goalMaxActiveMinutes: { type: 'integer', label: 'Goal Max Active Minutes', category: 'Model', requiresRestart: true, default: undefined as number | undefined, description: 'Active-time window armed on each new Goal, in minutes of wall time while the Goal remains active, including waits and idle time between turns. Paused, blocked or stopped time does not count, nor does downtime across a restart; a suspended process is still charged. A Goal that reaches the ceiling gets one wind-down turn to hand off, then stops until you resume it, which authorizes another window. The ceiling is read between turns, not by a timer, so a Goal can run well past it before it stops. Active time is measured between recorded transitions, so time in a turn that a restart interrupted is not charged. Unset runs Goals with no time ceiling, and -1 says so explicitly -- but the opt-out only takes a ceiling off a Goal that has already spent it, on the resume or edit that follows. A ceiling is armed only on a Goal created after the change, so bounding a Goal already on the record means replacing it with /goal set, which starts a new Goal at revision 1 with its meters reset and its earlier evidence no longer citable, or clearing it and starting again. Zero, values above 10,080 (one week), other negative, fractional, or non-number values are rejected at startup. Changes take effect after restart.', showInDialog: false, minimum: -1, maximum: GOAL_MAX_ACTIVE_MINUTES_CAP, excludedValues: [0], }, goalCheckpointTimeoutSeconds: { type: 'integer', label: 'Goal Checkpoint Timeout (seconds, deprecated)', category: 'Model', requiresRestart: false, default: undefined as number | undefined, minimum: 1, maximum: 900, description: 'Deprecated. Goals no longer run evidence-checkpoint model calls, because the verifier reads the transcript directly, so this value is ignored. The key is still accepted so that existing settings files keep loading without an unknown-key warning.', showInDialog: false, }, maxToolCalls: { type: 'number', label: 'Max Tool Calls', category: 'Model', requiresRestart: false, default: -1, description: 'Cumulative tool-call budget for a run (counts every executed tool, success or failure; structured_output under --json-schema is exempt). -1 means unlimited; 0 means "no tool calls allowed" (first call aborts). Capped at 1,000,000 to catch typos. Overridable via --max-tool-calls.', showInDialog: false, }, maxSubagentDepth: { type: 'number', label: 'Max Sub-agent Nesting Depth', category: 'Model', requiresRestart: false, default: DEFAULT_MAX_SUBAGENT_DEPTH, description: 'Maximum sub-agent nesting depth (1-based levels: a top-level sub-agent is level 1). 1 keeps sub-agents available but disables nesting; the default 5 allows nesting up to five levels deep. Values clamp to the range 1-100; non-finite values fall back to the default. Teammates, forks, and workflow-spawned agents never nest regardless of this setting. Overridable via --max-subagent-depth.', 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, }, skipWorkflowUsageWarning: { type: 'boolean', label: 'Skip Workflow Usage Warning', category: 'Model', requiresRestart: false, default: false, description: 'Suppress the one-time Workflow tool usage banner that describes the QWEN_CODE_MAX_TOKENS_PER_WORKFLOW env knob. The banner fires at most once per session regardless of this setting.', showInDialog: false, }, skipLoopDetection: { type: 'boolean', label: 'Skip Loop Detection', category: 'Model', requiresRestart: false, default: true, description: 'Skip the opt-in streaming loop-detection heuristics (content/thought repetition, read-file and action stagnation, global-duplicate and alternating tool-call patterns). Defaults to true to avoid false-positive interruptions; set to false to re-enable them as an unattended-run guardrail. Daemon/ACP sessions run none of the other detectors; setting this to false also enables a global-duplicate tool-call halt there. Core-client sessions keep a minimal always-on guard regardless of this setting (consecutive identical tool calls, shell inspection-command stagnation, and a per-turn tool-call cap, see model.maxToolCallsPerTurn); daemon/ACP sessions keep the per-turn tool-call cap and an invalid-tool-params stagnation guard.', showInDialog: false, }, maxToolCallsPerTurn: { type: 'integer', label: 'Max Tool Calls Per Turn', category: 'Model', requiresRestart: false, default: DEFAULT_MAX_TOOL_CALLS_PER_TURN, description: 'Per-turn tool-call cap (one model turn plus its tool-result continuations; blocking Stop-hook continuations and runtime-scheduled Goal turns each start a fresh budget). When set explicitly, this value is a hard cap: the turn halts on the next tool call after it is reached (the released behavior). When left unset (default 100), the cap is adaptive: once the turn exceeds 100 it halts only when the model keeps repeating the same call (a stuck loop); a productive turn (diverse calls) continues up to a hard backstop of 1000, which always halts. The adaptive default applies to the interactive TUI, non-interactive (-p / JSON / stream-JSON) core-client runs, and daemon/ACP sessions alike. Daemon/ACP sessions evaluate the cap once per tool batch, before execution: a batch that would cross an explicit cap or the hard backstop is skipped whole, so a turn never executes past either (it can halt up to one batch short), while the adaptive soft cap is exceeded by design, up to the backstop. They also have no in-session disable. An always-on circuit breaker against runaway turns, independent of model.skipLoopDetection. Set to 0 or a negative value to disable the cap.', 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, }, openAILogRetentionDays: { type: 'number', label: 'OpenAI Log Retention (days)', category: 'Model', // LoadedSettings._merged is cached without verified setValue→recompute // paths in all UI flows (same rationale as general.cleanupPeriodDays). requiresRestart: true, default: DEFAULT_OPENAI_LOG_RETENTION_DAYS, minimum: 0, description: 'Number of days to retain OpenAI API log files written when enableOpenAILogging is on. Completed background housekeeping passes run at most once per day in interactive, headless, stream-json SDK, and ACP sessions. Short-lived non-interactive processes make best-effort progress, while persistent processes scan to completion. Set to 0 for minimum retention (~1 hour). For a custom openAILoggingDir, configure this at user or system scope; workspace-scoped retention is skipped because one directory can be shared by multiple workspaces.', showInDialog: false, }, generationConfig: { type: 'object', label: 'Generation Configuration', category: 'Model', requiresRestart: false, default: undefined as Record | 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, }, streamIdleTimeoutMs: { type: 'integer', label: 'Stream Idle Timeout', category: 'Generation Configuration', requiresRestart: false, default: undefined as number | undefined, description: 'Maximum inactivity between streamed chunks for OpenAI-compatible and Anthropic models, in milliseconds. Set to 0 to disable the idle guard. For provider-backed models, configure this field in the selected modelProviders entry.', minimum: 0, maximum: 2_147_483_647, 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, }, retryInitialDelayMs: { type: 'number', label: 'Retry Initial Delay', category: 'Generation Configuration', requiresRestart: false, default: undefined as number | undefined, description: 'Initial delay in milliseconds for stream rate-limit retries.', minimum: 1, parentKey: 'generationConfig', showInDialog: false, }, retryMaxDelayMs: { type: 'number', label: 'Retry Max Delay', category: 'Generation Configuration', requiresRestart: false, default: undefined as number | undefined, description: 'Maximum delay in milliseconds for stream rate-limit retries.', minimum: 1, parentKey: 'generationConfig', showInDialog: false, }, enableCacheControl: { type: 'boolean', label: 'Enable Cache Control', category: 'Generation Configuration', requiresRestart: false, default: true, description: 'Enable provider prompt-cache controls.', parentKey: 'generationConfig', showInDialog: false, }, forceGlobalCacheScope: { type: 'boolean', label: 'Force Global Cache Scope', category: 'Generation Configuration', requiresRestart: false, default: false, description: "Force scope:'global' on Anthropic cache_control entries even when the base URL is not an Anthropic-native origin (e.g. proxy providers like Routify, OpenRouter). Requires the proxy to forward cache_control fields and the prompt-caching-scope-2026-01-05 beta.", parentKey: 'generationConfig', showInDialog: false, }, cacheRetention: { type: 'enum', label: 'Anthropic Cache Retention', category: 'Generation Configuration', requiresRestart: false, default: undefined as 'ephemeral' | '1h' | undefined, description: "Default Anthropic cache_control retention. 'ephemeral' uses the spec 5-minute default (no ttl on the wire). '1h' requests the extended cache tier (ttl: '1h') -- note the 1h tier writes at 2x base input token cost (vs 1.25x for the 5-minute default; cached reads stay 0.1x for both), so it only pays off when a prefix survives long enough between requests to outlast several 5-minute windows.", parentKey: 'generationConfig', showInDialog: false, options: [ { value: 'ephemeral', label: 'Ephemeral (5m, Default)' }, { value: '1h', label: 'Extended (1h)' }, ], }, cacheRetentionByBlock: { type: 'object', label: 'Anthropic Cache Retention By Block', category: 'Generation Configuration', requiresRestart: false, default: undefined as | Partial< Record<'system' | 'tool' | 'user.last', 'ephemeral' | '1h'> > | undefined, description: "Optional per-anchor override for Anthropic cache retention. Keys (system, tool, user.last) override generationConfig.cacheRetention when present. Resolution is normalized so retention is monotonically non-increasing in wire order (tool -> system -> user.last, per Anthropic's 'longer TTL must precede shorter TTL' rule): setting one anchor to '1h' promotes every anchor before it on the wire to '1h' as well, so any combination here is valid.", parentKey: 'generationConfig', showInDialog: false, jsonSchemaOverride: { type: 'object', properties: { system: { type: 'string', enum: ['ephemeral', '1h'] }, tool: { type: 'string', enum: ['ephemeral', '1h'] }, 'user.last': { type: 'string', enum: ['ephemeral', '1h'] }, }, additionalProperties: false, }, }, splitToolMedia: { type: 'boolean', label: 'Split Tool Result Media', category: 'Generation Configuration', requiresRestart: false, default: true, description: 'When true, media (images / audio / video / files) returned by tool calls — including the built-in read_file and MCP tools — is split into a follow-up user message instead of being embedded in the `role: "tool"` message. The OpenAI Chat Completions spec only permits text on tool messages, so strict OpenAI-compatible servers (e.g., doubao / new-api / LM Studio) silently drop or reject embedded media and the model never sees an image read via read_file (QwenLM/qwen-code#4876, #3616). Default true is spec-compliant and safe for permissive providers; set false only to restore the legacy embed-in-tool-message behavior.', parentKey: 'generationConfig', showInDialog: false, }, toolResultContentFormat: { type: 'enum', label: 'Tool Result Content Format', category: 'Generation Configuration', requiresRestart: false, default: 'parts', description: 'Controls how text-only tool results are serialized in OpenAI-compatible requests. Use "parts" for the default content-part array shape. Use "string" only for legacy OpenAI-compatible runtimes whose tool templates ignore text content parts (for example older GLM-5.1 vLLM/SGLang templates; QwenLM/qwen-code#3361). Tool-returned media is still handled by splitToolMedia.', parentKey: 'generationConfig', showInDialog: false, options: [ { value: 'parts', label: 'Content Parts (Default)' }, { value: 'string', label: 'String' }, ], }, 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, }, }, }, }, }, modelPricing: { type: 'object', label: 'Model Pricing', category: 'Model', requiresRestart: false, default: undefined as | Record< string, { inputPerMillionTokens?: number; outputPerMillionTokens?: number; } > | undefined, description: 'Optional per-model pricing for cost estimation in /stats model. Example: {"qwen3-coder": {"inputPerMillionTokens": 0.30, "outputPerMillionTokens": 1.20}}', 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 or files.', showInDialog: false, jsonSchemaOverride: { anyOf: [ { type: 'string' }, { type: 'array', items: { type: 'string' } }, ], }, }, importFormat: { type: 'enum', label: 'Memory Import Format', category: 'Context', requiresRestart: false, default: undefined as MemoryImportFormat | undefined, description: 'The format to use when importing memory.', showInDialog: false, options: [ { value: 'tree', label: 'Tree' }, { value: 'flat', label: 'Flat' }, ], }, 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 or oversized tool result context. Use -1 to disable a threshold.', showInDialog: false, properties: { 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: 'Integer number of most-recent compactable tool results to preserve when clearing. Values below 1 are floored to 1.', jsonSchemaOverride: { type: 'integer', default: 5, description: 'Integer number of most-recent compactable tool results to preserve when clearing. Values below 1 are floored to 1.', }, showInDialog: false, }, toolResultsTotalCharsThreshold: { type: 'number', label: 'Tool Results Total Chars Threshold', category: 'Context', requiresRestart: false, default: DEFAULT_TOOL_RESULTS_TOTAL_CHARS_THRESHOLD as number, description: 'Total compactable tool result output characters allowed in history before clearing oldest results. When exceeded, oldest results are cleared down to half this threshold (best effort) to preserve the provider prompt cache on later turns. Use -1 to disable. This is a soft threshold: protected recent tool results may keep the total above it.', 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 and configured custom ignore files when searching', showInDialog: true, }, customIgnoreFiles: { type: 'array', label: 'Custom Ignore Files', category: 'Context', requiresRestart: true, default: [...DEFAULT_QWEN_CUSTOM_IGNORE_FILE_NAMES] as string[], description: 'Project-root-relative ignore files to use instead of the defaults (`.agentignore`, `.aiignore`) when respectQwenIgnore is enabled. .qwenignore is always included when respectQwenIgnore is enabled.', showInDialog: false, items: { type: 'string' }, }, 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, }, }, }, autoCompactThreshold: { type: 'number', label: 'Auto-Compact Threshold', category: 'Context', requiresRestart: false, default: undefined as number | undefined, description: 'Target fraction of the context window at which auto-compaction triggers (greater than 0, up to 1). Acts as a ceiling on the trigger: on large windows this is the effective trigger (~85%); on smaller windows compaction may fire earlier to leave room to summarize. Default is 0.85 (85%).', showInDialog: false, jsonSchemaOverride: { type: 'number', minimum: 0.01, maximum: 1, }, }, }, }, memory: { type: 'object', label: 'Memory', category: 'Memory', requiresRestart: false, default: {}, description: 'Settings for managed auto-memory.', showInDialog: false, properties: { mem0: { type: 'object', label: 'Mem0', category: 'Memory', requiresRestart: true, default: undefined as Mem0Settings | undefined, description: 'Bundled Mem0 connection. Configure in user or system settings.', showInDialog: false, jsonSchemaOverride: { type: 'object', additionalProperties: false, required: ['baseUrl'], properties: { baseUrl: { type: 'string', format: 'uri' }, envKey: { type: 'string', pattern: '^[A-Za-z_][A-Za-z0-9_]*$', default: 'MEM0_API_KEY', description: 'Credential environment variable name. Its value may come from the process environment, .env, or the top-level settings env field.', }, credentialEnv: { type: 'string', pattern: '^[A-Za-z_][A-Za-z0-9_]*$', deprecated: true, description: 'Legacy alias for envKey. If both are set, their values must match.', }, protocol: { type: 'string', enum: [ 'mem0-v2', 'mem0-v3', 'mem0-oss-2026-08', 'aliyun-polardb-mysql-2026-08', 'mem0-platform-v3', 'mem0-oss-rest-2026-08', ], default: 'mem0-v2', }, scope: { type: 'object', additionalProperties: false, properties: { userId: { type: 'string', minLength: 1, maxLength: 256 }, agentId: { type: 'string', minLength: 1, maxLength: 256 }, appId: { type: 'string', minLength: 1, maxLength: 256 }, }, }, enableWrites: { type: 'boolean', default: false }, allowInsecureHttp: { type: 'boolean', default: false }, timeoutMs: { type: 'integer', minimum: 1, maximum: 30000, default: 5000, }, }, }, }, enableManagedAutoMemory: { type: 'boolean', label: 'Enable Managed Auto-Memory', category: 'Memory', requiresRestart: false, default: true, description: 'Enable background extraction of memories from conversations.', showInDialog: false, }, enableManagedAutoDream: { type: 'boolean', label: 'Enable Managed Auto-Dream', category: 'Memory', requiresRestart: false, default: true, description: 'Enable automatic consolidation (dream) of collected memories.', showInDialog: false, }, enableAutoSkill: { type: 'boolean', label: 'Enable Auto Skill', category: 'Memory', requiresRestart: false, default: false, description: 'Enable background review for reusable project skills after tool-heavy sessions.', showInDialog: false, }, autoSkillConfirm: { type: 'boolean', label: 'Confirm Auto Skills Before Saving', category: 'Memory', requiresRestart: false, default: true, description: 'Ask for confirmation before auto-generated skills are added to the skill library. When off, auto-skills are saved immediately.', showInDialog: false, }, agentTimeoutMinutes: { type: 'number', label: 'Memory Agent Timeout (minutes)', category: 'Memory', requiresRestart: true, default: undefined as number | undefined, minimum: 0, description: "Max runtime in minutes for background memory agents (extraction, dream, remember, skill review). Unset uses each agent's built-in default (2–5 minutes); 0 disables the time limit. Useful for slow local models that need longer than the defaults.", showInDialog: false, }, agentMaxTurns: { type: 'number', label: 'Memory Agent Max Turns', category: 'Memory', requiresRestart: true, default: undefined as number | undefined, minimum: 0, description: "Max turns for background memory agents (extraction, dream, remember, skill review). Unset uses each agent's built-in default (5–8); 0 disables the turn limit.", showInDialog: false, }, enableTeamMemory: { type: 'boolean', label: 'Enable Team Memory', category: 'Memory', requiresRestart: false, default: false, description: 'Enable a project memory tier shared with collaborators via the git-tracked `.qwen/team-memory/` directory. Off by default; writes to it are secret-scanned and reviewable in the git diff.', showInDialog: false, }, enableTeamMemorySync: { type: 'boolean', label: 'Enable Team Memory Git Sync', category: 'Memory', requiresRestart: false, default: false, description: 'When team memory is enabled, automatically commit, fast-forward-pull, and push the `.qwen/team-memory/` directory at session start so collaborators stay in sync. Off by default; requires a configured git upstream.', showInDialog: false, }, enableStructuredRecall: { type: 'boolean', label: 'Enable Structured Memory Recall', category: 'Memory', requiresRestart: true, default: false, description: 'Switch memory recall from the flat MEMORY.md listing to the structured protocol: hierarchical memory tree, focused subtree, and the search_memory tool. Off by default — while off, the metadata migration that would make the corpus structured-ready is never scheduled, so the protocol costs no background model calls. Override with QWEN_CODE_MEMORY_STRUCTURED_RECALL=0|1.', showInDialog: false, }, }, }, slashCommands: { type: 'object', label: 'Slash Commands', category: 'Advanced', requiresRestart: true, default: {}, description: 'Configuration for slash commands exposed by the CLI. Useful for ' + 'locking down the command surface in multi-tenant or enterprise ' + 'deployments.', showInDialog: false, properties: { disabled: { type: 'array', label: 'Disabled Slash Commands', category: 'Advanced', requiresRestart: true, default: undefined as string[] | undefined, description: 'Slash command names to hide and refuse to execute. Matched ' + 'case-insensitively against the final command name (for extension ' + 'commands this is the disambiguated form, e.g. "myext.deploy"), ' + 'except that a skill command is gated under either spelling — its ' + 'registered name (rust:pdf) or the name its SKILL.md authors (pdf) ' + '— so an entry written before that prefix existed still gates it. ' + 'Merged as a union across settings scopes, so workspace settings ' + 'can add to but not remove entries defined in system/user settings.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, }, }, skills: { type: 'object', label: 'Skills', category: 'Advanced', requiresRestart: false, default: {}, description: 'Configuration for skills (SKILL.md-based capabilities) exposed to ' + 'the model.', showInDialog: false, properties: { disabledLevels: { type: 'array', label: 'Disabled Skill Levels', category: 'Advanced', requiresRestart: true, default: undefined as string[] | undefined, description: 'Skill discovery levels to skip entirely. Supported levels are ' + 'project, user, extension, and bundled. UNION-merged across settings ' + 'scopes.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, items: { type: 'string', enum: ['project', 'user', 'extension', 'bundled'], }, }, disabled: { type: 'array', label: 'Disabled Skills', category: 'Advanced', requiresRestart: false, default: undefined as string[] | undefined, description: 'Skill names to hide. Matched case-insensitively against the skill ' + 'name. Hidden skills do not appear in or as ' + '/ slash commands. An extension skill matches under either its ' + 'registered name (rust:pdf) or the name its SKILL.md authors (pdf), ' + 'so an entry written before that prefix existed still blocks it. ' + 'UNION-merged across systemDefaults/user/' + 'workspace/system scopes — workspace cannot remove entries defined ' + 'in higher scopes.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, defaultDisabled: { type: 'array', label: 'Default Disabled Skills', category: 'Advanced', requiresRestart: false, default: undefined as string[] | undefined, description: 'Skill names disabled by default unless explicitly enabled through ' + 'skills.enabled. Matched case-insensitively and UNION-merged across ' + 'settings scopes. An extension skill is disabled under either its ' + 'registered name (rust:pdf) or the name its SKILL.md authors (pdf). ' + 'skills.disabled always wins; skills.enabled cancels an entry here ' + 'only when the two lists spell the name the same way.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, enabled: { type: 'array', label: 'Enabled Skills', category: 'Advanced', requiresRestart: false, default: undefined as string[] | undefined, description: 'Explicit opt-ins, matched against the skill name as registered — ' + 'an extension skill is rust:pdf there. An entry spelled as the ' + 'registered name overrides a matching skills.defaultDisabled ' + 'entry and, for an extension skill, both the default the owning ' + 'extension declares and the enablement stored for this workspace. ' + 'A bare pdf entry never matches as a grant; it only cancels an ' + 'identically-spelled skills.defaultDisabled entry, and once ' + 'cancelled the enablement stored for this workspace decides, else ' + 'the default the owning extension declares. Matched ' + 'case-insensitively and UNION-merged across settings scopes. Cannot ' + 'override skills.disabled or re-enable skills from a ' + 'skills.disabledLevels-excluded level.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, directories: { type: 'array', label: 'Skill Directories', category: 'Advanced', requiresRestart: true, default: undefined as string[] | undefined, description: 'Additional directories to scan for skills (SKILL.md files). ' + 'Entries should be absolute paths or ~-prefixed; relative paths ' + 'resolve against the working directory. Each directory is scanned ' + 'one level deep for subdirectories containing a SKILL.md file. ' + 'Skills from these directories are loaded at user level, after ' + 'the default user skill directories; a custom skill with the ' + 'same name as one in the default user directories will not ' + 'override it. Only point this at trusted locations, since ' + 'skills can define hooks and commands.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, }, }, 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, }, autoMode: { type: 'object', label: 'Auto Mode', category: 'Tools', requiresRestart: true, default: {}, description: 'Settings consumed by the AUTO approval mode classifier.', showInDialog: false, properties: { classifier: { type: 'object', label: 'Auto Mode Classifier', category: 'Tools', requiresRestart: true, default: {}, description: 'Runtime controls for the AUTO approval mode classifier.', showInDialog: false, properties: { timeouts: { type: 'object', label: 'Auto Mode Classifier Timeouts', category: 'Tools', requiresRestart: true, default: {}, description: 'Timeouts for the two AUTO classifier stages, in milliseconds.', showInDialog: false, properties: { stage1Ms: { type: 'number', label: 'Auto Mode Stage 1 Timeout', category: 'Tools', requiresRestart: true, default: undefined as number | undefined, description: 'Timeout in milliseconds for the fast stage-1 AUTO classifier.', showInDialog: false, }, stage2Ms: { type: 'number', label: 'Auto Mode Stage 2 Timeout', category: 'Tools', requiresRestart: true, default: undefined as number | undefined, description: 'Timeout in milliseconds for the stage-2 AUTO classifier review.', showInDialog: false, }, }, }, thinking: { type: 'object', label: 'Auto Mode Classifier Thinking', category: 'Tools', requiresRestart: true, default: {}, description: 'Provider/API-level thinking controls for the AUTO classifier.', showInDialog: false, properties: { stage2Enabled: { type: 'boolean', label: 'Auto Mode Stage 2 Thinking', category: 'Tools', requiresRestart: true, default: false, description: 'Whether stage 2 may use provider/API-level thinking. Stage 1 always keeps thinking disabled.', showInDialog: false, }, }, }, }, }, hints: { type: 'object', label: 'Classifier Hints', category: 'Tools', requiresRestart: true, default: {}, description: 'Natural-language hints injected into the classifier system prompt.', showInDialog: false, properties: { allow: { type: 'array', label: 'Auto Mode Allow Hints', category: 'Tools', requiresRestart: true, default: undefined as string[] | undefined, description: 'Natural-language descriptions of actions AUTO mode should allow.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, softDeny: { type: 'array', label: 'Auto Mode Soft-Deny Hints', category: 'Tools', requiresRestart: true, default: undefined as string[] | undefined, description: 'Natural-language descriptions of destructive / irreversible ' + 'actions AUTO mode should block unless the user explicitly ' + 'authorised that exact action and scope.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, hardDeny: { type: 'array', label: 'Auto Mode Hard-Deny Hints', category: 'Tools', requiresRestart: true, default: undefined as string[] | undefined, description: 'Natural-language descriptions of security-boundary actions ' + 'the AUTO classifier must block even when an autoMode ' + 'allow hint or recent user request would normally ' + 'authorise them. Does not override permissions.allow; use ' + 'permissions.deny for deterministic hard permission rules.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, deny: { type: 'array', label: 'Auto Mode Deny Hints (legacy)', category: 'Tools', requiresRestart: true, default: undefined as string[] | undefined, description: 'Deprecated alias for `softDeny`. Entries here are merged ' + 'into the SOFT BLOCK user section so existing settings keep ' + 'working; new configurations should use `softDeny` or ' + '`hardDeny` instead.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, }, }, environment: { type: 'array', label: 'Auto Mode Environment', category: 'Tools', requiresRestart: true, default: undefined as string[] | undefined, description: 'Environment / context lines injected into the classifier system prompt.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, classifyAllShell: { type: 'boolean', label: 'Classify All Shell Commands', category: 'Tools', requiresRestart: true, default: false, description: 'Route ALL shell commands through the auto-mode classifier, ' + 'including read-only commands that would otherwise be ' + 'auto-approved. Provides defense-in-depth for production ' + 'environments.', showInDialog: false, }, mcp: { type: 'object', label: 'Auto Mode MCP Tools', category: 'Tools', requiresRestart: true, default: {}, description: 'AUTO classifier controls for third-party MCP tools.', showInDialog: false, properties: { forwardArguments: { type: 'boolean', label: 'Forward MCP Arguments To Classifier', category: 'Tools', requiresRestart: true, default: true, description: 'Forward MCP tool arguments (bounded and truncated) to the ' + 'AUTO classifier so it can judge what the agent is about ' + 'to send to the server. When false the classifier sees ' + 'only the tool name, which usually results in a ' + 'conservative block.', showInDialog: false, }, }, }, }, }, }, }, tools: { type: 'object', label: 'Tools', category: 'Tools', requiresRestart: true, default: {}, description: 'Settings for built-in and custom tools.', showInDialog: false, properties: { executionSandbox: { type: 'object', label: 'Tool Execution Sandbox', category: 'Tools', requiresRestart: true, default: undefined as | import('./execution-sandbox-settings.js').ExecutionSandboxSettings | undefined, description: 'Linux tool execution confinement. Operator scopes only; workspace settings cannot override it. Model/auth/session traffic stays on the host.', showInDialog: false, jsonSchemaOverride: { type: 'object', required: ['filesystem', 'network'], additionalProperties: false, properties: { backend: { type: 'string', enum: ['auto', 'bwrap', 'landlock'], }, filesystem: { type: 'string', enum: ['read-only', 'workspace-write'], }, network: { type: 'string', enum: ['open', 'closed'] }, }, }, }, codeModeOnly: { type: 'boolean', label: 'Code Mode Only (Experimental)', category: 'Tools', requiresRestart: true, default: false, description: 'Expose ordinary tools through the isolated exec JavaScript tool. Load deferred descriptions and schemas on demand with tool_search; if search is unavailable, include all allowed signatures in exec. Direct control tools remain available. Ignored in safe and bare modes.', showInDialog: true, }, freeform: { type: 'boolean', label: 'Freeform Tool Input (Experimental)', category: 'Tools', requiresRestart: true, default: false, description: 'Use raw text input for the Code Mode exec tool on OpenAI Responses models. Effective only when tools.codeModeOnly is true and the selected model uses wireApi "responses". Enable only for endpoints that support Responses Custom Tools.', showInDialog: false, }, 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, jsonSchemaOverride: { anyOf: [{ type: 'boolean' }, { type: 'string' }], }, }, 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, }, webSearch: { type: 'object', label: 'Web Search', category: 'Tools', requiresRestart: true, default: {}, description: 'Settings for the built-in WebSearch tool (DashScope Responses API backend). On by default at startup for Alibaba ModelStudio Standard API Key / Token Plan and OpenAI-compatible entries on recognized DashScope Responses hosts with a direct key; set enabled=false to turn it off. Which providers can activate the tool is decided at startup; once active, the search backend follows the currently selected model on the next search. Fully env-configurable for environments without settings.json: ENABLE_WEB_SEARCH, WEB_SEARCH_MODEL, WEB_SEARCH_BASE_URL, WEB_SEARCH_API_KEY (falls back to DASHSCOPE_API_KEY), WEB_SEARCH_EXTRACTOR, WEB_SEARCH_TIMEOUT_MS, WEB_SEARCH_MAX_PER_SESSION. Note: baseUrl and API key are env-only (WEB_SEARCH_BASE_URL / WEB_SEARCH_API_KEY) and cannot be set in settings.json.', showInDialog: false, properties: { enabled: { type: 'boolean', label: 'Enable WebSearch', category: 'Tools', requiresRestart: true, default: undefined as boolean | undefined, description: 'Set false to disable the built-in web_search tool. Automatic startup activation requires leaving enabled, model, and the env-only backend unset. Setting true permits automatic derivation only when the env-only backend is also unset; otherwise a model is required. Env override: ENABLE_WEB_SEARCH.', showInDialog: true, }, model: { type: 'string', label: 'Search Model', category: 'Tools', requiresRestart: true, default: undefined as string | undefined, description: 'Model selector for the explicit search path ("modelId" or "authType:modelId"). With WEB_SEARCH_BASE_URL it is the plain model id for that endpoint; otherwise it must match a DashScope-compatible modelProviders entry with an envKey. The automatic path uses qwen3.8-flash. Env override: WEB_SEARCH_MODEL.', showInDialog: true, }, webExtractor: { type: 'boolean', label: 'Open Result Pages', category: 'Tools', requiresRestart: true, default: true, description: 'Let the search agent open and read result pages (DashScope web_extractor) for better-grounded answers. Billed separately by DashScope. Env override: WEB_SEARCH_EXTRACTOR.', showInDialog: true, }, timeoutMs: { type: 'integer', label: 'Search Timeout (ms)', category: 'Tools', requiresRestart: true, default: undefined as number | undefined, minimum: 1, maximum: MAX_WEB_SEARCH_TIMEOUT_MS, description: `Total time budget for one web_search call, in milliseconds (default ${DEFAULT_WEB_SEARCH_TIMEOUT_MS}, max ${MAX_WEB_SEARCH_TIMEOUT_MS}; other values fall back to the default). The search agent runs several queries and may open result pages; a search that exceeds the budget returns whatever arrived as a partial result once at least one search call has completed — if the budget expires before the first search call finishes, the tool reports a timeout error instead, because narration with no executed search is not auditable evidence. A per-tool execution cap (QWEN_CODE_TOOL_EXECUTION_TIMEOUT_MS) below this budget fires first and discards the partial result; keep it above timeoutMs. Env override: WEB_SEARCH_TIMEOUT_MS.`, showInDialog: true, }, maxPerSession: { type: 'integer', label: 'Max Searches per Session', category: 'Tools', requiresRestart: true, default: undefined as number | undefined, minimum: 1, maximum: MAX_WEB_SEARCH_MAX_PER_SESSION, description: `Maximum web_search calls in one session (default ${DEFAULT_WEB_SEARCH_MAX_PER_SESSION}, max ${MAX_WEB_SEARCH_MAX_PER_SESSION}; other values fall back to the default). The count is shared with subagents and resets when the session changes (/clear, /resume, branching). Once it is reached, further searches are skipped and the model is told to continue with what it has gathered. Env override: WEB_SEARCH_MAX_PER_SESSION.`, showInDialog: true, }, }, }, toolSearch: { type: 'object', label: 'Tool Search', category: 'Tools', requiresRestart: true, default: {}, description: 'Settings for deferred-tool discovery and invocation.', showInDialog: false, properties: { enabled: { type: 'boolean', label: 'Enable ToolSearch', category: 'Tools', requiresRestart: true, default: true, description: "When enabled, deferred tools are reviewed via ToolSearch and invoked through the stable ToolCall bridge. Bridge review and invocation keep the tool list stable — the bridge never re-declares what it reveals — reducing prompt size without touching the prompt-cache prefix. The declaration list is not immutable, though: a session still re-declares on resume, whenever a tool-set refresh (MCP discovery, the first plan-mode entry in a session, a subagent definition change) finds a direct call to a still-hidden deferred tool in the live history, when a subagent definition change rewrites the agent tool's own description, and when an MCP server registers mid-session with alwaysLoadTools: true.", showInDialog: true, }, threshold: { type: 'number', label: 'Deferred Tool Preload Threshold (%)', category: 'Tools', requiresRestart: true, default: 0, description: 'Context-window percentage used as the session-start budget for preloading ordinary deferred tools (bundled built-ins and MCP alike). Defaults to 0, which performs no threshold-based preload; ordinary deferred tools normally stay behind the stable ToolSearch + ToolCall bridge, at the cost of one tool_search round trip before first use. Raise it to N so that, when every eligible deferred schema fits within N% of the context window, all are declared upfront for direct calls with no bridge round trip; otherwise they stay behind the bridge while both bridge tools are registered. Tools demoted by tools.eager are excluded from this preload and stay reachable on demand through that bridge while it is registered; when either bridge tool is unregistered (tools.toolSearch.enabled false denies both; a tool_search or tool_call deny rule removes one) the demoted tools that remain hidden are not offered to the model and cannot be reached through the bridge for that session, and a warning is logged; these bridge and warning rules apply to direct tool mode. CodeModeOnly discovers deferred schemas through top-level tool_search and invokes them through exec. It skips deferred preload and startup catalogs; tools.eager also reduces the initial exec description. When search is unavailable in the current scope, exec includes all allowed tool signatures. In direct mode they stay registered, so a direct call by their own name is still evaluated and approved normally. Separate paths can still declare deferred tools at 0: tools.visible; the live-history compatibility scan on every tool-set refresh (including resume, MCP discovery, first plan-mode entry, and subagent definition changes); the incomplete-bridge eager fallback; and daemon ACP late registration, which explicitly reveals and pins create_sub_session.', showInDialog: true, // A percentage of the context window: values above 100 would set a // budget larger than the window and unconditionally preload every // deferred tool, defeating the point of the threshold. Bound it the // way autoCompactThreshold bounds its fraction. jsonSchemaOverride: { type: 'number', minimum: 0, maximum: 100, default: 0, }, }, }, }, listDirectory: { type: 'object', label: 'List Directory', category: 'Tools', requiresRestart: true, default: {}, description: 'Settings for the built-in list_directory tool. Opt-in: the tool is disabled by default because glob covers directory listing in most cases.', showInDialog: false, properties: { enabled: { type: 'boolean', label: 'Enable ListDirectory', category: 'Tools', requiresRestart: true, default: false, description: 'Enable the built-in list_directory tool. Disabled by default; it is also re-enabled automatically when explicitly listed in the coreTools allowlist (--core-tools / tools.core).', showInDialog: true, }, }, }, todoWrite: { type: 'object', label: 'Todo Write', category: 'Tools', requiresRestart: true, default: {}, description: 'Settings for the built-in todo_write tool. Opt-in: the tool is disabled by default.', showInDialog: false, properties: { enabled: { type: 'boolean', label: 'Enable Todo Write', category: 'Tools', requiresRestart: true, default: false, description: 'Enable the built-in todo_write tool and its system-prompt guidance.', showInDialog: true, }, }, }, 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: undefined as boolean | undefined, description: 'Use node-pty for an interactive shell experience. Explicit one-shot prompts default to child_process when this setting is unset; interactive and input-driven modes default to PTY.', showInDialog: true, }, pager: { type: 'string', label: 'Pager', category: 'Tools', requiresRestart: false, default: undefined as string | undefined, description: 'The pager command to use for shell output. Defaults to `cat` on non-Windows platforms and unset on Windows. Set to an empty string to disable pager environment variables.', showInDialog: false, }, showColor: { type: 'boolean', label: 'Show Color', category: 'Tools', requiresRestart: false, default: false, description: 'Show color in shell output.', showInDialog: false, }, defaultTimeoutMs: { type: 'integer', minimum: 0, maximum: 600000, label: 'Default Command Timeout (ms)', category: 'Tools', requiresRestart: true, default: undefined as number | undefined, description: 'Default timeout, in milliseconds, for foreground shell commands started by the agent. A per-call timeout on the shell tool overrides this. When unset, foreground commands time out after 120000 ms (2 minutes). Set to 0 to disable the timeout.', showInDialog: false, }, heartbeatIntervalMs: { type: 'integer', minimum: 0, maximum: 600000, label: 'Silent Command Heartbeat Interval (ms)', category: 'Tools', requiresRestart: true, default: undefined as number | undefined, description: 'Interval, in milliseconds, between liveness heartbeats emitted while a foreground shell command produces no output. Heartbeats are forwarded to ACP clients and stream-json consumers so they can tell a silent command from a dead session. When unset, heartbeats fire every 10000 ms (10 seconds). Set to 0 to disable heartbeats.', showInDialog: false, }, }, }, // Legacy tool permission fields – kept for backward compatibility. core: { type: 'array', label: 'Core Tools (deprecated)', category: 'Tools', requiresRestart: true, default: undefined as string[] | undefined, description: 'Deprecated. permissions.allow cannot reproduce this registration restriction because it only auto-approves calls. Use tools.eager to defer unlisted eager-by-default tools or permissions.deny to remove tools. An empty list is treated as unset and disables nothing.', 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, }, disabled: { type: 'array', label: 'Disabled Tools', category: 'Tools', requiresRestart: true, default: undefined as string[] | undefined, description: 'Tool names hidden from the registry. Differs from permissions.deny: disabled tools are not registered at all, so they never appear in /tools and cannot be discovered by the model. Managed by the daemon mutation route POST /workspace/tools/:name/enable.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, visible: { type: 'array', label: 'Visible Deferred Tools', category: 'Tools', requiresRestart: true, default: undefined as string[] | undefined, description: 'Deferred tool names made visible at startup without requiring the ToolSearch + ToolCall bridge. Listed tools appear alongside core tools in the initial session.', showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, eager: { type: 'array', label: 'Eager Tool Schemas', category: 'Tools', requiresRestart: true, default: undefined as string[] | undefined, description: 'Allowlist of eager-by-default built-in tool names whose schemas remain eligible for the initial model request. Unlisted non-exempt tools are deferred but stay registered, listed in /tools, and reachable through the tool_search + tool_call bridge. Tools already deferred by default stay on demand even when listed; use tools.visible to surface one at startup. tool_search, tool_call, structured_output, plan-mode lifecycle tools, task_stop, MCP tools, and computer_use__* tools are unaffected. An explicitly empty list ([]) defers every non-exempt eager-by-default tool; omit the setting for no restriction. Pairs with the ToolSearch + ToolCall bridge: when either half is not registered — tools.toolSearch.enabled false (which denies both), a tool_search or tool_call deny rule, or a tools.disabled entry — the allowlist still withholds the schemas, but nothing can load them back, so the demoted tools that remain hidden are not offered to the model and cannot be reached through the bridge for that session, and a warning is logged; these bridge and warning rules apply to direct tool mode. CodeModeOnly discovers deferred schemas through top-level tool_search and invokes them through exec. It skips deferred preload and startup catalogs; tools.eager also reduces the initial exec description. When search is unavailable in the current scope, exec includes all allowed tool signatures. In direct mode they stay registered, so a direct call by their own name is still evaluated and approved normally — except tools also listed in tools.visible, which are declared upfront, and sessions whose live history contains a direct call to a still-hidden demoted tool, which any tool-set refresh (resume, MCP discovery, the first plan-mode entry in a session, a subagent definition change) re-declares. Differs from tools.disabled, which removes tools entirely, and from permissions.allow, which only auto-approves calls.', showInDialog: false, }, approvalMode: { type: 'enum', label: 'Tool Approval Mode', category: 'Tools', requiresRestart: false, default: ApprovalMode.AUTO, 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: 'Ask permissions' }, { value: ApprovalMode.AUTO_EDIT, label: 'Auto Edit' }, { value: ApprovalMode.AUTO, label: 'Auto' }, { 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, }, workflowsEnabled: { type: 'boolean', label: 'Dynamic Workflows', category: 'Tools', // The Workflow tool is registered once while building the tool // registry and /workflows is gated when commands load. Keyword // steering reads the same startup-built Config on each submission, // so changing the settings file mid-session cannot update any of the // three surfaces until the next launch. requiresRestart: true, default: false, description: 'Enable the Workflow tool, which lets the model author and run a script that orchestrates subagents in parallel. Off by default; a run can dispatch many subagents and spend tokens accordingly. The QWEN_CODE_ENABLE_WORKFLOWS=1 and QWEN_CODE_DISABLE_WORKFLOWS=1 environment variables override this setting (disable wins). Unrelated to the Session Workflow plan-and-review view; to stop the "workflow" keyword from steering a turn, see Disable Workflow Keyword Trigger.', showInDialog: true, }, workflowSizeGuideline: { type: 'enum', label: 'Dynamic Workflow Size', category: 'Tools', // Read on the next prompt: the change is announced to the model then, // and runs started afterwards use the new agent threshold. requiresRestart: false, default: 'medium', description: 'Advisory size guideline for the dynamic workflows the model writes: "small" aims for fewer than 5 agents, "medium" (the default) fewer than 15, "large" fewer than 50, and "unrestricted" sends no guideline. It is a guideline, not an enforced limit. It also sets the agent count at which a running workflow is flagged as large (QWEN_CODE_WORKFLOW_SIZE_WARNING_AGENTS overrides that threshold). A change takes effect from your next message.', showInDialog: true, options: [ { value: 'small', label: 'Small (under 5 agents)' }, { value: 'medium', label: 'Medium (under 15 agents)' }, { value: 'large', label: 'Large (under 50 agents)' }, { value: 'unrestricted', label: 'Unrestricted (no guideline)' }, ], }, workflowNameOnly: { type: 'boolean', label: 'Named Workflows Only', category: 'Tools', // The Workflow tool builds its description and parameter schema from // this once, while the tool registry is built. requiresRestart: true, default: false, description: 'Restrict the model to running named workflows: saved workflows and the workflows extensions ship, called by name. The model cannot run an inline script or a script path, and a running script cannot nest one by path, so every run the model starts can be matched by a Workflow(name:...) permission rule. It does not replace an approval policy: the model can still save a new workflow file and run it by name, which an approval rule scoped to specific names or script digests will ask about. Runs a host starts over ACP (run-saved, run-script, retry, rerun) are not restricted. QWEN_CODE_WORKFLOW_NAME_ONLY=1 turns it on too. A workspace may set this to true only.', showInDialog: true, }, 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, }, toolOutputBatchBudget: { type: 'number', label: 'Tool Output Batch Budget', category: 'General', requiresRestart: true, default: DEFAULT_TOOL_OUTPUT_BATCH_BUDGET, description: 'Per-message character budget for the combined text output of one batch of tool calls. Oversized batches are reduced deterministically and recoverable output is persisted when possible. Set to -1 to disable.', showInDialog: false, }, }, }, policy: { type: 'object', label: 'Daemon Policy', category: 'Daemon', requiresRestart: true, default: {}, description: 'Daemon multi-client coordination policies. Tool-level allow/deny rules ' + 'live under `permissions`; this section is for runtime mediation behavior ' + 'between concurrent HTTP clients sharing one `qwen serve` daemon.', showInDialog: false, properties: { permissionStrategy: { type: 'enum', label: 'Permission Mediation Policy', category: 'Daemon', requiresRestart: true, default: 'first-responder', description: 'How permission requests resolve when multiple clients are attached. ' + '`first-responder` (default) = any client decides, first wins. ' + '`designated` = only the prompt originator decides; falls back to ' + 'first-responder if originator is anonymous. ' + 'NOTE: client identity comes from self-declared X-Qwen-Client-Id ' + 'with no proof-of-possession (pair-token identity is not implemented yet), ' + 'so any client observing originatorClientId on SSE frames can ' + 'register with the same id and impersonate the originator. ' + '`consensus` = N-of-M voters must agree. Default N=floor(M/2)+1, ' + 'which means UNANIMITY for M=2 (quorum=2, both must agree) and ' + 'supermajority for larger even M (M=4 → quorum=3; M=6 → quorum=4). ' + 'For M=2 specifically, split votes resolve only via the configured ' + 'permission timeout, voter cancellation, or session cancellation. ' + '`local-only` = only loopback clients can RESOLVE; remote clients ' + 'can still ABORT a pending permission via the cancel sentinel ' + '({outcome:"cancelled"}) — cancel stays cross-policy for ' + 'consistency. Strict-cancel-too deployments need a dedicated ' + 'loopback-bound daemon. ' + 'Requires daemon restart — read once at boot.', showInDialog: true, options: [ { value: 'first-responder', label: 'First Responder' }, { value: 'designated', label: 'Designated Originator' }, { value: 'consensus', label: 'Consensus Quorum' }, { value: 'local-only', label: 'Local Only' }, ], }, consensusQuorum: { type: 'number', label: 'Consensus Quorum Override', category: 'Daemon', requiresRestart: true, default: undefined as number | undefined, description: 'Optional fixed quorum size for consensus policy. Capped at M ' + '(count of registered voters at request issue time) to prevent ' + 'unreachable quorum. Unset = floor(M/2)+1. ' + 'Requires daemon restart — read once at boot.', showInDialog: false, // run-qwen-serve.ts validates `Number.isInteger(n) && n >= 1` and // refuses to boot otherwise. Override the generated schema so IDE // (VSCode, JetBrains via JSON Schema) flags `0`, `-1`, `1.5` // BEFORE the user restarts the daemon. The bare `type:'number'` // mapping accepts all of these. jsonSchemaOverride: { type: 'integer', minimum: 1, description: 'Optional fixed quorum size for consensus policy. Capped at M ' + '(count of registered voters at request issue time) to prevent ' + 'unreachable quorum. Unset = floor(M/2)+1. ' + 'Requires daemon restart — read once at boot.', }, }, }, }, 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', // Hot-reloadable (sub-task 3): read by mcpGatingEqual and re-applied // live during reconcile. See mcpServers above for why this must not be // restart-required. requiresRestart: false, default: undefined as string[] | undefined, description: 'A list of MCP servers to allow. Supports glob patterns (e.g. "*puppeteer*").', showInDialog: false, mergeStrategy: MergeStrategy.CONCAT, }, excluded: { type: 'array', label: 'Exclude MCP Servers', category: 'MCP', // Hot-reloadable (sub-task 3): read by mcpGatingEqual and re-applied // live during reconcile. See mcpServers above for why this must not be // restart-required. requiresRestart: false, default: undefined as string[] | undefined, description: 'A list of MCP servers to exclude. Supports glob patterns (e.g. "*puppeteer*"). Takes precedence over mcp.allowed.', showInDialog: false, mergeStrategy: MergeStrategy.CONCAT, }, toolIdleTimeoutMs: { type: 'number', label: 'MCP Tool Idle Timeout (ms)', category: 'MCP', requiresRestart: false, default: 300000, minimum: 10000, maximum: 3600000, description: 'Idle timeout in milliseconds for MCP tool calls. If the MCP server does not produce any response or progress update within this time, the call is aborted. Default: 300000 (5 minutes). Can be overridden via QWEN_CODE_MCP_TOOL_IDLE_TIMEOUT_MS environment variable.', showInDialog: false, }, }, }, 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: false, 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). A value in Workspace settings is honored only when no User, System, or SystemDefaults scope sets one, so a cloned repository can narrow but never replace your whitelist.', showInDialog: false, items: { type: 'string', description: 'URL pattern (supports * wildcard)', }, }, allowPrivateNetworkHooks: { type: 'boolean', label: 'Allow Private Network Hooks', category: 'Security', requiresRestart: false, default: false, description: 'When true, HTTP hooks may target private/link-local IP ranges (the SSRF IP-range checks are skipped). Cloud metadata hostnames (e.g. 169.254.169.254, metadata.google.internal) remain blocked. Only honored from User, System, and SystemDefaults settings scopes; values set in Workspace settings are ignored so a cloned repository cannot self-grant this bypass. Enable only in trusted, managed environments, and pair with security.allowedHttpHookUrls.', showInDialog: false, }, allowedInsecureVoiceBaseUrls: { type: 'array', label: 'Allowed Insecure Voice Base URLs', category: 'Security', requiresRestart: false, default: [] as string[], description: 'Complete voice base URLs that may use HTTP or private-network addresses. Entries must include an explicit http:// or https:// scheme and the full provider path; only URL serialization and trailing slashes are normalized. Wildcards are not supported; metadata, link-local, local-use NAT64, 6to4, and Teredo addresses remain blocked even when listed, as do hostnames that resolve to loopback; IPv4-mapped, IPv4-compatible, and well-known NAT64 (64:ff9b::/96) literals are classified by their embedded IPv4 address. Only honored from User, System, and SystemDefaults settings scopes; values set in Workspace settings are ignored. Enable only for trusted endpoints in managed private networks. Cleartext HTTP also exposes the provider API key transmitted in the Authorization header. An allowlisted hostname is only as trustworthy as its DNS; prefer IP-literal entries when the gateway address is stable.', showInDialog: false, items: { type: 'string', description: 'Complete voice provider base URL with explicit scheme and full path (no wildcards)', }, }, }, }, 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: 'enum', label: 'DNS Resolution Order', category: 'Advanced', requiresRestart: true, default: undefined as DnsResolutionOrder | undefined, description: 'The DNS resolution order.', showInDialog: false, options: [ { value: 'ipv4first', label: 'IPv4 First' }, { value: 'verbatim', label: 'Verbatim' }, ], }, 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 (or QWEN_HOME if set). Env var QWEN_RUNTIME_DIR takes priority.', showInDialog: false, }, }, }, goals: { type: 'object', label: 'Goals', category: 'Advanced', requiresRestart: true, default: {}, description: 'Settings for session Goals (/goal).', showInDialog: false, properties: { modelProposed: { type: 'enum', label: 'Model-Proposed Goals', category: 'Advanced', requiresRestart: true, default: 'alwaysAsk', description: 'Controls the propose_goal tool, which lets the model propose a session Goal for you to approve. "alwaysAsk" (default) shows every proposal in an approval dialog and nothing is set until you accept it; "disabled" removes the tool. A typed /goal is unaffected. Consent-affecting, so this setting is only honored from User, System, or SystemDefaults scope; workspace values are ignored.', showInDialog: true, options: [ { value: 'alwaysAsk', label: 'Always ask' }, { value: 'disabled', label: 'Disabled' }, ], }, }, }, agents: { type: 'object', label: 'Agents', category: 'Advanced', requiresRestart: false, default: {}, description: 'Settings for built-in agents and multi-agent collaboration features (Arena, Team, Swarm).', showInDialog: false, properties: { builtin: { type: 'object', label: 'Built-in Agents', category: 'Advanced', requiresRestart: true, default: {}, description: 'Settings for built-in subagents.', showInDialog: false, properties: { exploreModel: { type: 'string', label: 'Explore Model', category: 'Model', requiresRestart: true, default: 'inherit' as string, description: 'Model selector for the built-in Explore subagent. Use "inherit" for the main session model, "fast" for fastModel, a model ID, or an authType:model-id selector. Custom same-name agents are unaffected.', showInDialog: false, }, }, }, crossSessionMessaging: { type: 'boolean', label: 'Cross-Session Messaging', category: 'Advanced', requiresRestart: true, default: true, description: 'Let Qwen Code sessions on this machine send each other messages over a per-session local socket. On by default: this session is discoverable by the others, takes peer messages under the review rules of agents.crossSessionInbound, and its model can address them from send_message. Two senders are delivered without review unless agents.crossSessionInbound is "hold" or "refuse": processes this session starts, which inherit its child token, and a same-user process that claims this session\'s own review class, which nothing authenticates. Set to false to keep this session invisible and unreachable.', showInDialog: false, }, crossSessionInbound: { type: 'enum', label: 'Inbound Cross-Session Messages', category: 'Advanced', requiresRestart: false, default: undefined as string | undefined, description: 'What happens to inbound cross-session messages. "accept" delivers them; "hold" parks them for your review without letting the model act; "refuse" opts this session out. Unset means user-minted controllers and this session\'s own child processes auto-deliver, while other sessions use review-class parity: both must review every action, or both must declare a mode that can apply actions without per-action review. Other messages are held for you to review.', showInDialog: false, options: [ { value: 'accept', label: 'Accept' }, { value: 'hold', label: 'Hold for review' }, { value: 'refuse', label: 'Refuse' }, ], }, crossSessionHeldExpiry: { type: 'enum', label: 'Held Message Expiry', category: 'Advanced', requiresRestart: false, default: '5m' as string, description: 'How long a message held for your review waits before it expires and the sending session is told nobody answered. "never" keeps held messages until the session ends. Only affects messages that are held; accepted and refused ones are settled on arrival.', showInDialog: false, options: [ { value: '1m', label: '1 minute' }, { value: '5m', label: '5 minutes' }, { value: '10m', label: '10 minutes' }, { value: 'never', label: 'Never' }, ], }, modelGrades: { type: 'object', label: 'Model Grades', category: 'Model', requiresRestart: true, default: undefined as Record | undefined, description: 'Maps semantic model grade names exposed to the Agent tool to concrete model selectors.', showInDialog: false, mergeStrategy: MergeStrategy.SHALLOW_MERGE, jsonSchemaOverride: { type: 'object', additionalProperties: { type: 'string' }, }, }, allowedGrades: { type: 'array', label: 'Allowed Model Grades', category: 'Model', requiresRestart: true, default: undefined as string[] | undefined, description: 'Optional whitelist of model grade names the Agent tool may use.', showInDialog: false, items: { type: 'string' }, }, maxParallelAgents: { type: 'number', label: 'Max Parallel Agents', category: 'Advanced', requiresRestart: true, default: undefined as number | undefined, minimum: 1, description: 'Global maximum number of background sub-agents that can run concurrently. Additional background agents wait in a queue until a slot is available. Use maxParallelAgentsByModel to cap a specific model below this global limit.', showInDialog: false, jsonSchemaOverride: { type: 'integer', minimum: 1, }, }, maxParallelAgentsByModel: { type: 'object', label: 'Max Parallel Agents Per Model', category: 'Advanced', requiresRestart: true, default: undefined as Record | undefined, description: 'Per-model maximum number of background sub-agents that can run concurrently, keyed by model ID (e.g. { "qwen3-max": 2 }). Useful when a model has a lower concurrency capacity. Takes precedence over the global maxParallelAgents for the matched model; models not listed here fall back to the global limit.', showInDialog: false, mergeStrategy: MergeStrategy.SHALLOW_MERGE, jsonSchemaOverride: { type: 'object', additionalProperties: { type: 'integer', minimum: 1, }, }, }, 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, }, stopHookBlockingCap: { type: 'number', label: 'Stop Hook Blocking Cap', category: 'Advanced', requiresRestart: true, default: DEFAULT_STOP_HOOK_BLOCK_CAP, description: 'Maximum consecutive blocking Stop/SubagentStop hook decisions before Qwen Code overrides the hook loop and ends the turn. Can be overridden by QWEN_CODE_STOP_HOOK_BLOCK_CAP.', // This is an advanced safety valve for runaway hook loops, not a common // interactive preference. showInDialog: false, jsonSchemaOverride: { type: 'integer', minimum: 1, default: DEFAULT_STOP_HOOK_BLOCK_CAP, }, }, 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, }, UserPromptExpansion: { type: 'array', label: 'Prompt Expansion Hooks', category: 'Advanced', requiresRestart: false, default: [], description: 'Hooks that execute when a slash command expands into a prompt.', 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, }, StopFailure: { type: 'array', label: 'After Agent Failure Hooks', category: 'Advanced', requiresRestart: false, default: [], description: 'Hooks that execute when a turn ends due to an API error or loop detection, instead of the normal Stop hooks. Receives error type and details.', showInDialog: false, mergeStrategy: MergeStrategy.CONCAT, items: HOOK_DEFINITION_ITEMS, }, MessageDisplay: { type: 'array', label: 'Message Display Hooks', category: 'Advanced', requiresRestart: false, default: [], description: 'Hooks that execute repeatedly as the assistant reply streams, before the After Agent (Stop) hooks fire.', 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, }, PostToolBatch: { type: 'array', label: 'Post Tool Batch Hooks', category: 'Advanced', requiresRestart: false, default: [], description: 'Hooks that execute once after all tool calls in a batch resolve.', 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, }, SessionDelete: { type: 'array', label: 'Session Delete Hooks', category: 'Advanced', requiresRestart: false, default: [], description: 'Hooks that execute after an explicitly selected session is deleted.', 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, }, PostCompact: { type: 'array', label: 'Post Compact Hooks', category: 'Advanced', requiresRestart: false, default: [], description: 'Hooks that execute after conversation compaction completes.', showInDialog: false, mergeStrategy: MergeStrategy.CONCAT, items: HOOK_DEFINITION_ITEMS, }, PermissionDenied: { type: 'array', label: 'Permission Denied Hooks', category: 'Advanced', requiresRestart: false, default: [], description: 'Hooks that execute when AUTO-mode classification denies a tool call.', showInDialog: false, mergeStrategy: MergeStrategy.CONCAT, items: HOOK_DEFINITION_ITEMS, }, TodoCreated: { type: 'array', label: 'Todo Created Hooks', category: 'Advanced', requiresRestart: false, default: [], description: 'Hooks that execute when a new todo item is created. They can block creation during validation.', showInDialog: false, mergeStrategy: MergeStrategy.CONCAT, items: HOOK_DEFINITION_ITEMS, }, TodoCompleted: { type: 'array', label: 'Todo Completed Hooks', category: 'Advanced', requiresRestart: false, default: [], description: 'Hooks that execute when a todo item is marked as completed. They can block completion during validation.', showInDialog: false, mergeStrategy: MergeStrategy.CONCAT, items: HOOK_DEFINITION_ITEMS, }, InstructionsLoaded: { type: 'array', label: 'Instructions Loaded Hooks', category: 'Advanced', requiresRestart: false, default: [], description: 'Hooks that execute when an instruction file such as QWEN.md is loaded into context.', 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: { liveVoice: { type: 'object', label: 'Live Voice', category: 'Experimental', requiresRestart: false, default: {}, description: 'Experimental realtime voice conversations through Qwen Live Host on macOS WebShell.', showInDialog: false, properties: { enabled: { type: 'boolean', label: 'Live Voice', category: 'Experimental', requiresRestart: false, default: false, description: 'Enable experimental realtime voice conversations on macOS WebShell.', showInDialog: false, }, apiKey: { type: 'string', label: 'DashScope Realtime API Key', category: 'Experimental', requiresRestart: false, default: '' as string, description: 'Dedicated DashScope API key for qwen3.5-omni-plus-realtime.', showInDialog: false, }, model: { type: 'string', label: 'Live Voice Model', category: 'Experimental', requiresRestart: false, default: 'qwen3.5-omni-plus-realtime' as string, description: 'Realtime model used for Live Voice: a modelId or provider:modelId. When it names a modelProviders entry with realtimeOnly: true, that entry supplies the endpoint (derived from baseUrl) and the key (envKey); otherwise endpoint and apiKey below are used.', showInDialog: false, }, endpoint: { type: 'string', label: 'Live Voice Endpoint', category: 'Experimental', requiresRestart: false, default: 'wss://dashscope.aliyuncs.com/api-ws/v1/realtime' as string, description: 'Base URL of the DashScope Realtime service, such as https://dashscope.aliyuncs.com/compatible-mode/v1; a Realtime WebSocket URL is also accepted.', showInDialog: false, }, voice: { type: 'string', label: 'Live Voice Output Voice', category: 'Experimental', requiresRestart: false, default: 'Tina' as string, description: 'Voice used for Realtime model audio output.', showInDialog: false, }, shortcut: { type: 'string', label: 'Live Voice Global Shortcut', category: 'Experimental', requiresRestart: false, default: 'Command+E' as string, description: 'Electron accelerator registered globally by Qwen Live Host.', showInDialog: false, }, }, }, sessionWorkflow: { type: 'boolean', label: 'Session Workflow Plan & Review', category: 'Experimental', requiresRestart: false, default: false, description: 'Enable the daemon Web Shell Session Workflow DAG and present Plan mode as Plan & Review. Disabled by default; Workflow markers, approval gates, and visualization stay off until enabled. Todo updates preserve omitted active dependencies in every mode.', showInDialog: true, }, cron: { type: 'boolean', label: 'Enable Cron/Loop Tools', category: 'Experimental', requiresRestart: true, default: true, description: 'Enable in-session cron/loop tools. When enabled, the model can create recurring prompts using cron_create, cron_list, and cron_delete tools. Can be disabled via QWEN_CODE_DISABLE_CRON=1 environment variable.', showInDialog: true, }, todoStopGuard: { type: 'boolean', label: 'Enable Daemon Todo Stop Guard', category: 'Experimental', requiresRestart: true, default: false, description: 'Allow daemon and ACP sessions to continue an unfinished top-level Todo list for at most two consecutive primary-model calls without new user input. Requires tools.todoWrite.enabled. Mid-turn user input starts a fresh two-attempt stage. Disabled in safe, bare, and Approval plan modes.', showInDialog: false, }, sessionWriterLease: { type: 'boolean', label: 'Enable ACP Session Writer Lease', category: 'Experimental', requiresRestart: true, default: false, description: 'Enable cross-process write fencing for persisted ACP and daemon sessions. The effective value is frozen when the ACP or daemon process starts. Every concurrent ACP or daemon writer must enable the setting; interactive and headless writers remain outside the protocol.', showInDialog: true, }, cronRecurringMaxAgeDays: { type: 'number', label: 'Recurring Cron Max Age (Days)', category: 'Experimental', requiresRestart: true, default: 7, description: 'Days a recurring cron/loop job lives before auto-expiring (it fires one final time, then is deleted). Set to 0 to disable expiry so jobs run until deleted — useful for long-running daemon deployments. Can be overridden via the QWEN_CODE_CRON_MAX_AGE_DAYS environment variable.', // A deployment-time knob (cloud daemons, containers), not a common // interactive preference. showInDialog: false, jsonSchemaOverride: { type: 'number', minimum: 0, default: 7, }, }, agentTeam: { type: 'boolean', label: 'Enable Agent Team', category: 'Experimental', requiresRestart: true, default: false, description: 'Enable agent team collaboration tools (experimental). When enabled, the model can create agent teams and coordinate work using team_create, team_delete, send_message, task_create, task_update, and task_list tools. Can also be enabled via QWEN_CODE_ENABLE_AGENT_TEAM=1 environment variable.', showInDialog: true, }, agentCollaboration: { type: 'boolean', label: 'Enable Agent Collaboration', category: 'Experimental', requiresRestart: true, default: false, description: 'Enable persistent workspace Agents collaborating on shared task threads (experimental). Independent of Agent Team: neither flag implies the other. Enabling permits collaboration; opening an Agent to outside callers, trusting a connection and registering a host each still require their own explicit configuration. Can also be enabled via QWEN_CODE_ENABLE_AGENT_COLLABORATION=1.', showInDialog: true, }, artifact: { type: 'boolean', label: 'Enable Artifacts', category: 'Experimental', requiresRestart: true, default: true, description: 'Enable artifact tools. Enabled by default. In interactive, non-SDK sessions, the model can publish a self-contained HTML page as an interactive Artifact and open it in the browser. Non-SDK daemon sessions can use the metadata-only record_artifact tool. Set this to false or use QWEN_CODE_DISABLE_ARTIFACT=1 to disable both.', showInDialog: true, }, emitToolUseSummaries: { type: 'boolean', label: 'Tool Use Summaries', category: 'Experimental', requiresRestart: false, default: true, description: 'Generate a short LLM-based label after each tool batch completes. For a completed tool group the label replaces the generic `Tool × N` header; when the group is force-expanded it appears as a dim `●