diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md
new file mode 100644
index 000000000..b43df951a
--- /dev/null
+++ b/docs/users/configuration/settings.md
@@ -0,0 +1,733 @@
+# Qwen Code Configuration
+
+> [!tip]
+>
+> **Authentication / API keys:** Authentication (API Key, Alibaba Cloud Coding Plan) and auth-related environment variables (like `OPENAI_API_KEY`) are documented in **[Authentication](../configuration/auth)**.
+
+> [!note]
+>
+> **Note on New Configuration Format**: The format of the `settings.json` file has been updated to a new, more organized structure. The old format will be migrated automatically.
+> Qwen Code offers several ways to configure its behavior, including environment variables, command-line arguments, and settings files. This document outlines the different configuration methods and available settings.
+
+## Configuration layers
+
+Configuration is applied in the following order of precedence (lower numbers are overridden by higher numbers):
+
+| Level | Configuration Source | Description |
+| ----- | ---------------------- | ------------------------------------------------------------------------------- |
+| 1 | Default values | Hardcoded defaults within the application |
+| 2 | System defaults file | System-wide default settings that can be overridden by other settings files |
+| 3 | User settings file | Global settings for the current user |
+| 4 | Project settings file | Project-specific settings |
+| 5 | System settings file | System-wide settings that override all other settings files |
+| 6 | Environment variables | System-wide or session-specific variables, potentially loaded from `.env` files |
+| 7 | Command-line arguments | Values passed when launching the CLI |
+
+## Settings files
+
+Qwen Code uses JSON settings files for persistent configuration. There are four locations for these files:
+
+| File Type | Location | Scope |
+| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| System defaults file | Linux: `/etc/qwen-code/system-defaults.json` Windows: `C:\ProgramData\qwen-code\system-defaults.json` macOS: `/Library/Application Support/QwenCode/system-defaults.json` The path can be overridden using the `QWEN_CODE_SYSTEM_DEFAULTS_PATH` environment variable. | Provides a base layer of system-wide default settings. These settings have the lowest precedence and are intended to be overridden by user, project, or system override settings. |
+| User settings file | `~/.qwen/settings.json` (where `~` is your home directory). | Applies to all Qwen Code sessions for the current user. |
+| Project settings file | `.qwen/settings.json` within your project's root directory. | Applies only when running Qwen Code from that specific project. Project settings override user settings. |
+| System settings file | Linux: `/etc/qwen-code/settings.json` Windows: `C:\ProgramData\qwen-code\settings.json` macOS: `/Library/Application Support/QwenCode/settings.json` The path can be overridden using the `QWEN_CODE_SYSTEM_SETTINGS_PATH` environment variable. | Applies to all Qwen Code sessions on the system, for all users. System settings override user and project settings. May be useful for system administrators at enterprises to have controls over users' Qwen Code setups. |
+
+> [!note]
+>
+> **Note on environment variables in settings:** String values within your `settings.json` files can reference environment variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically resolved when the settings are loaded. For example, if you have an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`.
+
+### The `.qwen` directory in your project
+
+In addition to a project settings file, a project's `.qwen` directory can contain other project-specific files related to Qwen Code's operation, such as:
+
+- [Custom sandbox profiles](../features/sandbox) (e.g. `.qwen/sandbox-macos-custom.sb`, `.qwen/sandbox.Dockerfile`).
+- [Agent Skills](../features/skills) under `.qwen/skills/` (each Skill is a directory containing a `SKILL.md`).
+
+### Configuration migration
+
+Qwen Code automatically migrates legacy configuration settings to the new format. Old settings files are backed up before migration. The following settings have been renamed from negative (`disable*`) to positive (`enable*`) naming:
+
+| Old Setting | New Setting | Notes |
+| ---------------------------------------- | ------------------------------------------- | ---------------------------------- |
+| `disableAutoUpdate` + `disableUpdateNag` | `general.enableAutoUpdate` | Consolidated into a single setting |
+| `disableLoadingPhrases` | `ui.accessibility.enableLoadingPhrases` | |
+| `disableFuzzySearch` | `context.fileFiltering.enableFuzzySearch` | |
+| `disableCacheControl` | `model.generationConfig.enableCacheControl` | |
+
+> [!note]
+>
+> **Boolean value inversion:** When migrating, boolean values are inverted (e.g., `disableAutoUpdate: true` becomes `enableAutoUpdate: false`).
+
+#### Consolidation policy for `disableAutoUpdate` and `disableUpdateNag`
+
+When both legacy settings are present with different values, the migration follows this policy: if **either** `disableAutoUpdate` **or** `disableUpdateNag` is `true`, then `enableAutoUpdate` becomes `false`:
+
+| `disableAutoUpdate` | `disableUpdateNag` | Migrated `enableAutoUpdate` |
+| ------------------- | ------------------ | --------------------------- |
+| `false` | `false` | `true` |
+| `false` | `true` | `false` |
+| `true` | `false` | `false` |
+| `true` | `true` | `false` |
+
+### Available settings in `settings.json`
+
+Settings are organized into categories. All settings should be placed within their corresponding top-level category object in your `settings.json` file.
+
+#### general
+
+| Setting | Type | Description | Default |
+| ------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
+| `general.preferredEditor` | string | The preferred editor to open files in. | `undefined` |
+| `general.vimMode` | boolean | Enable Vim keybindings. | `false` |
+| `general.enableAutoUpdate` | boolean | Enable automatic update checks and installations on startup. | `true` |
+| `general.showSessionRecap` | boolean | Show a 1-3 sentence summary of where you left off when returning to the terminal after being away for 5+ minutes. Use `/recap` to trigger manually. | `true` |
+| `general.gitCoAuthor` | boolean | Automatically add a Co-authored-by trailer to git commit messages when commits are made through Qwen Code. | `true` |
+| `general.checkpointing.enabled` | boolean | Enable session checkpointing for recovery. | `false` |
+| `general.defaultFileEncoding` | string | 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. | `"utf-8"` |
+
+#### output
+
+| Setting | Type | Description | Default | Possible Values |
+| --------------- | ------ | ----------------------------- | -------- | ------------------ |
+| `output.format` | string | The format of the CLI output. | `"text"` | `"text"`, `"json"` |
+
+#### ui
+
+| Setting | Type | Description | Default |
+| --------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
+| `ui.theme` | string | The color theme for the UI. See [Themes](../configuration/themes) for available options. | `undefined` |
+| `ui.customThemes` | object | Custom theme definitions. | `{}` |
+| `ui.statusLine` | object | Custom status line configuration. A shell command whose output is shown in the footer's left section. See [Status Line](../features/status-line). | `undefined` |
+| `ui.hideWindowTitle` | boolean | Hide the window title bar. | `false` |
+| `ui.hideTips` | boolean | Hide all tips (startup and post-response) in the UI. See [Contextual Tips](../features/tips). | `false` |
+| `ui.hideBanner` | boolean | Hide the application banner. | `false` |
+| `ui.hideFooter` | boolean | Hide the footer from the UI. | `false` |
+| `ui.showMemoryUsage` | boolean | Display memory usage information in the UI. | `false` |
+| `ui.showLineNumbers` | boolean | Show line numbers in code blocks in the CLI output. | `true` |
+| `ui.showCitations` | boolean | Show citations for generated text in the chat. | `true` |
+| `ui.compactMode` | boolean | Hide tool output and thinking for a cleaner view. Toggle with `Ctrl+O` during a session or via the Settings dialog. Tool approval prompts are never hidden, even in compact mode. The setting persists across sessions. | `false` |
+| `enableWelcomeBack` | boolean | Show welcome back dialog when returning to a project with conversation history. When enabled, Qwen Code will automatically detect if you're returning to a project with a previously generated project summary (`.qwen/PROJECT_SUMMARY.md`) and show a dialog allowing you to continue your previous conversation or start fresh. If you choose **Start new chat session**, that choice is remembered for the current project until the project summary changes. This feature integrates with the `/summary` command and quit confirmation dialog. | `true` |
+| `ui.accessibility.enableLoadingPhrases` | boolean | Enable loading phrases (disable for accessibility). | `true` |
+| `ui.accessibility.screenReader` | boolean | Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. | `false` |
+| `ui.customWittyPhrases` | array of strings | A list of custom phrases to display during loading states. When provided, the CLI will cycle through these phrases instead of the default ones. | `[]` |
+| `ui.enableFollowupSuggestions` | boolean | Enable [followup suggestions](../features/followup-suggestions) that predict what you want to type next after the model responds. Suggestions appear as ghost text and can be accepted with Tab, Enter, or Right Arrow. | `true` |
+| `ui.enableCacheSharing` | boolean | Use cache-aware forked queries for suggestion generation. Reduces cost on providers that support prefix caching (experimental). | `true` |
+| `ui.enableSpeculation` | boolean | Speculatively execute accepted suggestions before submission. Results appear instantly when you accept (experimental). | `false` |
+
+#### ide
+
+| Setting | Type | Description | Default |
+| ------------------ | ------- | ---------------------------------------------------- | ------- |
+| `ide.enabled` | boolean | Enable IDE integration mode. | `false` |
+| `ide.hasSeenNudge` | boolean | Whether the user has seen the IDE integration nudge. | `false` |
+
+#### privacy
+
+| Setting | Type | Description | Default |
+| -------------------------------- | ------- | -------------------------------------- | ------- |
+| `privacy.usageStatisticsEnabled` | boolean | Enable collection of usage statistics. | `true` |
+
+#### model
+
+| Setting | Type | Description | Default |
+| -------------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
+| `model.name` | string | The Qwen model to use for conversations. | `undefined` |
+| `model.maxSessionTurns` | number | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
+| `model.generationConfig` | object | Advanced overrides passed to the underlying content generator. Supports request controls such as `timeout`, `maxRetries`, `enableCacheControl`, `contextWindowSize` (override model's context window size), `modalities` (override auto-detected input modalities), `customHeaders` (custom HTTP headers for API requests), and `extra_body` (additional body parameters for OpenAI-compatible API requests only), along with fine-tuning knobs under `samplingParams` (for example `temperature`, `top_p`, `max_tokens`). Leave unset to rely on provider defaults. | `undefined` |
+| `model.chatCompression.contextPercentageThreshold` | number | Sets the threshold for chat history compression as a percentage of the model's total token limit. This is a value between 0 and 1 that applies to both automatic compression and the manual `/compress` command. For example, a value of `0.6` will trigger compression when the chat history exceeds 60% of the token limit. Use `0` to disable compression entirely. | `0.7` |
+| `model.skipNextSpeakerCheck` | boolean | Skip the next speaker check. | `false` |
+| `model.skipLoopDetection` | boolean | Disables loop detection checks. Loop detection prevents infinite loops in AI responses but can generate false positives that interrupt legitimate workflows. Enable this option if you experience frequent false positive loop detection interruptions. | `false` |
+| `model.skipStartupContext` | boolean | Skips sending the startup workspace context (environment summary and acknowledgement) at the beginning of each session. Enable this if you prefer to provide context manually or want to save tokens on startup. | `false` |
+| `model.enableOpenAILogging` | boolean | Enables logging of OpenAI API calls for debugging and analysis. When enabled, API requests and responses are logged to JSON files. | `false` |
+| `model.openAILoggingDir` | string | Custom directory path for OpenAI API logs. If not specified, defaults to `logs/openai` in the current working directory. Supports absolute paths, relative paths (resolved from current working directory), and `~` expansion (home directory). | `undefined` |
+
+**Example model.generationConfig:**
+
+```json
+{
+ "model": {
+ "generationConfig": {
+ "timeout": 60000,
+ "contextWindowSize": 128000,
+ "modalities": {
+ "image": true
+ },
+ "enableCacheControl": true,
+ "customHeaders": {
+ "X-Client-Request-ID": "req-123"
+ },
+ "extra_body": {
+ "enable_thinking": true
+ },
+ "samplingParams": {
+ "temperature": 0.2,
+ "top_p": 0.8,
+ "max_tokens": 1024
+ }
+ }
+ }
+}
+```
+
+**max_tokens (adaptive output tokens):**
+
+When `samplingParams.max_tokens` is not set, Qwen Code uses an adaptive output token strategy to optimize GPU resource usage:
+
+1. Requests start with a default limit of **8K** output tokens
+2. If the response is truncated (the model hits the limit), Qwen Code automatically retries with **64K** tokens
+3. The partial output is discarded and replaced with the full response from the retry
+
+This is transparent to users — you may briefly see a retry indicator if escalation occurs. Since 99% of responses are under 5K tokens, the retry happens rarely (<1% of requests).
+
+To override this behavior, either set `samplingParams.max_tokens` in your settings or use the `QWEN_CODE_MAX_OUTPUT_TOKENS` environment variable.
+
+**contextWindowSize:**
+
+Overrides the default context window size for the selected model. Qwen Code determines the context window using built-in defaults based on model name matching, with a constant fallback value. 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.
+
+**modalities:**
+
+Overrides the auto-detected input modalities for the selected model. Qwen Code automatically detects supported modalities (image, PDF, audio, video) based on model name pattern matching. Use this setting when the auto-detection is incorrect — for example, to enable `pdf` for a model that supports it but isn't recognized. Format: `{ "image": true, "pdf": true, "audio": true, "video": true }`. Omit a key or set it to `false` for unsupported types.
+
+**customHeaders:**
+
+Allows you to add custom HTTP headers to all API requests. This is useful for request tracing, monitoring, API gateway routing, or when different models require different headers. If `customHeaders` is defined in `modelProviders[].generationConfig.customHeaders`, it will be used directly; otherwise, headers from `model.generationConfig.customHeaders` will be used. No merging occurs between the two levels.
+
+The `extra_body` field allows you to add custom parameters to the request body sent to the API. This is useful for provider-specific options that are not covered by the standard configuration fields. **Note: This field is only supported for OpenAI-compatible providers (`openai`, `qwen-oauth`). It is ignored for Anthropic and Gemini providers.** If `extra_body` is defined in `modelProviders[].generationConfig.extra_body`, it will be used directly; otherwise, values from `model.generationConfig.extra_body` will be used.
+
+**model.openAILoggingDir examples:**
+
+- `"~/qwen-logs"` - Logs to `~/qwen-logs` directory
+- `"./custom-logs"` - Logs to `./custom-logs` relative to current directory
+- `"/tmp/openai-logs"` - Logs to absolute path `/tmp/openai-logs`
+
+#### fastModel
+
+| Setting | Type | Description | Default |
+| ----------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
+| `fastModel` | string | Model used for generating [prompt suggestions](../features/followup-suggestions) and speculative execution. Leave empty to use the main model. A smaller/faster model (e.g., `qwen3-coder-flash`) reduces latency and cost. Can also be set via `/model --fast`. | `""` |
+
+#### context
+
+| Setting | Type | Description | Default |
+| -------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
+| `context.fileName` | string or array of strings | The name of the context file(s). | `undefined` |
+| `context.importFormat` | string | The format to use when importing memory. | `undefined` |
+| `context.includeDirectories` | array | Additional directories to include in the workspace context. Specifies an array of additional absolute or relative paths to include in the workspace context. Missing directories will be skipped with a warning by default. Paths can use `~` to refer to the user's home directory. This setting can be combined with the `--include-directories` command-line flag. | `[]` |
+| `context.loadFromIncludeDirectories` | boolean | Controls the behavior of the `/memory refresh` command. If set to `true`, `QWEN.md` files should be loaded from all directories that are added. If set to `false`, `QWEN.md` should only be loaded from the current directory. | `false` |
+| `context.fileFiltering.respectGitIgnore` | boolean | Respect .gitignore files when searching. | `true` |
+| `context.fileFiltering.respectQwenIgnore` | boolean | Respect .qwenignore files when searching. | `true` |
+| `context.fileFiltering.enableRecursiveFileSearch` | boolean | Whether to enable searching recursively for filenames under the current tree when completing `@` prefixes in the prompt. | `true` |
+| `context.fileFiltering.enableFuzzySearch` | boolean | When `true`, enables fuzzy search capabilities when searching for files. Set to `false` to improve performance on projects with a large number of files. | `true` |
+| `context.clearContextOnIdle.thinkingThresholdMinutes` | number | Minutes of inactivity before clearing old thinking blocks to free context tokens. Aligns with typical provider prompt-cache TTL. Use `-1` to disable. | `5` |
+| `context.clearContextOnIdle.toolResultsThresholdMinutes` | number | Minutes of inactivity before clearing old tool result content. Use `-1` to disable. | `60` |
+| `context.clearContextOnIdle.toolResultsNumToKeep` | number | Number of most-recent compactable tool results to preserve when clearing. Floor at 1. | `5` |
+
+#### Troubleshooting File Search Performance
+
+If you are experiencing performance issues with file searching (e.g., with `@` completions), especially in projects with a very large number of files, here are a few things you can try in order of recommendation:
+
+1. **Use `.qwenignore`:** Create a `.qwenignore` file in your project root to exclude directories that contain a large number of files that you don't need to reference (e.g., build artifacts, logs, `node_modules`). Reducing the total number of files crawled is the most effective way to improve performance.
+2. **Disable Fuzzy Search:** If ignoring files is not enough, you can disable fuzzy search by setting `enableFuzzySearch` to `false` in your `settings.json` file. This will use a simpler, non-fuzzy matching algorithm, which can be faster.
+3. **Disable Recursive File Search:** As a last resort, you can disable recursive file search entirely by setting `enableRecursiveFileSearch` to `false`. This will be the fastest option as it avoids a recursive crawl of your project. However, it means you will need to type the full path to files when using `@` completions.
+
+#### tools
+
+| Setting | Type | Description | Default | Notes |
+| ------------------------------------ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `tools.sandbox` | boolean or string | Sandbox execution environment (can be a boolean or a path string). | `undefined` | |
+| `tools.sandboxImage` | string | Sandbox image URI used by Docker/Podman when `--sandbox-image` and `QWEN_SANDBOX_IMAGE` are not set. | `undefined` | |
+| `tools.shell.enableInteractiveShell` | boolean | Use `node-pty` for an interactive shell experience. Fallback to `child_process` still applies. | `false` | |
+| `tools.core` | array of strings | **Deprecated.** Will be removed in next version. Use `permissions.allow` + `permissions.deny` instead. Restricts built-in tools to an allowlist. All tools not in the list are disabled. | `undefined` | |
+| `tools.exclude` | array of strings | **Deprecated.** Use `permissions.deny` instead. Tool names to exclude from discovery. Automatically migrated to the `permissions` format on first load. | `undefined` | |
+| `tools.allowed` | array of strings | **Deprecated.** Use `permissions.allow` instead. Tool names that bypass the confirmation dialog. Automatically migrated to the `permissions` format on first load. | `undefined` | |
+| `tools.approvalMode` | string | Sets the default approval mode for tool usage. | `default` | Possible values: `plan` (analyze only, do not modify files or execute commands), `default` (require approval before file edits or shell commands run), `auto-edit` (automatically approve file edits), `yolo` (automatically approve all tool calls) |
+| `tools.discoveryCommand` | string | Command to run for tool discovery. | `undefined` | |
+| `tools.callCommand` | string | Defines a custom shell command for calling a specific tool that was discovered using `tools.discoveryCommand`. The shell command must meet the following criteria: It must take function `name` (exactly as in [function declaration](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations)) as first command line argument. It must read function arguments as JSON on `stdin`, analogous to [`functionCall.args`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functioncall). It must return function output as JSON on `stdout`, analogous to [`functionResponse.response.content`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functionresponse). | `undefined` | |
+| `tools.useRipgrep` | boolean | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` | |
+| `tools.useBuiltinRipgrep` | boolean | Use the bundled ripgrep binary. When set to `false`, the system-level `rg` command will be used instead. This setting is only effective when `tools.useRipgrep` is `true`. | `true` | |
+| `tools.truncateToolOutputThreshold` | number | Truncate tool output if it is larger than this many characters. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `25000` | Requires restart: Yes |
+| `tools.truncateToolOutputLines` | number | Maximum lines or entries kept when truncating tool output. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `1000` | Requires restart: Yes |
+
+> [!note]
+>
+> **Migrating from `tools.core` / `tools.exclude` / `tools.allowed`:** These legacy settings are **deprecated** and automatically migrated to the new `permissions` format on first load. Prefer configuring `permissions.allow` / `permissions.deny` directly. Use `/permissions` to manage rules interactively.
+
+#### memory
+
+| Setting | Type | Description | Default |
+| -------------------------------- | ------- | --------------------------------------------------------------------------------- | ------- |
+| `memory.enableManagedAutoMemory` | boolean | Enable background extraction of memories from conversations. | `true` |
+| `memory.enableManagedAutoDream` | boolean | Enable automatic consolidation (deduplication and cleanup) of collected memories. | `false` |
+
+See [Memory](../features/memory) for details on how auto-memory works and how to use the `/memory`, `/remember`, and `/dream` commands.
+
+#### permissions
+
+The permissions system provides fine-grained control over which tools can run, which require confirmation, and which are blocked.
+
+**Decision priority (highest first): `deny` > `ask` > `allow` > _(default/interactive mode)_**
+
+The first matching rule wins. Rules use the format `"ToolName"` or `"ToolName(specifier)"`.
+
+| Setting | Type | Description | Default |
+| ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------- | ----------- |
+| `permissions.allow` | array of strings | Rules for auto-approved tool calls (no confirmation needed). Merged across all scopes (user + project + system). | `undefined` |
+| `permissions.ask` | array of strings | Rules for tool calls that always require user confirmation. Takes priority over `allow`. | `undefined` |
+| `permissions.deny` | array of strings | Rules for blocked tool calls. Highest priority — overrides both `allow` and `ask`. | `undefined` |
+
+**Tool name aliases (any of these work in rules):**
+
+| Alias | Canonical tool | Notes |
+| --------------------- | ------------------- | ------------------------- |
+| `Bash`, `Shell` | `run_shell_command` | |
+| `Read`, `ReadFile` | `read_file` | Meta-category — see below |
+| `Edit`, `EditFile` | `edit` | Meta-category — see below |
+| `Write`, `WriteFile` | `write_file` | |
+| `Grep`, `SearchFiles` | `grep_search` | |
+| `Glob`, `FindFiles` | `glob` | |
+| `ListFiles` | `list_directory` | |
+| `WebFetch` | `web_fetch` | |
+| `Agent` | `task` | |
+| `Skill` | `skill` | |
+
+**Meta-categories:**
+
+Some rule names automatically cover multiple tools:
+
+| Rule name | Tools covered |
+| --------- | ---------------------------------------------------- |
+| `Read` | `read_file`, `grep_search`, `glob`, `list_directory` |
+| `Edit` | `edit`, `write_file` |
+
+> [!important]
+> `Read(/path/**)` matches **all four** read tools (file read, grep, glob, and directory listing).
+> To restrict only file reading, use `ReadFile(/path/**)` or `read_file(/path/**)`.
+
+**Rule syntax examples:**
+
+| Rule | Meaning |
+| ----------------------------- | -------------------------------------------------------------- |
+| `"Bash"` | All shell commands |
+| `"Bash(git *)"` | Shell commands starting with `git` (word boundary: NOT `gitk`) |
+| `"Bash(git push *)"` | Shell commands like `git push origin main` |
+| `"Bash(npm run *)"` | Any `npm run` script |
+| `"Read"` | All file read operations (read, grep, glob, list) |
+| `"Read(./secrets/**)"` | Read any file under `./secrets/` recursively |
+| `"Edit(/src/**/*.ts)"` | Edit TypeScript files under project root `/src/` |
+| `"WebFetch(api.example.com)"` | Fetch from `api.example.com` and all its subdomains |
+| `"mcp__puppeteer"` | All tools from the puppeteer MCP server |
+
+**Path pattern prefixes:**
+
+| Prefix | Meaning | Example |
+| ------ | ------------------------------------- | ------------------- |
+| `//` | Absolute path from filesystem root | `//etc/passwd` |
+| `~/` | Relative to home directory | `~/Documents/*.pdf` |
+| `/` | Relative to project root | `/src/**/*.ts` |
+| `./` | Relative to current working directory | `./secrets/**` |
+| (none) | Same as `./` | `secrets/**` |
+
+**Shell command bypass prevention:**
+
+Permission rules for `Read`, `Edit`, and `WebFetch` are also enforced when the agent runs equivalent shell commands. For example, if `Read(./.env)` is in `deny`, the agent cannot bypass it via `cat .env` in a shell command. Supported shell commands include `cat`, `grep`, `curl`, `wget`, `cp`, `mv`, `rm`, `chmod`, and many more. Unknown/safe commands (e.g. `git`) are unaffected by file/network rules.
+
+**Migrating from legacy settings:**
+
+| Legacy setting | Equivalent `permissions` rule | Notes |
+| --------------- | ------------------------------- | ------------------------------------------------------------ |
+| `tools.allowed` | `permissions.allow` | Auto-migrated on first load |
+| `tools.exclude` | `permissions.deny` | Auto-migrated on first load |
+| `tools.core` | `permissions.allow` (allowlist) | Auto-migrated; unlisted tools are disabled at registry level |
+
+**Example configuration:**
+
+```json
+{
+ "permissions": {
+ "allow": ["Bash(git *)", "Bash(npm run *)", "Read(//Users/alice/code/**)"],
+ "ask": ["Bash(git push *)", "Edit"],
+ "deny": ["Bash(rm -rf *)", "Read(.env)", "WebFetch(malicious.com)"]
+ }
+}
+```
+
+> [!tip]
+> Use `/permissions` in the interactive CLI to view, add, and remove rules without editing `settings.json` directly.
+
+#### slashCommands
+
+Controls which slash commands are available in the CLI. Useful for locking down
+the command surface in multi-tenant or enterprise deployments.
+
+| Setting | Type | Description | Default |
+| ------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
+| `slashCommands.disabled` | array of strings | 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`). **Merged as a union across scopes**, so workspace settings can add to but not remove entries defined in user or system settings. | `undefined` |
+
+The same denylist can also be provided via the `--disabled-slash-commands` CLI
+flag (comma-separated or repeated) and the `QWEN_DISABLED_SLASH_COMMANDS`
+environment variable; values from all three sources are unioned together.
+
+**Example — lock down built-ins for a sandboxed deployment:**
+
+```json
+{
+ "slashCommands": {
+ "disabled": ["auth", "mcp", "extensions", "ide", "quit"]
+ }
+}
+```
+
+With these values in a system-level `settings.json` (`/etc/qwen-code/settings.json`
+or `QWEN_CODE_SYSTEM_SETTINGS_PATH`), users cannot shrink the denylist from
+their own scope, and the disabled commands will not appear in autocomplete or
+execute when typed.
+
+> [!note]
+> This setting only gates slash commands (e.g. `/auth`, `/mcp`). It does not
+> affect tool permissions — see `permissions.deny` for that. It also does not
+> intercept keyboard shortcuts such as `Ctrl+C` or `Esc`.
+
+#### mcp
+
+| Setting | Type | Description | Default |
+| ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
+| `mcp.serverCommand` | string | Command to start an MCP server. | `undefined` |
+| `mcp.allowed` | array of strings | An allowlist of MCP servers to allow. Allows you to specify a list of MCP server names that should be made available to the model. This can be used to restrict the set of MCP servers to connect to. Note that this will be ignored if `--allowed-mcp-server-names` is set. | `undefined` |
+| `mcp.excluded` | array of strings | A denylist of MCP servers to exclude. A server listed in both `mcp.excluded` and `mcp.allowed` is excluded. Note that this will be ignored if `--allowed-mcp-server-names` is set. | `undefined` |
+
+> [!note]
+>
+> **Security Note for MCP servers:** These settings use simple string matching on MCP server names, which can be modified. If you're a system administrator looking to prevent users from bypassing this, consider configuring the `mcpServers` at the system settings level such that the user will not be able to configure any MCP servers of their own. This should not be used as an airtight security mechanism.
+
+#### lsp
+
+> [!warning]
+> **Experimental Feature**: LSP support is currently experimental and disabled by default. Enable it using the `--experimental-lsp` command line flag.
+
+Language Server Protocol (LSP) provides code intelligence features like go-to-definition, find references, and diagnostics.
+
+LSP server configuration is done through `.lsp.json` files in your project root directory, not through `settings.json`. See the [LSP documentation](../features/lsp) for configuration details and examples.
+
+#### security
+
+| Setting | Type | Description | Default |
+| ------------------------------ | ------- | ------------------------------------------------- | ----------- |
+| `security.folderTrust.enabled` | boolean | Setting to track whether Folder trust is enabled. | `false` |
+| `security.auth.selectedType` | string | The currently selected authentication type. | `undefined` |
+| `security.auth.enforcedType` | string | The required auth type (useful for enterprises). | `undefined` |
+| `security.auth.useExternal` | boolean | Whether to use an external authentication flow. | `undefined` |
+
+#### advanced
+
+| Setting | Type | Description | Default |
+| ------------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
+| `advanced.autoConfigureMemory` | boolean | Automatically configure Node.js memory limits. | `false` |
+| `advanced.dnsResolutionOrder` | string | The DNS resolution order. | `undefined` |
+| `advanced.excludedEnvVars` | array of strings | Environment variables to exclude from project context. Specifies environment variables that should be excluded from being loaded from project `.env` files. This prevents project-specific environment variables (like `DEBUG=true`) from interfering with the CLI behavior. Variables from `.qwen/.env` files are never excluded. | `["DEBUG","DEBUG_MODE"]` |
+| `advanced.bugCommand` | object | Configuration for the bug report command. Overrides the default URL for the `/bug` command. Properties: `urlTemplate` (string): A URL that can contain `{title}` and `{info}` placeholders. Example: `"bugCommand": { "urlTemplate": "https://bug.example.com/new?title={title}&info={info}" }` | `undefined` |
+| `advanced.tavilyApiKey` | string | API key for Tavily web search service. Used to enable the `web_search` tool functionality. | `undefined` |
+
+> [!note]
+>
+> **Note about advanced.tavilyApiKey:** This is a legacy configuration format. For Qwen OAuth users, DashScope provider is automatically available without any configuration. For other authentication types, configure Tavily or Google providers using the new `webSearch` configuration format.
+
+#### mcpServers
+
+Configures connections to one or more Model-Context Protocol (MCP) servers for discovering and using custom tools. Qwen Code attempts to connect to each configured MCP server to discover available tools. If multiple MCP servers expose a tool with the same name, the tool names will be prefixed with the server alias you defined in the configuration (e.g., `serverAlias__actualToolName`) to avoid conflicts. Note that the system might strip certain schema properties from MCP tool definitions for compatibility. At least one of `command`, `url`, or `httpUrl` must be provided. If multiple are specified, the order of precedence is `httpUrl`, then `url`, then `command`.
+
+| Property | Type | Description | Optional |
+| --------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
+| `mcpServers..command` | string | The command to execute to start the MCP server via standard I/O. | Yes |
+| `mcpServers..args` | array of strings | Arguments to pass to the command. | Yes |
+| `mcpServers..env` | object | Environment variables to set for the server process. | Yes |
+| `mcpServers..cwd` | string | The working directory in which to start the server. | Yes |
+| `mcpServers..url` | string | The URL of an MCP server that uses Server-Sent Events (SSE) for communication. | Yes |
+| `mcpServers..httpUrl` | string | The URL of an MCP server that uses streamable HTTP for communication. | Yes |
+| `mcpServers..headers` | object | A map of HTTP headers to send with requests to `url` or `httpUrl`. | Yes |
+| `mcpServers..timeout` | number | Timeout in milliseconds for requests to this MCP server. | Yes |
+| `mcpServers..trust` | boolean | Trust this server and bypass all tool call confirmations. | Yes |
+| `mcpServers..description` | string | A brief description of the server, which may be used for display purposes. | Yes |
+| `mcpServers..includeTools` | array of strings | List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (allowlist behavior). If not specified, all tools from the server are enabled by default. | Yes |
+| `mcpServers..excludeTools` | array of strings | List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server. **Note:** `excludeTools` takes precedence over `includeTools` - if a tool is in both lists, it will be excluded. | Yes |
+
+#### telemetry
+
+Configures logging and metrics collection for Qwen Code. For more information, see [telemetry](/developers/development/telemetry).
+
+| Setting | Type | Description | Default |
+| ------------------------ | ------- | -------------------------------------------------------------------------------- | ------- |
+| `telemetry.enabled` | boolean | Whether or not telemetry is enabled. | |
+| `telemetry.target` | string | The destination for collected telemetry. Supported values are `local` and `gcp`. | |
+| `telemetry.otlpEndpoint` | string | The endpoint for the OTLP Exporter. | |
+| `telemetry.otlpProtocol` | string | The protocol for the OTLP Exporter (`grpc` or `http`). | |
+| `telemetry.logPrompts` | boolean | Whether or not to include the content of user prompts in the logs. | |
+| `telemetry.outfile` | string | The file to write telemetry to when `target` is `local`. | |
+| `telemetry.useCollector` | boolean | Whether to use an external OTLP collector. | |
+
+### Example `settings.json`
+
+Here is an example of a `settings.json` file with the nested structure, new as of v0.3.0:
+
+```
+{
+ "general": {
+ "vimMode": true,
+ "preferredEditor": "code"
+ },
+ "ui": {
+ "theme": "GitHub",
+ "hideTips": false,
+ "customWittyPhrases": [
+ "You forget a thousand things every day. Make sure this is one of 'em",
+ "Connecting to AGI"
+ ]
+ },
+ "tools": {
+ "approvalMode": "yolo",
+ "sandbox": "docker",
+ "sandboxImage": "ghcr.io/qwenlm/qwen-code:0.14.1",
+ "discoveryCommand": "bin/get_tools",
+ "callCommand": "bin/call_tool",
+ "exclude": ["write_file"]
+ },
+ "mcpServers": {
+ "mainServer": {
+ "command": "bin/mcp_server.py"
+ },
+ "anotherServer": {
+ "command": "node",
+ "args": ["mcp_server.js", "--verbose"]
+ }
+ },
+ "telemetry": {
+ "enabled": true,
+ "target": "local",
+ "otlpEndpoint": "http://localhost:4317",
+ "logPrompts": true
+ },
+ "privacy": {
+ "usageStatisticsEnabled": true
+ },
+ "model": {
+ "name": "qwen3-coder-plus",
+ "maxSessionTurns": 10,
+ "enableOpenAILogging": false,
+ "openAILoggingDir": "~/qwen-logs",
+ },
+ "context": {
+ "fileName": ["CONTEXT.md", "QWEN.md"],
+ "includeDirectories": ["path/to/dir1", "~/path/to/dir2", "../path/to/dir3"],
+ "loadFromIncludeDirectories": true,
+ "fileFiltering": {
+ "respectGitIgnore": false
+ }
+ },
+ "advanced": {
+ "excludedEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"]
+ }
+}
+```
+
+## Shell History
+
+The CLI keeps a history of shell commands you run. To avoid conflicts between different projects, this history is stored in a project-specific directory within your user's home folder.
+
+- **Location:** `~/.qwen/tmp//shell_history`
+ - `` is a unique identifier generated from your project's root path.
+ - The history is stored in a file named `shell_history`.
+
+## Environment Variables & `.env` Files
+
+Environment variables are a common way to configure applications, especially for sensitive information (like tokens) or for settings that might change between environments.
+
+Qwen Code can automatically load environment variables from `.env` files.
+For authentication-related variables (like `OPENAI_*`) and the recommended `.qwen/.env` approach, see **[Authentication](../configuration/auth)**.
+
+> [!tip]
+>
+> **Environment Variable Exclusion:** Some environment variables (like `DEBUG` and `DEBUG_MODE`) are automatically excluded from project `.env` files by default to prevent interference with the CLI behavior. Variables from `.qwen/.env` files are never excluded. You can customize this behavior using the `advanced.excludedEnvVars`setting in your `settings.json` file.
+
+### Environment Variables Table
+
+| Variable | Description | Notes |
+| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `QWEN_TELEMETRY_ENABLED` | Set to `true` or `1` to enable telemetry. Any other value is treated as disabling it. | Overrides the `telemetry.enabled` setting. |
+| `QWEN_TELEMETRY_TARGET` | Sets the telemetry target (`local` or `gcp`). | Overrides the `telemetry.target` setting. |
+| `QWEN_TELEMETRY_OTLP_ENDPOINT` | Sets the OTLP endpoint for telemetry. | Overrides the `telemetry.otlpEndpoint` setting. |
+| `QWEN_TELEMETRY_OTLP_PROTOCOL` | Sets the OTLP protocol (`grpc` or `http`). | Overrides the `telemetry.otlpProtocol` setting. |
+| `QWEN_TELEMETRY_LOG_PROMPTS` | Set to `true` or `1` to enable or disable logging of user prompts. Any other value is treated as disabling it. | Overrides the `telemetry.logPrompts` setting. |
+| `QWEN_TELEMETRY_OUTFILE` | Sets the file path to write telemetry to when the target is `local`. | Overrides the `telemetry.outfile` setting. |
+| `QWEN_TELEMETRY_USE_COLLECTOR` | Set to `true` or `1` to enable or disable using an external OTLP collector. Any other value is treated as disabling it. | Overrides the `telemetry.useCollector` setting. |
+| `QWEN_SANDBOX` | Alternative to the `sandbox` setting in `settings.json`. | Accepts `true`, `false`, `docker`, `podman`, or a custom command string. |
+| `QWEN_SANDBOX_IMAGE` | Overrides sandbox image selection for Docker/Podman. | Takes precedence over `tools.sandboxImage`. |
+| `SEATBELT_PROFILE` | (macOS specific) Switches the Seatbelt (`sandbox-exec`) profile on macOS. | `permissive-open`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other operations. `strict`: Uses a strict profile that declines operations by default. ``: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-.sb` in your project's `.qwen/` directory (e.g., `my-project/.qwen/sandbox-macos-custom.sb`). |
+| `DEBUG` or `DEBUG_MODE` | (often used by underlying libraries or the CLI itself) Set to `true` or `1` to enable verbose debug logging, which can be helpful for troubleshooting. | **Note:** These variables are automatically excluded from project `.env` files by default to prevent interference with the CLI behavior. Use `.qwen/.env` files if you need to set these for Qwen Code specifically. |
+| `NO_COLOR` | Set to any value to disable all color output in the CLI. | |
+| `CLI_TITLE` | Set to a string to customize the title of the CLI. | |
+| `CODE_ASSIST_ENDPOINT` | Specifies the endpoint for the code assist server. | This is useful for development and testing. |
+| `QWEN_CODE_MAX_OUTPUT_TOKENS` | Overrides the default maximum output tokens per response. When not set, Qwen Code uses an adaptive strategy: starts with 8K tokens and automatically retries with 64K if the response is truncated. Set this to a specific value (e.g., `16000`) to use a fixed limit instead. | Takes precedence over the capped default (8K) but is overridden by `samplingParams.max_tokens` in settings. Disables automatic escalation when set. Example: `export QWEN_CODE_MAX_OUTPUT_TOKENS=16000` |
+| `TAVILY_API_KEY` | Your API key for the Tavily web search service. | Used to enable the `web_search` tool functionality. Example: `export TAVILY_API_KEY="tvly-your-api-key-here"` |
+| `QWEN_CODE_PROFILE_STARTUP` | Set to `1` to enable startup performance profiling. Writes a JSON timing report to `~/.qwen/startup-perf/` with per-phase durations. | Only active inside the sandbox child process. Zero overhead when not set. Example: `export QWEN_CODE_PROFILE_STARTUP=1` |
+
+## Command-Line Arguments
+
+Arguments passed directly when running the CLI can override other configurations for that specific session.
+
+For sandbox image selection, precedence is:
+`--sandbox-image` > `QWEN_SANDBOX_IMAGE` > `tools.sandboxImage` > built-in default image.
+
+### Command-Line Arguments Table
+
+| Argument | Alias | Description | Possible Values | Notes |
+| ---------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `--model` | `-m` | Specifies the Qwen model to use for this session. | Model name | Example: `npm start -- --model qwen3-coder-plus` |
+| `--prompt` | `-p` | Used to pass a prompt directly to the command. This invokes Qwen Code in a non-interactive mode. | Your prompt text | For scripting examples, use the `--output-format json` flag to get structured output. |
+| `--prompt-interactive` | `-i` | Starts an interactive session with the provided prompt as the initial input. | Your prompt text | The prompt is processed within the interactive session, not before it. Cannot be used when piping input from stdin. Example: `qwen -i "explain this code"` |
+| `--system-prompt` | | Overrides the built-in main session system prompt for this run. | Your prompt text | Loaded context files such as `QWEN.md` are still appended after this override. Can be combined with `--append-system-prompt`. |
+| `--append-system-prompt` | | Appends extra instructions to the main session system prompt for this run. | Your prompt text | Applied after the built-in prompt and loaded context files. Can be combined with `--system-prompt`. See [Headless Mode](../features/headless) for examples. |
+| `--output-format` | `-o` | Specifies the format of the CLI output for non-interactive mode. | `text`, `json`, `stream-json` | `text`: (Default) The standard human-readable output. `json`: A machine-readable JSON output emitted at the end of execution. `stream-json`: Streaming JSON messages emitted as they occur during execution. For structured output and scripting, use the `--output-format json` or `--output-format stream-json` flag. See [Headless Mode](../features/headless) for detailed information. |
+| `--input-format` | | Specifies the format consumed from standard input. | `text`, `stream-json` | `text`: (Default) Standard text input from stdin or command-line arguments. `stream-json`: JSON message protocol via stdin for bidirectional communication. Requirement: `--input-format stream-json` requires `--output-format stream-json` to be set. When using `stream-json`, stdin is reserved for protocol messages. See [Headless Mode](../features/headless) for detailed information. |
+| `--include-partial-messages` | | Include partial assistant messages when using `stream-json` output format. When enabled, emits stream events (message_start, content_block_delta, etc.) as they occur during streaming. | | Default: `false`. Requirement: Requires `--output-format stream-json` to be set. See [Headless Mode](../features/headless) for detailed information about stream events. |
+| `--sandbox` | `-s` | Enables sandbox mode for this session. | | |
+| `--sandbox-image` | | Sets the sandbox image URI. | | |
+| `--debug` | `-d` | Enables debug mode for this session, providing more verbose output. | | |
+| `--all-files` | `-a` | If set, recursively includes all files within the current directory as context for the prompt. | | |
+| `--help` | `-h` | Displays help information about command-line arguments. | | |
+| `--show-memory-usage` | | Displays the current memory usage. | | |
+| `--yolo` | | Enables YOLO mode, which automatically approves all tool calls. | | |
+| `--approval-mode` | | Sets the approval mode for tool calls. | `plan`, `default`, `auto-edit`, `yolo` | Supported modes: `plan`: Analyze only—do not modify files or execute commands. `default`: Require approval for file edits or shell commands (default behavior). `auto-edit`: Automatically approve edit tools (edit, write_file) while prompting for others. `yolo`: Automatically approve all tool calls (equivalent to `--yolo`). Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of `--yolo` for the new unified approach. Example: `qwen --approval-mode auto-edit` See more about [Approval Mode](../features/approval-mode). |
+| `--allowed-tools` | | A comma-separated list of tool names that will bypass the confirmation dialog. | Tool names | Example: `qwen --allowed-tools "Shell(git status)"` |
+| `--disabled-slash-commands` | | Slash command names to hide/disable (comma-separated or repeated). Unioned with the `slashCommands.disabled` setting and the `QWEN_DISABLED_SLASH_COMMANDS` environment variable. Matched case-insensitively against the final command name. | Command names | Example: `qwen --disabled-slash-commands "auth,mcp,extensions"` |
+| `--telemetry` | | Enables [telemetry](/developers/development/telemetry). | | |
+| `--telemetry-target` | | Sets the telemetry target. | | See [telemetry](/developers/development/telemetry) for more information. |
+| `--telemetry-otlp-endpoint` | | Sets the OTLP endpoint for telemetry. | | See [telemetry](../../developers/development/telemetry) for more information. |
+| `--telemetry-otlp-protocol` | | Sets the OTLP protocol for telemetry (`grpc` or `http`). | | Defaults to `grpc`. See [telemetry](../../developers/development/telemetry) for more information. |
+| `--telemetry-log-prompts` | | Enables logging of prompts for telemetry. | | See [telemetry](../../developers/development/telemetry) for more information. |
+| `--checkpointing` | | Enables [checkpointing](../features/checkpointing). | | |
+| `--acp` | | Enables ACP mode (Agent Client Protocol). Useful for IDE/editor integrations like [Zed](../integration-zed). | | Stable. Replaces the deprecated `--experimental-acp` flag. |
+| `--experimental-lsp` | | Enables experimental [LSP (Language Server Protocol)](../features/lsp) feature for code intelligence (go-to-definition, find references, diagnostics, etc.). | | Experimental. Requires language servers to be installed. |
+| `--extensions` | `-e` | Specifies a list of extensions to use for the session. | Extension names | If not provided, all available extensions are used. Use the special term `qwen -e none` to disable all extensions. Example: `qwen -e my-extension -e my-other-extension` |
+| `--list-extensions` | `-l` | Lists all available extensions and exits. | | |
+| `--proxy` | | Sets the proxy for the CLI. | Proxy URL | Example: `--proxy http://localhost:7890`. |
+| `--include-directories` | | Includes additional directories in the workspace for multi-directory support. | Directory paths | Can be specified multiple times or as comma-separated values. 5 directories can be added at maximum. Example: `--include-directories /path/to/project1,/path/to/project2` or `--include-directories /path/to/project1 --include-directories /path/to/project2` |
+| `--screen-reader` | | Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. | | |
+| `--version` | | Displays the version of the CLI. | | |
+| `--openai-logging` | | Enables logging of OpenAI API calls for debugging and analysis. | | This flag overrides the `enableOpenAILogging` setting in `settings.json`. |
+| `--openai-logging-dir` | | Sets a custom directory path for OpenAI API logs. | Directory path | This flag overrides the `openAILoggingDir` setting in `settings.json`. Supports absolute paths, relative paths, and `~` expansion. Example: `qwen --openai-logging-dir "~/qwen-logs" --openai-logging` |
+| `--tavily-api-key` | | Sets the Tavily API key for web search functionality for this session. | API key | Example: `qwen --tavily-api-key tvly-your-api-key-here` |
+
+## Context Files (Hierarchical Instructional Context)
+
+While not strictly configuration for the CLI's _behavior_, context files (defaulting to `QWEN.md` but configurable via the `context.fileName` setting) are crucial for configuring the _instructional context_ (also referred to as "memory"). This powerful feature allows you to give project-specific instructions, coding style guides, or any relevant background information to the AI, making its responses more tailored and accurate to your needs. The CLI includes UI elements, such as an indicator in the footer showing the number of loaded context files, to keep you informed about the active context.
+
+- **Purpose:** These Markdown files contain instructions, guidelines, or context that you want the Qwen model to be aware of during your interactions. The system is designed to manage this instructional context hierarchically.
+
+### Example Context File Content (e.g. `QWEN.md`)
+
+Here's a conceptual example of what a context file at the root of a TypeScript project might contain:
+
+```
+# Project: My Awesome TypeScript Library
+
+## General Instructions:
+- When generating new TypeScript code, please follow the existing coding style.
+- Ensure all new functions and classes have JSDoc comments.
+- Prefer functional programming paradigms where appropriate.
+- All code should be compatible with TypeScript 5.0 and Node.js 20+.
+
+## Coding Style:
+- Use 2 spaces for indentation.
+- Interface names should be prefixed with `I` (e.g., `IUserService`).
+- Private class members should be prefixed with an underscore (`_`).
+- Always use strict equality (`===` and `!==`).
+
+## Specific Component: `src/api/client.ts`
+- This file handles all outbound API requests.
+- When adding new API call functions, ensure they include robust error handling and logging.
+- Use the existing `fetchWithRetry` utility for all GET requests.
+
+## Regarding Dependencies:
+- Avoid introducing new external dependencies unless absolutely necessary.
+- If a new dependency is required, please state the reason.
+```
+
+This example demonstrates how you can provide general project context, specific coding conventions, and even notes about particular files or components. The more relevant and precise your context files are, the better the AI can assist you. Project-specific context files are highly encouraged to establish conventions and context.
+
+- **Hierarchical Loading and Precedence:** The CLI implements a hierarchical memory system by loading context files (e.g., `QWEN.md`) from several locations. Content from files lower in this list (more specific) typically overrides or supplements content from files higher up (more general). The exact concatenation order and final context can be inspected using the `/memory show` command. The typical loading order is:
+ 1. **Global Context File:**
+ - Location: `~/.qwen/` (e.g., `~/.qwen/QWEN.md` in your user home directory).
+ - Scope: Provides default instructions for all your projects.
+ 2. **Project Root & Ancestors Context Files:**
+ - Location: The CLI searches for the configured context file in the current working directory and then in each parent directory up to either the project root (identified by a `.git` folder) or your home directory.
+ - Scope: Provides context relevant to the entire project or a significant portion of it.
+- **Concatenation & UI Indication:** The contents of all found context files are concatenated (with separators indicating their origin and path) and provided as part of the system prompt. The CLI footer displays the count of loaded context files, giving you a quick visual cue about the active instructional context.
+- **Importing Content:** You can modularize your context files by importing other Markdown files using the `@path/to/file.md` syntax. For more details, see the [Memory Import Processor documentation](../configuration/memory).
+- **Commands for Memory Management:**
+ - Use `/memory refresh` to force a re-scan and reload of all context files from all configured locations. This updates the AI's instructional context.
+ - Use `/memory show` to display the combined instructional context currently loaded, allowing you to verify the hierarchy and content being used by the AI.
+ - See the [Commands documentation](../features/commands) for full details on the `/memory` command and its sub-commands (`show` and `refresh`).
+
+By understanding and utilizing these configuration layers and the hierarchical nature of context files, you can effectively manage the AI's memory and tailor Qwen Code's responses to your specific needs and projects.
+
+## Sandbox
+
+Qwen Code can execute potentially unsafe operations (like shell commands and file modifications) within a sandboxed environment to protect your system.
+
+[Sandbox](../features/sandbox) is disabled by default, but you can enable it in a few ways:
+
+- Using `--sandbox` or `-s` flag.
+- Setting `QWEN_SANDBOX` environment variable.
+- Sandbox is enabled when using `--yolo` or `--approval-mode=yolo` by default.
+
+By default, it uses a pre-built `qwen-code-sandbox` Docker image.
+
+For project-specific sandboxing needs, you can create a custom Dockerfile at `.qwen/sandbox.Dockerfile` in your project's root directory. This Dockerfile can be based on the base sandbox image:
+
+```
+FROM qwen-code-sandbox
+# Add your custom dependencies or configurations here
+# For example:
+# RUN apt-get update && apt-get install -y some-package
+# COPY ./my-config /app/my-config
+```
+
+When `.qwen/sandbox.Dockerfile` exists, you can use `BUILD_SANDBOX` environment variable when running Qwen Code to automatically build the custom sandbox image:
+
+```
+BUILD_SANDBOX=1 qwen -s
+```
+
+## Usage Statistics
+
+To help us improve Qwen Code, we collect anonymized usage statistics. This data helps us understand how the CLI is used, identify common issues, and prioritize new features.
+
+**What we collect:**
+
+- **Tool Calls:** We log the names of the tools that are called, whether they succeed or fail, and how long they take to execute. We do not collect the arguments passed to the tools or any data returned by them.
+- **API Requests:** We log the model used for each request, the duration of the request, and whether it was successful. We do not collect the content of the prompts or responses.
+- **Session Information:** We collect information about the configuration of the CLI, such as the enabled tools and the approval mode.
+
+**What we DON'T collect:**
+
+- **Personally Identifiable Information (PII):** We do not collect any personal information, such as your name, email address, or API keys.
+- **Prompt and Response Content:** We do not log the content of your prompts or the responses from the model.
+- **File Content:** We do not log the content of any files that are read or written by the CLI.
+
+**How to opt out:**
+
+You can opt out of usage statistics collection at any time by setting the `usageStatisticsEnabled` property to `false` under the `privacy` category in your `settings.json` file:
+
+```
+{
+ "privacy": {
+ "usageStatisticsEnabled": false
+ }
+}
+```
+
+> [!note]
+>
+> When usage statistics are enabled, events are sent to an Alibaba Cloud RUM collection endpoint.
diff --git a/packages/cli/src/commands/auth/handler.ts b/packages/cli/src/commands/auth/handler.ts
index 612d81a2b..1134602b3 100644
--- a/packages/cli/src/commands/auth/handler.ts
+++ b/packages/cli/src/commands/auth/handler.ts
@@ -106,6 +106,7 @@ export async function handleQwenAuth(
maxSessionTurns: undefined,
coreTools: undefined,
excludeTools: undefined,
+ disabledSlashCommands: undefined,
authType: undefined,
channel: undefined,
systemPrompt: undefined,
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 8683e897d..73a9ca606 100755
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -160,6 +160,7 @@ export interface CliArgs {
maxSessionTurns: number | undefined;
coreTools: string[] | undefined;
excludeTools: string[] | undefined;
+ disabledSlashCommands: string[] | undefined;
authType: string | undefined;
channel: string | undefined;
}
@@ -507,6 +508,17 @@ export async function parseArguments(): Promise {
coerce: (tools: string[]) =>
tools.flatMap((tool) => tool.split(',').map((t) => t.trim())),
})
+ .option('disabled-slash-commands', {
+ type: 'array',
+ string: true,
+ description:
+ 'Slash command names to hide/disable (comma-separated or ' +
+ 'repeated). Merged with the `slashCommands.disabled` setting ' +
+ 'and QWEN_DISABLED_SLASH_COMMANDS. Matched case-insensitively ' +
+ 'against the final command name.',
+ coerce: (names: string[]) =>
+ names.flatMap((n) => n.split(',').map((t) => t.trim())),
+ })
.option('auth-type', {
type: 'string',
choices: [
@@ -887,6 +899,29 @@ export async function loadCliConfig(
if (t && !mergedDeny.includes(t)) mergedDeny.push(t);
}
+ // Merge the slash-command denylist from settings + CLI flag + env var.
+ // Settings merge (UNION across scopes) is already handled upstream; we
+ // only de-duplicate while preserving case for diagnostic purposes.
+ const disabledSlashCommands: string[] = [];
+ const seenDisabled = new Set();
+ const addDisabled = (value: string | undefined) => {
+ if (!value) return;
+ const trimmed = value.trim();
+ if (!trimmed) return;
+ const key = trimmed.toLowerCase();
+ if (!seenDisabled.has(key)) {
+ seenDisabled.add(key);
+ disabledSlashCommands.push(trimmed);
+ }
+ };
+ for (const name of settings.slashCommands?.disabled ?? []) addDisabled(name);
+ for (const name of argv.disabledSlashCommands ?? []) addDisabled(name);
+ for (const name of (process.env['QWEN_DISABLED_SLASH_COMMANDS'] ?? '').split(
+ ',',
+ )) {
+ addDisabled(name);
+ }
+
// Helper: check if a tool is explicitly covered by an allow rule OR by the
// coreTools whitelist. Uses alias matching for coreTools (via isToolEnabled)
// to preserve the original behaviour where "ShellTool", "Shell", and
@@ -1041,6 +1076,8 @@ export async function loadCliConfig(
coreTools: argv.coreTools || settings.tools?.core || undefined,
allowedTools: argv.allowedTools || settings.tools?.allowed || undefined,
excludeTools: mergedDeny,
+ disabledSlashCommands:
+ disabledSlashCommands.length > 0 ? disabledSlashCommands : undefined,
// New unified permissions (PermissionManager source of truth).
permissions: {
allow: mergedAllow.length > 0 ? mergedAllow : undefined,
diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts
index f80404233..acea53a81 100644
--- a/packages/cli/src/config/settings.test.ts
+++ b/packages/cli/src/config/settings.test.ts
@@ -858,6 +858,34 @@ describe('Settings Loading and Merging', () => {
expect(settings.merged.advanced?.excludedEnvVars).toHaveLength(2);
});
+ it('should UNION-merge slashCommands.disabled across user and workspace scopes', () => {
+ (mockFsExistsSync as Mock).mockReturnValue(true);
+ const userSettings = {
+ slashCommands: { disabled: ['auth', 'quit'] },
+ };
+ const workspaceSettings = {
+ // Workspace overlaps with user and adds one entry. UNION de-dupes the
+ // overlap and merges the new entry; it cannot remove user entries.
+ slashCommands: { disabled: ['quit', 'clear'] },
+ };
+
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettings);
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
+ return JSON.stringify(workspaceSettings);
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ const disabled = settings.merged.slashCommands?.disabled ?? [];
+ expect(disabled).toEqual(
+ expect.arrayContaining(['auth', 'quit', 'clear']),
+ );
+ expect(disabled).toHaveLength(3);
+ });
+
it('should merge all settings files with the correct precedence', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
const systemDefaultsContent = {
diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts
index 83688b5be..268f6c09c 100644
--- a/packages/cli/src/config/settingsSchema.ts
+++ b/packages/cli/src/config/settingsSchema.ts
@@ -1016,6 +1016,36 @@ const SETTINGS_SCHEMA = {
},
},
+ 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"). ' +
+ '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,
+ },
+ },
+ },
+
tools: {
type: 'object',
label: 'Tools',
diff --git a/packages/cli/src/core/theme.test.ts b/packages/cli/src/core/theme.test.ts
new file mode 100644
index 000000000..a94c67c44
--- /dev/null
+++ b/packages/cli/src/core/theme.test.ts
@@ -0,0 +1,72 @@
+/**
+ * @license
+ * Copyright 2025 Qwen Code
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { validateTheme } from './theme.js';
+
+const mockFindThemeByName = vi.fn();
+vi.mock('../ui/themes/theme-manager.js', () => ({
+ themeManager: {
+ findThemeByName: (...args: unknown[]) => mockFindThemeByName(...args),
+ },
+ AUTO_THEME_NAME: 'auto',
+}));
+
+vi.mock('../i18n/index.js', () => ({
+ t: (msg: string, params?: Record) => {
+ if (params) {
+ return msg.replace(
+ /\{\{(\w+)\}\}/g,
+ (_, key) => params[key] ?? `{{${key}}}`,
+ );
+ }
+ return msg;
+ },
+}));
+
+describe('validateTheme', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should return null when no theme is configured', () => {
+ const settings = { merged: { ui: {} } };
+ const result = validateTheme(settings as never);
+ expect(result).toBeNull();
+ });
+
+ it('should return null when theme is found', () => {
+ mockFindThemeByName.mockReturnValue({ name: 'dark' });
+ const settings = { merged: { ui: { theme: 'dark' } } };
+
+ const result = validateTheme(settings as never);
+
+ expect(result).toBeNull();
+ expect(mockFindThemeByName).toHaveBeenCalledWith('dark');
+ });
+
+ it('should return error message when theme is not found', () => {
+ mockFindThemeByName.mockReturnValue(undefined);
+ const settings = { merged: { ui: { theme: 'nonexistent-theme' } } };
+
+ const result = validateTheme(settings as never);
+
+ expect(result).toBe('Theme "nonexistent-theme" not found.');
+ });
+
+ it('should return null when ui section is undefined', () => {
+ const settings = { merged: {} };
+ const result = validateTheme(settings as never);
+ expect(result).toBeNull();
+ });
+
+ it('should return null when theme is set to auto', () => {
+ const settings = { merged: { ui: { theme: 'auto' } } };
+ const result = validateTheme(settings as never);
+ expect(result).toBeNull();
+ expect(mockFindThemeByName).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/cli/src/core/theme.ts b/packages/cli/src/core/theme.ts
index 7acb4abd2..11123a16b 100644
--- a/packages/cli/src/core/theme.ts
+++ b/packages/cli/src/core/theme.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { themeManager } from '../ui/themes/theme-manager.js';
+import { themeManager, AUTO_THEME_NAME } from '../ui/themes/theme-manager.js';
import { type LoadedSettings } from '../config/settings.js';
import { t } from '../i18n/index.js';
@@ -15,7 +15,11 @@ import { t } from '../i18n/index.js';
*/
export function validateTheme(settings: LoadedSettings): string | null {
const effectiveTheme = settings.merged.ui?.theme;
- if (effectiveTheme && !themeManager.findThemeByName(effectiveTheme)) {
+ if (
+ effectiveTheme &&
+ effectiveTheme !== AUTO_THEME_NAME &&
+ !themeManager.findThemeByName(effectiveTheme)
+ ) {
return t('Theme "{{themeName}}" not found.', {
themeName: effectiveTheme,
});
diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx
index 62f83deb3..7e5a7a260 100644
--- a/packages/cli/src/gemini.test.tsx
+++ b/packages/cli/src/gemini.test.tsx
@@ -277,18 +277,16 @@ describe('gemini.tsx main function', () => {
throw new MockProcessExitError(code);
});
- const { loadCliConfig, parseArguments } = await import(
- './config/config.js'
- );
+ const { loadCliConfig, parseArguments } =
+ await import('./config/config.js');
const { loadSettings } = await import('./config/settings.js');
const cleanupModule = await import('./utils/cleanup.js');
const validatorModule = await import('./validateNonInterActiveAuth.js');
const streamJsonModule = await import('./nonInteractive/session.js');
const initializerModule = await import('./core/initializer.js');
const startupWarningsModule = await import('./utils/startupWarnings.js');
- const userStartupWarningsModule = await import(
- './utils/userStartupWarnings.js'
- );
+ const userStartupWarningsModule =
+ await import('./utils/userStartupWarnings.js');
vi.mocked(cleanupModule.cleanupCheckpoints).mockResolvedValue(undefined);
vi.mocked(cleanupModule.registerCleanup).mockImplementation(() => {});
@@ -427,12 +425,10 @@ describe('gemini.tsx main function kitty protocol', () => {
});
it('should call setRawMode and detectAndEnableKittyProtocol when isInteractive is true', async () => {
- const { detectAndEnableKittyProtocol } = await import(
- './ui/utils/kittyProtocolDetector.js'
- );
- const { loadCliConfig, parseArguments } = await import(
- './config/config.js'
- );
+ const { detectAndEnableKittyProtocol } =
+ await import('./ui/utils/kittyProtocolDetector.js');
+ const { loadCliConfig, parseArguments } =
+ await import('./config/config.js');
const { loadSettings } = await import('./config/settings.js');
vi.mocked(loadCliConfig).mockResolvedValue({
isInteractive: () => true,
@@ -503,6 +499,7 @@ describe('gemini.tsx main function kitty protocol', () => {
resume: undefined,
coreTools: undefined,
excludeTools: undefined,
+ disabledSlashCommands: undefined,
authType: undefined,
maxSessionTurns: undefined,
experimentalLsp: undefined,
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index a8ebca80c..7465a271a 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -38,7 +38,7 @@ import { SettingsContext } from './ui/contexts/SettingsContext.js';
import { VimModeProvider } from './ui/contexts/VimModeContext.js';
import { AgentViewProvider } from './ui/contexts/AgentViewContext.js';
import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js';
-import { themeManager } from './ui/themes/theme-manager.js';
+import { themeManager, AUTO_THEME_NAME } from './ui/themes/theme-manager.js';
import { detectAndEnableKittyProtocol } from './ui/utils/kittyProtocolDetector.js';
import { checkForUpdates } from './ui/utils/updateCheck.js';
import {
@@ -259,14 +259,21 @@ export async function main() {
// Load custom themes from settings
themeManager.loadCustomThemes(settings.merged.ui?.customThemes);
- if (settings.merged.ui?.theme) {
- if (!themeManager.setActiveTheme(settings.merged.ui?.theme)) {
+ const configuredTheme = settings.merged.ui?.theme;
+ if (configuredTheme && configuredTheme !== AUTO_THEME_NAME) {
+ if (!themeManager.setActiveTheme(configuredTheme)) {
// If the theme is not found during initial load, log a warning and continue.
// The useThemeCommand hook in AppContainer.tsx will handle opening the dialog.
- writeStderrLine(
- `Warning: Theme "${settings.merged.ui?.theme}" not found.`,
- );
+ writeStderrLine(`Warning: Theme "${configuredTheme}" not found.`);
}
+ } else {
+ // 'auto' or unset: resolve a synchronous baseline (COLORFGBG + macOS)
+ // so non-interactive runs and any pre-render UI (e.g. the --resume
+ // session picker) already have a sensible theme. The interactive
+ // startup block refines this with an OSC 11 probe later on, which is
+ // intentionally deferred to run inside the early-capture window so
+ // terminal response bytes cannot leak into the TUI input.
+ themeManager.setActiveTheme(AUTO_THEME_NAME);
}
// hop into sandbox if we are outside and sandboxing is enabled
@@ -403,6 +410,7 @@ export async function main() {
const wasRaw = process.stdin.isRaw;
let kittyProtocolDetectionComplete: Promise | undefined;
+ let themeAutoDetectionComplete: Promise | undefined;
if (config.isInteractive() && !wasRaw && process.stdin.isTTY) {
// Set this as early as possible to avoid spurious characters from
// input showing up in the output.
@@ -418,6 +426,24 @@ export async function main() {
// Detect and enable Kitty keyboard protocol once at startup.
kittyProtocolDetectionComplete = detectAndEnableKittyProtocol();
+
+ // Auto-detect theme (OSC 11 + COLORFGBG + macOS) when the user has
+ // opted into 'auto' or has not configured a theme at all. Kicked off
+ // here without awaiting so the OSC 11 timeout overlaps with the
+ // heavier startup work below (initializeApp, warnings) instead of
+ // blocking the critical path. The synchronous baseline picked above
+ // keeps the active theme valid in the meantime; this probe only
+ // refines it. Running inside the early-capture window is deliberate:
+ // the filter in startEarlyInputCapture absorbs the OSC 11 response
+ // bytes so they cannot leak into the TUI input, even though our
+ // probe attaches its own listener to parse the RGB value.
+ if (!configuredTheme || configuredTheme === AUTO_THEME_NAME) {
+ themeAutoDetectionComplete = themeManager
+ .resolveAutoThemeAsync()
+ .catch((err) => {
+ debugLogger.warn('Async theme auto-detection failed:', err);
+ });
+ }
}
setMaxSizedBoxDebugging(isDebugMode);
@@ -456,6 +482,11 @@ export async function main() {
if (config.isInteractive()) {
// Need kitty detection to be complete before we can start the interactive UI.
await kittyProtocolDetectionComplete;
+ // Drain the auto-theme probe before render so the OSC 11 response is
+ // absorbed by the early-capture filter (which is closed inside
+ // startInteractiveUI) and so the first paint uses the refined theme
+ // when the probe finishes in time.
+ await themeAutoDetectionComplete;
await startInteractiveUI(
config,
settings,
diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js
index 602fe03a9..01f1ef3c8 100644
--- a/packages/cli/src/i18n/locales/de.js
+++ b/packages/cli/src/i18n/locales/de.js
@@ -346,6 +346,8 @@ export default {
'Tool Schema Compliance': 'Werkzeug-Schema-Konformität',
// Settings enum options
'Auto (detect from system)': 'Automatisch (vom System erkennen)',
+ 'Auto (detect terminal theme)': 'Automatisch (Terminal-Theme erkennen)',
+ Auto: 'Automatisch',
Text: 'Text',
JSON: 'JSON',
Plan: 'Plan',
diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js
index 1a7b92b9c..07856355a 100644
--- a/packages/cli/src/i18n/locales/en.js
+++ b/packages/cli/src/i18n/locales/en.js
@@ -430,6 +430,8 @@ export default {
'Tool Schema Compliance': 'Tool Schema Compliance',
// Settings enum options
'Auto (detect from system)': 'Auto (detect from system)',
+ 'Auto (detect terminal theme)': 'Auto (detect terminal theme)',
+ Auto: 'Auto',
Text: 'Text',
JSON: 'JSON',
Plan: 'Plan',
diff --git a/packages/cli/src/i18n/locales/fr.js b/packages/cli/src/i18n/locales/fr.js
new file mode 100644
index 000000000..138f7bf8d
--- /dev/null
+++ b/packages/cli/src/i18n/locales/fr.js
@@ -0,0 +1,2097 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// Traductions françaises pour Qwen Code CLI
+
+export default {
+ // ============================================================================
+ // Aide / Composants UI
+ // ============================================================================
+ '↑ to manage attachments': '↑ pour gérer les pièces jointes',
+ '← → select, Delete to remove, ↓ to exit':
+ '← → sélectionner, Suppr pour retirer, ↓ pour quitter',
+ 'Attachments: ': 'Pièces jointes : ',
+
+ 'Basics:': 'Bases :',
+ 'Add context': 'Ajouter du contexte',
+ 'Use {{symbol}} to specify files for context (e.g., {{example}}) to target specific files or folders.':
+ 'Utilisez {{symbol}} pour spécifier des fichiers de contexte (ex. {{example}}) pour cibler des fichiers ou dossiers spécifiques.',
+ '@': '@',
+ '@src/myFile.ts': '@src/myFile.ts',
+ 'Shell mode': 'Mode shell',
+ 'YOLO mode': 'Mode YOLO',
+ 'plan mode': 'mode plan',
+ 'auto-accept edits': 'acceptation automatique des modifications',
+ 'Accepting edits': 'Acceptation des modifications',
+ '(shift + tab to cycle)': '(maj + tab pour cycler)',
+ '(tab to cycle)': '(tab pour cycler)',
+ 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).':
+ 'Exécutez des commandes shell via {{symbol}} (ex. {{example1}}) ou utilisez le langage naturel (ex. {{example2}}).',
+ '!': '!',
+ '!npm run start': '!npm run start',
+ 'start server': 'démarrer le serveur',
+ 'Commands:': 'Commandes :',
+ 'shell command': 'commande shell',
+ 'Model Context Protocol command (from external servers)':
+ 'Commande Model Context Protocol (depuis des serveurs externes)',
+ 'Keyboard Shortcuts:': 'Raccourcis clavier :',
+ 'Toggle this help display': 'Afficher/masquer cette aide',
+ 'Toggle shell mode': 'Basculer le mode shell',
+ 'Open command menu': 'Ouvrir le menu des commandes',
+ 'Add file context': 'Ajouter un contexte de fichier',
+ 'Accept suggestion / Autocomplete': 'Accepter la suggestion / Autocomplétion',
+ 'Reverse search history': "Recherche inversée dans l'historique",
+ 'Press ? again to close': 'Appuyez à nouveau sur ? pour fermer',
+ 'for shell mode': 'pour le mode shell',
+ 'for commands': 'pour les commandes',
+ 'for file paths': 'pour les chemins de fichiers',
+ 'to clear input': "pour effacer l'entrée",
+ 'to cycle approvals': 'pour cycler les approbations',
+ 'to quit': 'pour quitter',
+ 'for newline': 'pour une nouvelle ligne',
+ 'to clear screen': "pour effacer l'écran",
+ 'to search history': "pour rechercher dans l'historique",
+ 'to paste images': 'pour coller des images',
+ 'for external editor': 'pour un éditeur externe',
+ 'Jump through words in the input': "Sauter de mot en mot dans l'entrée",
+ 'Close dialogs, cancel requests, or quit application':
+ "Fermer les boîtes de dialogue, annuler les requêtes ou quitter l'application",
+ 'New line': 'Nouvelle ligne',
+ 'New line (Alt+Enter works for certain linux distros)':
+ 'Nouvelle ligne (Alt+Entrée fonctionne sur certaines distributions Linux)',
+ 'Clear the screen': "Effacer l'écran",
+ 'Open input in external editor': "Ouvrir l'entrée dans un éditeur externe",
+ 'Send message': 'Envoyer le message',
+ 'Initializing...': 'Initialisation...',
+ 'Connecting to MCP servers... ({{connected}}/{{total}})':
+ 'Connexion aux serveurs MCP... ({{connected}}/{{total}})',
+ 'Type your message or @path/to/file':
+ 'Tapez votre message ou @chemin/vers/fichier',
+ '? for shortcuts': '? pour les raccourcis',
+ "Press 'i' for INSERT mode and 'Esc' for NORMAL mode.":
+ "Appuyez sur 'i' pour le mode INSERTION et 'Échap' pour le mode NORMAL.",
+ 'Cancel operation / Clear input (double press)':
+ "Annuler l'opération / Effacer l'entrée (double appui)",
+ 'Cycle approval modes': "Cycler les modes d'approbation",
+ 'Cycle through your prompt history': "Parcourir l'historique des invites",
+ 'For a full list of shortcuts, see {{docPath}}':
+ 'Pour la liste complète des raccourcis, voir {{docPath}}',
+ 'docs/keyboard-shortcuts.md': 'docs/keyboard-shortcuts.md',
+ 'for help on Qwen Code': "pour l'aide de Qwen Code",
+ 'show version info': 'afficher les informations de version',
+ 'submit a bug report': 'soumettre un rapport de bogue',
+ 'About Qwen Code': 'À propos de Qwen Code',
+ Status: 'Statut',
+
+ // ============================================================================
+ // Informations système
+ // ============================================================================
+ 'Qwen Code': 'Qwen Code',
+ Runtime: 'Environnement',
+ OS: 'OS',
+ Auth: 'Auth',
+ 'CLI Version': 'Version CLI',
+ 'Git Commit': 'Commit Git',
+ Model: 'Modèle',
+ 'Fast Model': 'Modèle rapide',
+ Sandbox: 'Bac à sable',
+ 'OS Platform': 'Plateforme OS',
+ 'OS Arch': 'Architecture OS',
+ 'OS Release': 'Version OS',
+ 'Node.js Version': 'Version Node.js',
+ 'NPM Version': 'Version NPM',
+ 'Session ID': 'ID de session',
+ 'Auth Method': "Méthode d'authentification",
+ 'Base URL': 'URL de base',
+ Proxy: 'Proxy',
+ 'Memory Usage': 'Utilisation mémoire',
+ 'IDE Client': 'Client IDE',
+
+ // ============================================================================
+ // Commandes - Général
+ // ============================================================================
+ 'Analyzes the project and creates a tailored QWEN.md file.':
+ 'Analyse le projet et crée un fichier QWEN.md personnalisé.',
+ 'List available Qwen Code tools. Usage: /tools [desc]':
+ 'Lister les outils Qwen Code disponibles. Utilisation : /tools [desc]',
+ 'List available skills.': 'Lister les compétences disponibles.',
+ 'Available Qwen Code CLI tools:': 'Outils Qwen Code CLI disponibles :',
+ 'No tools available': 'Aucun outil disponible',
+ 'View or change the approval mode for tool usage':
+ "Voir ou modifier le mode d'approbation pour l'utilisation des outils",
+ 'Invalid approval mode "{{arg}}". Valid modes: {{modes}}':
+ 'Mode d\'approbation invalide "{{arg}}". Modes valides : {{modes}}',
+ 'Approval mode set to "{{mode}}"':
+ 'Mode d\'approbation défini sur "{{mode}}"',
+ 'View or change the language setting':
+ 'Voir ou modifier le paramètre de langue',
+ 'change the theme': 'changer le thème',
+ 'Select Theme': 'Sélectionner un thème',
+ Preview: 'Aperçu',
+ '(Use Enter to select, Tab to configure scope)':
+ '(Utilisez Entrée pour sélectionner, Tab pour configurer la portée)',
+ '(Use Enter to apply scope, Tab to go back)':
+ '(Utilisez Entrée pour appliquer la portée, Tab pour revenir)',
+ 'Theme configuration unavailable due to NO_COLOR env variable.':
+ "Configuration du thème indisponible en raison de la variable d'environnement NO_COLOR.",
+ 'Theme "{{themeName}}" not found.': 'Thème "{{themeName}}" introuvable.',
+ 'Theme "{{themeName}}" not found in selected scope.':
+ 'Thème "{{themeName}}" introuvable dans la portée sélectionnée.',
+ 'Clear conversation history and free up context':
+ "Effacer l'historique de conversation et libérer le contexte",
+ 'Compresses the context by replacing it with a summary.':
+ 'Compresse le contexte en le remplaçant par un résumé.',
+ 'open full Qwen Code documentation in your browser':
+ 'ouvrir la documentation complète de Qwen Code dans votre navigateur',
+ 'Configuration not available.': 'Configuration non disponible.',
+ 'change the auth method': "changer la méthode d'authentification",
+ 'Configure authentication information for login':
+ "Configurer les informations d'authentification pour la connexion",
+ 'Copy the last result or code snippet to clipboard':
+ 'Copier le dernier résultat ou extrait de code dans le presse-papiers',
+
+ // ============================================================================
+ // Commandes - Agents
+ // ============================================================================
+ 'Manage subagents for specialized task delegation.':
+ 'Gérer les sous-agents pour la délégation de tâches spécialisées.',
+ 'Manage existing subagents (view, edit, delete).':
+ 'Gérer les sous-agents existants (voir, modifier, supprimer).',
+ 'Create a new subagent with guided setup.':
+ 'Créer un nouveau sous-agent avec configuration guidée.',
+
+ // ============================================================================
+ // Agents - Boîte de dialogue de gestion
+ // ============================================================================
+ Agents: 'Agents',
+ 'Choose Action': 'Choisir une action',
+ 'Edit {{name}}': 'Modifier {{name}}',
+ 'Edit Tools: {{name}}': 'Modifier les outils : {{name}}',
+ 'Edit Color: {{name}}': 'Modifier la couleur : {{name}}',
+ 'Delete {{name}}': 'Supprimer {{name}}',
+ 'Unknown Step': 'Étape inconnue',
+ 'Esc to close': 'Échap pour fermer',
+ 'Enter to select, ↑↓ to navigate, Esc to close':
+ 'Entrée pour sélectionner, ↑↓ pour naviguer, Échap pour fermer',
+ 'Esc to go back': 'Échap pour revenir',
+ 'Enter to confirm, Esc to cancel':
+ 'Entrée pour confirmer, Échap pour annuler',
+ 'Enter to select, ↑↓ to navigate, Esc to go back':
+ 'Entrée pour sélectionner, ↑↓ pour naviguer, Échap pour revenir',
+ 'Enter to submit, Esc to go back':
+ 'Entrée pour soumettre, Échap pour revenir',
+ 'Invalid step: {{step}}': 'Étape invalide : {{step}}',
+ 'No subagents found.': 'Aucun sous-agent trouvé.',
+ "Use '/agents create' to create your first subagent.":
+ "Utilisez '/agents create' pour créer votre premier sous-agent.",
+ '(built-in)': '(intégré)',
+ '(overridden by project level agent)':
+ '(remplacé par un agent au niveau du projet)',
+ 'Project Level ({{path}})': 'Niveau projet ({{path}})',
+ 'User Level ({{path}})': 'Niveau utilisateur ({{path}})',
+ 'Built-in Agents': 'Agents intégrés',
+ 'Extension Agents': "Agents d'extension",
+ 'Using: {{count}} agents': 'Utilisation : {{count}} agents',
+ 'View Agent': "Voir l'agent",
+ 'Edit Agent': "Modifier l'agent",
+ 'Delete Agent': "Supprimer l'agent",
+ Back: 'Retour',
+ 'No agent selected': 'Aucun agent sélectionné',
+ 'File Path: ': 'Chemin du fichier : ',
+ 'Tools: ': 'Outils : ',
+ 'Color: ': 'Couleur : ',
+ 'Description:': 'Description :',
+ 'System Prompt:': 'Invite système :',
+ 'Open in editor': "Ouvrir dans l'éditeur",
+ 'Edit tools': 'Modifier les outils',
+ 'Edit color': 'Modifier la couleur',
+ '❌ Error:': '❌ Erreur :',
+ 'Are you sure you want to delete agent "{{name}}"?':
+ 'Êtes-vous sûr de vouloir supprimer l\'agent "{{name}}" ?',
+
+ // ============================================================================
+ // Agents - Assistant de création
+ // ============================================================================
+ 'Project Level (.qwen/agents/)': 'Niveau projet (.qwen/agents/)',
+ 'User Level (~/.qwen/agents/)': 'Niveau utilisateur (~/.qwen/agents/)',
+ '✅ Subagent Created Successfully!': '✅ Sous-agent créé avec succès !',
+ 'Subagent "{{name}}" has been saved to {{level}} level.':
+ 'Le sous-agent "{{name}}" a été enregistré au niveau {{level}}.',
+ 'Name: ': 'Nom : ',
+ 'Location: ': 'Emplacement : ',
+ '❌ Error saving subagent:':
+ '❌ Erreur lors de la sauvegarde du sous-agent :',
+ 'Warnings:': 'Avertissements :',
+ 'Name "{{name}}" already exists at {{level}} level - will overwrite existing subagent':
+ 'Le nom "{{name}}" existe déjà au niveau {{level}} - le sous-agent existant sera écrasé',
+ 'Name "{{name}}" exists at user level - project level will take precedence':
+ 'Le nom "{{name}}" existe au niveau utilisateur - le niveau projet aura la priorité',
+ 'Name "{{name}}" exists at project level - existing subagent will take precedence':
+ 'Le nom "{{name}}" existe au niveau projet - le sous-agent existant aura la priorité',
+ 'Description is over {{length}} characters':
+ 'La description dépasse {{length}} caractères',
+ 'System prompt is over {{length}} characters':
+ "L'invite système dépasse {{length}} caractères",
+ 'Step {{n}}: Choose Location': "Étape {{n}} : Choisir l'emplacement",
+ 'Step {{n}}: Choose Generation Method':
+ 'Étape {{n}} : Choisir la méthode de génération',
+ 'Generate with Qwen Code (Recommended)':
+ 'Générer avec Qwen Code (Recommandé)',
+ 'Manual Creation': 'Création manuelle',
+ 'Describe what this subagent should do and when it should be used. (Be comprehensive for best results)':
+ 'Décrivez ce que ce sous-agent doit faire et quand il doit être utilisé. (Soyez complet pour de meilleurs résultats)',
+ 'e.g., Expert code reviewer that reviews code based on best practices...':
+ 'ex. Réviseur de code expert qui révise le code selon les meilleures pratiques...',
+ 'Generating subagent configuration...':
+ 'Génération de la configuration du sous-agent...',
+ 'Failed to generate subagent: {{error}}':
+ 'Échec de la génération du sous-agent : {{error}}',
+ 'Step {{n}}: Describe Your Subagent':
+ 'Étape {{n}} : Décrire votre sous-agent',
+ 'Step {{n}}: Enter Subagent Name':
+ 'Étape {{n}} : Entrer le nom du sous-agent',
+ 'Step {{n}}: Enter System Prompt': "Étape {{n}} : Entrer l'invite système",
+ 'Step {{n}}: Enter Description': 'Étape {{n}} : Entrer la description',
+ 'Step {{n}}: Select Tools': 'Étape {{n}} : Sélectionner les outils',
+ 'All Tools (Default)': 'Tous les outils (par défaut)',
+ 'All Tools': 'Tous les outils',
+ 'Read-only Tools': 'Outils en lecture seule',
+ 'Read & Edit Tools': 'Outils lecture et édition',
+ 'Read & Edit & Execution Tools': 'Outils lecture, édition et exécution',
+ 'All tools selected, including MCP tools':
+ 'Tous les outils sélectionnés, y compris les outils MCP',
+ 'Selected tools:': 'Outils sélectionnés :',
+ 'Read-only tools:': 'Outils en lecture seule :',
+ 'Edit tools:': "Outils d'édition :",
+ 'Execution tools:': "Outils d'exécution :",
+ 'Step {{n}}: Choose Background Color':
+ "Étape {{n}} : Choisir la couleur d'arrière-plan",
+ 'Step {{n}}: Confirm and Save': 'Étape {{n}} : Confirmer et enregistrer',
+ 'Esc to cancel': 'Échap pour annuler',
+ 'Press Enter to save, e to save and edit, Esc to go back':
+ 'Appuyez sur Entrée pour enregistrer, e pour enregistrer et modifier, Échap pour revenir',
+ 'Press Enter to continue, {{navigation}}Esc to {{action}}':
+ 'Appuyez sur Entrée pour continuer, {{navigation}}Échap pour {{action}}',
+ cancel: 'annuler',
+ 'go back': 'revenir',
+ '↑↓ to navigate, ': '↑↓ pour naviguer, ',
+ 'Enter a clear, unique name for this subagent.':
+ 'Entrez un nom clair et unique pour ce sous-agent.',
+ 'e.g., Code Reviewer': 'ex. Réviseur de code',
+ 'Name cannot be empty.': 'Le nom ne peut pas être vide.',
+ "Write the system prompt that defines this subagent's behavior. Be comprehensive for best results.":
+ "Rédigez l'invite système qui définit le comportement de ce sous-agent. Soyez complet pour de meilleurs résultats.",
+ 'e.g., You are an expert code reviewer...':
+ 'ex. Vous êtes un réviseur de code expert...',
+ 'System prompt cannot be empty.': "L'invite système ne peut pas être vide.",
+ 'Describe when and how this subagent should be used.':
+ 'Décrivez quand et comment ce sous-agent doit être utilisé.',
+ 'e.g., Reviews code for best practices and potential bugs.':
+ 'ex. Révise le code pour les meilleures pratiques et les bogues potentiels.',
+ 'Description cannot be empty.': 'La description ne peut pas être vide.',
+ 'Failed to launch editor: {{error}}':
+ "Échec du lancement de l'éditeur : {{error}}",
+ 'Failed to save and edit subagent: {{error}}':
+ 'Échec de la sauvegarde et modification du sous-agent : {{error}}',
+
+ // ============================================================================
+ // Extensions - Boîte de dialogue de gestion
+ // ============================================================================
+ 'Manage Extensions': 'Gérer les extensions',
+ 'Extension Details': "Détails de l'extension",
+ 'View Extension': "Voir l'extension",
+ 'Update Extension': "Mettre à jour l'extension",
+ 'Disable Extension': "Désactiver l'extension",
+ 'Enable Extension': "Activer l'extension",
+ 'Uninstall Extension': "Désinstaller l'extension",
+ 'Select Scope': 'Sélectionner la portée',
+ 'User Scope': 'Portée utilisateur',
+ 'Workspace Scope': 'Portée espace de travail',
+ 'No extensions found.': 'Aucune extension trouvée.',
+ Active: 'Actif',
+ Disabled: 'Désactivé',
+ 'Update available': 'Mise à jour disponible',
+ 'Up to date': 'À jour',
+ 'Checking...': 'Vérification...',
+ 'Updating...': 'Mise à jour...',
+ Unknown: 'Inconnu',
+ Error: 'Erreur',
+ 'Version:': 'Version :',
+ 'Status:': 'Statut :',
+ 'Are you sure you want to uninstall extension "{{name}}"?':
+ 'Êtes-vous sûr de vouloir désinstaller l\'extension "{{name}}" ?',
+ 'This action cannot be undone.': 'Cette action est irréversible.',
+ 'Extension "{{name}}" disabled successfully.':
+ 'Extension "{{name}}" désactivée avec succès.',
+ 'Extension "{{name}}" enabled successfully.':
+ 'Extension "{{name}}" activée avec succès.',
+ 'Extension "{{name}}" updated successfully.':
+ 'Extension "{{name}}" mise à jour avec succès.',
+ 'Failed to update extension "{{name}}": {{error}}':
+ 'Échec de la mise à jour de l\'extension "{{name}}" : {{error}}',
+ 'Select the scope for this action:':
+ 'Sélectionnez la portée pour cette action :',
+ 'User - Applies to all projects':
+ "Utilisateur - S'applique à tous les projets",
+ 'Workspace - Applies to current project only':
+ "Espace de travail - S'applique uniquement au projet actuel",
+ 'Name:': 'Nom :',
+ 'MCP Servers:': 'Serveurs MCP :',
+ 'Settings:': 'Paramètres :',
+ active: 'actif',
+ disabled: 'désactivé',
+ 'View Details': 'Voir les détails',
+ 'Update failed:': 'Échec de la mise à jour :',
+ 'Updating {{name}}...': 'Mise à jour de {{name}}...',
+ 'Update complete!': 'Mise à jour terminée !',
+ 'User (global)': 'Utilisateur (global)',
+ 'Workspace (project-specific)': 'Espace de travail (spécifique au projet)',
+ 'Disable "{{name}}" - Select Scope':
+ 'Désactiver "{{name}}" - Sélectionner la portée',
+ 'Enable "{{name}}" - Select Scope':
+ 'Activer "{{name}}" - Sélectionner la portée',
+ 'No extension selected': 'Aucune extension sélectionnée',
+ 'Press Y/Enter to confirm, N/Esc to cancel':
+ 'Appuyez sur O/Entrée pour confirmer, N/Échap pour annuler',
+ 'Y/Enter to confirm, N/Esc to cancel':
+ 'O/Entrée pour confirmer, N/Échap pour annuler',
+ '{{count}} extensions installed': '{{count}} extensions installées',
+ "Use '/extensions install' to install your first extension.":
+ "Utilisez '/extensions install' pour installer votre première extension.",
+ 'up to date': 'à jour',
+ 'update available': 'mise à jour disponible',
+ 'checking...': 'vérification...',
+ 'not updatable': 'non mise à jour possible',
+ error: 'erreur',
+
+ // ============================================================================
+ // Commandes - Général (suite)
+ // ============================================================================
+ 'View and edit Qwen Code settings':
+ 'Voir et modifier les paramètres de Qwen Code',
+ Settings: 'Paramètres',
+ 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.':
+ 'Pour voir les changements, Qwen Code doit être redémarré. Appuyez sur r pour quitter et appliquer les changements maintenant.',
+ 'The command "/{{command}}" is not supported in non-interactive mode.':
+ 'La commande "/{{command}}" n\'est pas prise en charge en mode non interactif.',
+
+ // ============================================================================
+ // Étiquettes des paramètres
+ // ============================================================================
+ 'Vim Mode': 'Mode Vim',
+ 'Disable Auto Update': 'Désactiver la mise à jour automatique',
+ 'Attribution: commit': 'Attribution : commit',
+ 'Terminal Bell Notification': 'Notification sonore du terminal',
+ 'Enable Usage Statistics': "Activer les statistiques d'utilisation",
+ Theme: 'Thème',
+ 'Preferred Editor': 'Éditeur préféré',
+ 'Auto-connect to IDE': "Connexion automatique à l'IDE",
+ 'Enable Prompt Completion': "Activer la complétion d'invite",
+ 'Debug Keystroke Logging': 'Journalisation des frappes de débogage',
+ 'Language: UI': 'Langue : Interface',
+ 'Language: Model': 'Langue : Modèle',
+ 'Output Format': 'Format de sortie',
+ 'Hide Window Title': 'Masquer le titre de la fenêtre',
+ 'Show Status in Title': 'Afficher le statut dans le titre',
+ 'Hide Tips': 'Masquer les conseils',
+ 'Show Line Numbers in Code': 'Afficher les numéros de ligne dans le code',
+ 'Show Citations': 'Afficher les citations',
+ 'Custom Witty Phrases': 'Phrases personnalisées spirituelles',
+ 'Show Welcome Back Dialog': 'Afficher le dialogue de bienvenue',
+ 'Enable User Feedback': 'Activer les retours utilisateur',
+ 'How is Qwen doing this session? (optional)':
+ 'Comment se passe cette session avec Qwen ? (facultatif)',
+ Bad: 'Mauvais',
+ Fine: 'Correct',
+ Good: 'Bien',
+ Dismiss: 'Ignorer',
+ 'Not Sure Yet': 'Pas encore sûr',
+ 'Any other key': 'Toute autre touche',
+ 'Disable Loading Phrases': 'Désactiver les phrases de chargement',
+ 'Screen Reader Mode': "Mode lecteur d'écran",
+ 'IDE Mode': 'Mode IDE',
+ 'Max Session Turns': 'Nombre maximum de tours de session',
+ 'Skip Next Speaker Check':
+ 'Ignorer la vérification du prochain interlocuteur',
+ 'Skip Loop Detection': 'Ignorer la détection de boucle',
+ 'Skip Startup Context': 'Ignorer le contexte de démarrage',
+ 'Enable OpenAI Logging': 'Activer la journalisation OpenAI',
+ 'OpenAI Logging Directory': 'Répertoire de journalisation OpenAI',
+ Timeout: "Délai d'attente",
+ 'Max Retries': 'Nombre maximum de tentatives',
+ 'Disable Cache Control': 'Désactiver le contrôle du cache',
+ 'Memory Discovery Max Dirs': 'Répertoires max pour la découverte mémoire',
+ 'Load Memory From Include Directories':
+ 'Charger la mémoire depuis les répertoires inclus',
+ 'Respect .gitignore': 'Respecter .gitignore',
+ 'Respect .qwenignore': 'Respecter .qwenignore',
+ 'Enable Recursive File Search': 'Activer la recherche récursive de fichiers',
+ 'Disable Fuzzy Search': 'Désactiver la recherche approximative',
+ 'Interactive Shell (PTY)': 'Shell interactif (PTY)',
+ 'Show Color': 'Afficher les couleurs',
+ 'Auto Accept': 'Acceptation automatique',
+ 'Use Ripgrep': 'Utiliser Ripgrep',
+ 'Use Builtin Ripgrep': 'Utiliser Ripgrep intégré',
+ 'Enable Tool Output Truncation': 'Activer la troncature de sortie des outils',
+ 'Tool Output Truncation Threshold':
+ 'Seuil de troncature de sortie des outils',
+ 'Tool Output Truncation Lines': 'Lignes de troncature de sortie des outils',
+ 'Folder Trust': 'Confiance des dossiers',
+ 'Vision Model Preview': 'Aperçu du modèle de vision',
+ 'Tool Schema Compliance': 'Conformité au schéma des outils',
+ 'Auto (detect from system)': 'Auto (détecter depuis le système)',
+ 'Auto (detect terminal theme)': 'Auto (détecter le thème du terminal)',
+ Auto: 'Auto',
+ Text: 'Texte',
+ JSON: 'JSON',
+ Plan: 'Plan',
+ Default: 'Par défaut',
+ 'Auto Edit': 'Édition automatique',
+ YOLO: 'YOLO',
+ 'toggle vim mode on/off': 'activer/désactiver le mode Vim',
+ 'check session stats. Usage: /stats [model|tools]':
+ 'vérifier les stats de session. Utilisation : /stats [modèle|outils]',
+ 'Show model-specific usage statistics.':
+ "Afficher les statistiques d'utilisation spécifiques au modèle.",
+ 'Show tool-specific usage statistics.':
+ "Afficher les statistiques d'utilisation spécifiques aux outils.",
+ 'exit the cli': 'quitter le CLI',
+ 'Open MCP management dialog, or authenticate with OAuth-enabled servers':
+ 'Ouvrir le dialogue de gestion MCP, ou authentifier avec des serveurs compatibles OAuth',
+ 'List configured MCP servers and tools, or authenticate with OAuth-enabled servers':
+ 'Lister les serveurs MCP et outils configurés, ou authentifier avec des serveurs compatibles OAuth',
+ 'Manage workspace directories':
+ "Gérer les répertoires de l'espace de travail",
+ 'Add directories to the workspace. Use comma to separate multiple paths':
+ "Ajouter des répertoires à l'espace de travail. Utilisez une virgule pour séparer plusieurs chemins",
+ 'Show all directories in the workspace':
+ "Afficher tous les répertoires de l'espace de travail",
+ 'set external editor preference': "définir la préférence d'éditeur externe",
+ 'Select Editor': "Sélectionner l'éditeur",
+ 'Editor Preference': "Préférence d'éditeur",
+ 'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.':
+ 'Ces éditeurs sont actuellement pris en charge. Notez que certains éditeurs ne peuvent pas être utilisés en mode bac à sable.',
+ 'Your preferred editor is:': 'Votre éditeur préféré est :',
+ 'Manage extensions': 'Gérer les extensions',
+ 'Manage installed extensions': 'Gérer les extensions installées',
+ 'List active extensions': 'Lister les extensions actives',
+ 'Update extensions. Usage: update |--all':
+ 'Mettre à jour les extensions. Utilisation : update |--all',
+ 'Disable an extension': 'Désactiver une extension',
+ 'Enable an extension': 'Activer une extension',
+ 'Install an extension from a git repo or local path':
+ 'Installer une extension depuis un dépôt git ou un chemin local',
+ 'Uninstall an extension': 'Désinstaller une extension',
+ 'No extensions installed.': 'Aucune extension installée.',
+ 'Usage: /extensions update |--all':
+ 'Utilisation : /extensions update |--all',
+ 'Extension "{{name}}" not found.': 'Extension "{{name}}" introuvable.',
+ 'No extensions to update.': 'Aucune extension à mettre à jour.',
+ 'Usage: /extensions install ':
+ 'Utilisation : /extensions install ',
+ 'Installing extension from "{{source}}"...':
+ 'Installation de l\'extension depuis "{{source}}"...',
+ 'Extension "{{name}}" installed successfully.':
+ 'Extension "{{name}}" installée avec succès.',
+ 'Failed to install extension from "{{source}}": {{error}}':
+ 'Échec de l\'installation de l\'extension depuis "{{source}}" : {{error}}',
+ 'Usage: /extensions uninstall ':
+ 'Utilisation : /extensions uninstall ',
+ 'Uninstalling extension "{{name}}"...':
+ 'Désinstallation de l\'extension "{{name}}"...',
+ 'Extension "{{name}}" uninstalled successfully.':
+ 'Extension "{{name}}" désinstallée avec succès.',
+ 'Failed to uninstall extension "{{name}}": {{error}}':
+ 'Échec de la désinstallation de l\'extension "{{name}}" : {{error}}',
+ 'Usage: /extensions {{command}} [--scope=]':
+ 'Utilisation : /extensions {{command}} [--scope=]',
+ 'Unsupported scope "{{scope}}", should be one of "user" or "workspace"':
+ 'Portée non prise en charge "{{scope}}", doit être "user" ou "workspace"',
+ 'Extension "{{name}}" disabled for scope "{{scope}}"':
+ 'Extension "{{name}}" désactivée pour la portée "{{scope}}"',
+ 'Extension "{{name}}" enabled for scope "{{scope}}"':
+ 'Extension "{{name}}" activée pour la portée "{{scope}}"',
+ 'Do you want to continue? [Y/n]: ': 'Voulez-vous continuer ? [O/n] : ',
+ 'Do you want to continue?': 'Voulez-vous continuer ?',
+ 'Installing extension "{{name}}".':
+ 'Installation de l\'extension "{{name}}".',
+ '**Extensions may introduce unexpected behavior. Ensure you have investigated the extension source and trust the author.**':
+ "**Les extensions peuvent introduire des comportements inattendus. Assurez-vous d'avoir examiné la source de l'extension et de faire confiance à l'auteur.**",
+ 'This extension will run the following MCP servers:':
+ 'Cette extension exécutera les serveurs MCP suivants :',
+ local: 'local',
+ remote: 'distant',
+ 'This extension will add the following commands: {{commands}}.':
+ 'Cette extension ajoutera les commandes suivantes : {{commands}}.',
+ 'This extension will append info to your QWEN.md context using {{fileName}}':
+ 'Cette extension ajoutera des informations à votre contexte QWEN.md en utilisant {{fileName}}',
+ 'This extension will exclude the following core tools: {{tools}}':
+ 'Cette extension exclura les outils principaux suivants : {{tools}}',
+ 'This extension will install the following skills:':
+ 'Cette extension installera les compétences suivantes :',
+ 'This extension will install the following subagents:':
+ 'Cette extension installera les sous-agents suivants :',
+ 'Installation cancelled for "{{name}}".':
+ 'Installation annulée pour "{{name}}".',
+ 'You are installing an extension from {{originSource}}. Some features may not work perfectly with Qwen Code.':
+ 'Vous installez une extension depuis {{originSource}}. Certaines fonctionnalités peuvent ne pas fonctionner parfaitement avec Qwen Code.',
+ '--ref and --auto-update are not applicable for marketplace extensions.':
+ '--ref et --auto-update ne sont pas applicables aux extensions du marketplace.',
+ 'Extension "{{name}}" installed successfully and enabled.':
+ 'Extension "{{name}}" installée et activée avec succès.',
+ 'Installs an extension from a git repository URL, local path, or claude marketplace (marketplace-url:plugin-name).':
+ 'Installe une extension depuis une URL de dépôt git, un chemin local ou le marketplace claude (marketplace-url:nom-plugin).',
+ 'The github URL, local path, or marketplace source (marketplace-url:plugin-name) of the extension to install.':
+ "L'URL GitHub, le chemin local ou la source marketplace (marketplace-url:nom-plugin) de l'extension à installer.",
+ 'The git ref to install from.': 'La référence git depuis laquelle installer.',
+ 'Enable auto-update for this extension.':
+ 'Activer la mise à jour automatique pour cette extension.',
+ 'Enable pre-release versions for this extension.':
+ 'Activer les versions pré-release pour cette extension.',
+ 'Acknowledge the security risks of installing an extension and skip the confirmation prompt.':
+ "Reconnaître les risques de sécurité liés à l'installation d'une extension et ignorer la confirmation.",
+ 'The source argument must be provided.':
+ "L'argument source doit être fourni.",
+ 'Extension "{{name}}" successfully uninstalled.':
+ 'Extension "{{name}}" désinstallée avec succès.',
+ 'Uninstalls an extension.': 'Désinstalle une extension.',
+ 'The name or source path of the extension to uninstall.':
+ "Le nom ou le chemin source de l'extension à désinstaller.",
+ 'Please include the name of the extension to uninstall as a positional argument.':
+ "Veuillez inclure le nom de l'extension à désinstaller comme argument positionnel.",
+ 'Enables an extension.': 'Active une extension.',
+ 'The name of the extension to enable.': "Le nom de l'extension à activer.",
+ 'The scope to enable the extenison in. If not set, will be enabled in all scopes.':
+ "La portée dans laquelle activer l'extension. Si non définie, sera activée dans toutes les portées.",
+ 'Extension "{{name}}" successfully enabled for scope "{{scope}}".':
+ 'Extension "{{name}}" activée avec succès pour la portée "{{scope}}".',
+ 'Extension "{{name}}" successfully enabled in all scopes.':
+ 'Extension "{{name}}" activée avec succès dans toutes les portées.',
+ 'Invalid scope: {{scope}}. Please use one of {{scopes}}.':
+ "Portée invalide : {{scope}}. Veuillez utiliser l'une de : {{scopes}}.",
+ 'Disables an extension.': 'Désactive une extension.',
+ 'The name of the extension to disable.':
+ "Le nom de l'extension à désactiver.",
+ 'The scope to disable the extenison in.':
+ "La portée dans laquelle désactiver l'extension.",
+ 'Extension "{{name}}" successfully disabled for scope "{{scope}}".':
+ 'Extension "{{name}}" désactivée avec succès pour la portée "{{scope}}".',
+ 'Extension "{{name}}" successfully updated: {{oldVersion}} → {{newVersion}}.':
+ 'Extension "{{name}}" mise à jour avec succès : {{oldVersion}} → {{newVersion}}.',
+ 'Unable to install extension "{{name}}" due to missing install metadata':
+ "Impossible d'installer l'extension \"{{name}}\" en raison de métadonnées d'installation manquantes",
+ 'Extension "{{name}}" is already up to date.':
+ 'L\'extension "{{name}}" est déjà à jour.',
+ 'Updates all extensions or a named extension to the latest version.':
+ 'Met à jour toutes les extensions ou une extension nommée vers la dernière version.',
+ 'Update all extensions.': 'Mettre à jour toutes les extensions.',
+ 'Either an extension name or --all must be provided':
+ "Un nom d'extension ou --all doit être fourni",
+ 'Lists installed extensions.': 'Liste les extensions installées.',
+ 'Path:': 'Chemin :',
+ 'Source:': 'Source :',
+ 'Type:': 'Type :',
+ 'Ref:': 'Réf :',
+ 'Release tag:': 'Tag de version :',
+ 'Enabled (User):': 'Activé (Utilisateur) :',
+ 'Enabled (Workspace):': 'Activé (Espace de travail) :',
+ 'Context files:': 'Fichiers de contexte :',
+ 'Skills:': 'Compétences :',
+ 'Agents:': 'Agents :',
+ 'MCP servers:': 'Serveurs MCP :',
+ 'Link extension failed to install.':
+ "Échec de l'installation de l'extension liée.",
+ 'Extension "{{name}}" linked successfully and enabled.':
+ 'Extension "{{name}}" liée et activée avec succès.',
+ 'Links an extension from a local path. Updates made to the local path will always be reflected.':
+ 'Lie une extension depuis un chemin local. Les modifications apportées au chemin local seront toujours reflétées.',
+ 'The name of the extension to link.': "Le nom de l'extension à lier.",
+ 'Set a specific setting for an extension.':
+ 'Définir un paramètre spécifique pour une extension.',
+ 'Name of the extension to configure.': "Nom de l'extension à configurer.",
+ 'The setting to configure (name or env var).':
+ "Le paramètre à configurer (nom ou variable d'environnement).",
+ 'The scope to set the setting in.':
+ 'La portée dans laquelle définir le paramètre.',
+ 'List all settings for an extension.':
+ "Lister tous les paramètres d'une extension.",
+ 'Name of the extension.': "Nom de l'extension.",
+ 'Extension "{{name}}" has no settings to configure.':
+ 'L\'extension "{{name}}" n\'a aucun paramètre à configurer.',
+ 'Settings for "{{name}}":': 'Paramètres pour "{{name}}" :',
+ '(workspace)': '(espace de travail)',
+ '(user)': '(utilisateur)',
+ '[not set]': '[non défini]',
+ '[value stored in keychain]': '[valeur stockée dans le trousseau]',
+ 'Value:': 'Valeur :',
+ 'Manage extension settings.': 'Gérer les paramètres des extensions.',
+ 'You need to specify a command (set or list).':
+ 'Vous devez spécifier une commande (set ou list).',
+
+ // ============================================================================
+ // Choix de plugin / Marketplace
+ // ============================================================================
+ 'No plugins available in this marketplace.':
+ 'Aucun plugin disponible dans ce marketplace.',
+ 'Select a plugin to install from marketplace "{{name}}":':
+ 'Sélectionnez un plugin à installer depuis le marketplace "{{name}}" :',
+ 'Plugin selection cancelled.': 'Sélection de plugin annulée.',
+ 'Select a plugin from "{{name}}"': 'Sélectionner un plugin depuis "{{name}}"',
+ 'Use ↑↓ or j/k to navigate, Enter to select, Escape to cancel':
+ 'Utilisez ↑↓ ou j/k pour naviguer, Entrée pour sélectionner, Échap pour annuler',
+ '{{count}} more above': '{{count}} de plus au-dessus',
+ '{{count}} more below': '{{count}} de plus en dessous',
+ 'manage IDE integration': "gérer l'intégration IDE",
+ 'check status of IDE integration': "vérifier le statut de l'intégration IDE",
+ 'install required IDE companion for {{ideName}}':
+ 'installer le compagnon IDE requis pour {{ideName}}',
+ 'enable IDE integration': "activer l'intégration IDE",
+ 'disable IDE integration': "désactiver l'intégration IDE",
+ 'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.':
+ "L'intégration IDE n'est pas prise en charge dans votre environnement actuel. Pour utiliser cette fonctionnalité, exécutez Qwen Code dans l'un des IDEs pris en charge : VS Code ou ses dérivés.",
+ 'Set up GitHub Actions': 'Configurer GitHub Actions',
+ 'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf, Trae)':
+ 'Configurer les raccourcis du terminal pour la saisie multiligne (VS Code, Cursor, Windsurf, Trae)',
+ 'Please restart your terminal for the changes to take effect.':
+ 'Veuillez redémarrer votre terminal pour que les modifications prennent effet.',
+ 'Failed to configure terminal: {{error}}':
+ 'Échec de la configuration du terminal : {{error}}',
+ 'Could not determine {{terminalName}} config path on Windows: APPDATA environment variable is not set.':
+ "Impossible de déterminer le chemin de configuration de {{terminalName}} sur Windows : la variable d'environnement APPDATA n'est pas définie.",
+ '{{terminalName}} keybindings.json exists but is not a valid JSON array. Please fix the file manually or delete it to allow automatic configuration.':
+ "{{terminalName}} keybindings.json existe mais n'est pas un tableau JSON valide. Veuillez corriger le fichier manuellement ou le supprimer pour permettre la configuration automatique.",
+ 'File: {{file}}': 'Fichier : {{file}}',
+ 'Failed to parse {{terminalName}} keybindings.json. The file contains invalid JSON. Please fix the file manually or delete it to allow automatic configuration.':
+ "Échec de l'analyse de {{terminalName}} keybindings.json. Le fichier contient du JSON invalide. Veuillez corriger le fichier manuellement ou le supprimer pour permettre la configuration automatique.",
+ 'Error: {{error}}': 'Erreur : {{error}}',
+ 'Shift+Enter binding already exists': 'Le raccourci Maj+Entrée existe déjà',
+ 'Ctrl+Enter binding already exists': 'Le raccourci Ctrl+Entrée existe déjà',
+ 'Existing keybindings detected. Will not modify to avoid conflicts.':
+ 'Raccourcis existants détectés. Aucune modification pour éviter les conflits.',
+ 'Please check and modify manually if needed: {{file}}':
+ 'Veuillez vérifier et modifier manuellement si nécessaire : {{file}}',
+ 'Added Shift+Enter and Ctrl+Enter keybindings to {{terminalName}}.':
+ 'Raccourcis Maj+Entrée et Ctrl+Entrée ajoutés à {{terminalName}}.',
+ 'Modified: {{file}}': 'Modifié : {{file}}',
+ '{{terminalName}} keybindings already configured.':
+ 'Raccourcis {{terminalName}} déjà configurés.',
+ 'Failed to configure {{terminalName}}.':
+ 'Échec de la configuration de {{terminalName}}.',
+ 'Your terminal is already configured for an optimal experience with multiline input (Shift+Enter and Ctrl+Enter).':
+ 'Votre terminal est déjà configuré pour une expérience optimale avec la saisie multiligne (Maj+Entrée et Ctrl+Entrée).',
+
+ // ============================================================================
+ // Commandes - Hooks
+ // ============================================================================
+ 'Manage Qwen Code hooks': 'Gérer les hooks Qwen Code',
+ 'List all configured hooks': 'Lister tous les hooks configurés',
+ 'Enable a disabled hook': 'Activer un hook désactivé',
+ 'Disable an active hook': 'Désactiver un hook actif',
+ Hooks: 'Hooks',
+ 'Loading hooks...': 'Chargement des hooks...',
+ 'Error loading hooks:': 'Erreur lors du chargement des hooks :',
+ 'Press Escape to close': 'Appuyez sur Échap pour fermer',
+ 'Press Escape, Ctrl+C, or Ctrl+D to cancel':
+ 'Appuyez sur Échap, Ctrl+C ou Ctrl+D pour annuler',
+ 'Press Space, Enter, or Escape to dismiss':
+ 'Appuyez sur Espace, Entrée ou Échap pour ignorer',
+ 'No hook selected': 'Aucun hook sélectionné',
+ 'No hook events found.': 'Aucun événement de hook trouvé.',
+ '{{count}} hook configured': '{{count}} hook configuré',
+ '{{count}} hooks configured': '{{count}} hooks configurés',
+ 'This menu is read-only. To add or modify hooks, edit settings.json directly or ask Qwen Code.':
+ 'Ce menu est en lecture seule. Pour ajouter ou modifier des hooks, éditez settings.json directement ou demandez à Qwen Code.',
+ 'Enter to select · Esc to cancel':
+ 'Entrée pour sélectionner · Échap pour annuler',
+ 'Exit codes:': 'Codes de sortie :',
+ 'Configured hooks:': 'Hooks configurés :',
+ 'No hooks configured for this event.':
+ 'Aucun hook configuré pour cet événement.',
+ 'To add hooks, edit settings.json directly or ask Qwen.':
+ 'Pour ajouter des hooks, éditez settings.json directement ou demandez à Qwen.',
+ 'Enter to select · Esc to go back':
+ 'Entrée pour sélectionner · Échap pour revenir',
+ 'Hook details': 'Détails du hook',
+ 'Event:': 'Événement :',
+ 'Extension:': 'Extension :',
+ 'Desc:': 'Description :',
+ 'No hook config selected': 'Aucune configuration de hook sélectionnée',
+ 'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
+ 'Pour modifier ou supprimer ce hook, éditez settings.json directement ou demandez à Qwen.',
+ 'Hook Configuration - Disabled': 'Configuration du hook - Désactivé',
+ 'All hooks are currently disabled. You have {{count}} that are not running.':
+ "Tous les hooks sont actuellement désactivés. Vous en avez {{count}} qui ne s'exécutent pas.",
+ '{{count}} configured hook': '{{count}} hook configuré',
+ '{{count}} configured hooks': '{{count}} hooks configurés',
+ 'When hooks are disabled:': 'Quand les hooks sont désactivés :',
+ 'No hook commands will execute': "Aucune commande de hook ne s'exécutera",
+ 'StatusLine will not be displayed': 'La barre de statut ne sera pas affichée',
+ 'Tool operations will proceed without hook validation':
+ "Les opérations d'outils se poursuivront sans validation des hooks",
+ 'To re-enable hooks, remove "disableAllHooks" from settings.json or ask Qwen Code.':
+ 'Pour réactiver les hooks, supprimez "disableAllHooks" de settings.json ou demandez à Qwen Code.',
+ Project: 'Projet',
+ User: 'Utilisateur',
+ System: 'Système',
+ Extension: 'Extension',
+ 'Local Settings': 'Paramètres locaux',
+ 'User Settings': 'Paramètres utilisateur',
+ 'System Settings': 'Paramètres système',
+ Extensions: 'Extensions',
+ '✓ Enabled': '✓ Activé',
+ '✗ Disabled': '✗ Désactivé',
+ 'Before tool execution': "Avant l'exécution de l'outil",
+ 'After tool execution': "Après l'exécution de l'outil",
+ 'After tool execution fails': "Après l'échec de l'exécution de l'outil",
+ 'When notifications are sent': 'Quand des notifications sont envoyées',
+ 'When the user submits a prompt': "Quand l'utilisateur soumet une invite",
+ 'When a new session is started': 'Quand une nouvelle session est démarrée',
+ 'Right before Qwen Code concludes its response':
+ 'Juste avant que Qwen Code conclue sa réponse',
+ 'When a subagent (Agent tool call) is started':
+ "Quand un sous-agent (appel d'outil Agent) est démarré",
+ 'Right before a subagent concludes its response':
+ "Juste avant qu'un sous-agent conclue sa réponse",
+ 'Before conversation compaction': 'Avant la compaction de la conversation',
+ 'When a session is ending': 'Quand une session se termine',
+ 'When a permission dialog is displayed':
+ 'Quand un dialogue de permission est affiché',
+ 'Input to command is JSON of tool call arguments.':
+ "L'entrée de la commande est du JSON des arguments d'appel d'outil.",
+ 'Input to command is JSON with fields "inputs" (tool call arguments) and "response" (tool call response).':
+ "L'entrée de la commande est du JSON avec les champs \"inputs\" (arguments d'appel d'outil) et \"response\" (réponse de l'appel d'outil).",
+ 'Input to command is JSON with tool_name, tool_input, tool_use_id, error, error_type, is_interrupt, and is_timeout.':
+ "L'entrée de la commande est du JSON avec tool_name, tool_input, tool_use_id, error, error_type, is_interrupt et is_timeout.",
+ 'Input to command is JSON with notification message and type.':
+ "L'entrée de la commande est du JSON avec le message et le type de notification.",
+ 'Input to command is JSON with original user prompt text.':
+ "L'entrée de la commande est du JSON avec le texte d'invite original de l'utilisateur.",
+ 'Input to command is JSON with session start source.':
+ "L'entrée de la commande est du JSON avec la source de démarrage de session.",
+ 'Input to command is JSON with session end reason.':
+ "L'entrée de la commande est du JSON avec la raison de fin de session.",
+ 'Input to command is JSON with agent_id and agent_type.':
+ "L'entrée de la commande est du JSON avec agent_id et agent_type.",
+ 'Input to command is JSON with agent_id, agent_type, and agent_transcript_path.':
+ "L'entrée de la commande est du JSON avec agent_id, agent_type et agent_transcript_path.",
+ 'Input to command is JSON with compaction details.':
+ "L'entrée de la commande est du JSON avec les détails de compaction.",
+ 'Input to command is JSON with tool_name, tool_input, and tool_use_id. Output JSON with hookSpecificOutput containing decision to allow or deny.':
+ "L'entrée de la commande est du JSON avec tool_name, tool_input et tool_use_id. Sortie JSON avec hookSpecificOutput contenant la décision d'autoriser ou de refuser.",
+ 'stdout/stderr not shown': 'stdout/stderr non affiché',
+ 'show stderr to model and continue conversation':
+ 'afficher stderr au modèle et continuer la conversation',
+ 'show stderr to user only': "afficher stderr à l'utilisateur uniquement",
+ 'stdout shown in transcript mode (ctrl+o)':
+ 'stdout affiché en mode transcription (ctrl+o)',
+ 'show stderr to model immediately': 'afficher stderr au modèle immédiatement',
+ 'show stderr to user only but continue with tool call':
+ "afficher stderr à l'utilisateur uniquement mais continuer l'appel d'outil",
+ 'block processing, erase original prompt, and show stderr to user only':
+ "bloquer le traitement, effacer l'invite originale et afficher stderr à l'utilisateur uniquement",
+ 'stdout shown to Qwen': 'stdout affiché à Qwen',
+ 'show stderr to user only (blocking errors ignored)':
+ "afficher stderr à l'utilisateur uniquement (erreurs bloquantes ignorées)",
+ 'command completes successfully': 'la commande se termine avec succès',
+ 'stdout shown to subagent': 'stdout affiché au sous-agent',
+ 'show stderr to subagent and continue having it run':
+ 'afficher stderr au sous-agent et continuer son exécution',
+ 'stdout appended as custom compact instructions':
+ 'stdout ajouté comme instructions compactes personnalisées',
+ 'block compaction': 'bloquer la compaction',
+ 'show stderr to user only but continue with compaction':
+ "afficher stderr à l'utilisateur uniquement mais continuer la compaction",
+ 'use hook decision if provided': 'utiliser la décision du hook si fournie',
+ 'Config not loaded.': 'Configuration non chargée.',
+ 'Hooks are not enabled. Enable hooks in settings to use this feature.':
+ 'Les hooks ne sont pas activés. Activez les hooks dans les paramètres pour utiliser cette fonctionnalité.',
+ 'No hooks configured. Add hooks in your settings.json file.':
+ 'Aucun hook configuré. Ajoutez des hooks dans votre fichier settings.json.',
+ 'Configured Hooks ({{count}} total)': 'Hooks configurés ({{count}} au total)',
+
+ // ============================================================================
+ // Commandes - Export de session
+ // ============================================================================
+ 'Export current session message history to a file':
+ "Exporter l'historique des messages de la session actuelle vers un fichier",
+ 'Export session to HTML format': 'Exporter la session au format HTML',
+ 'Export session to JSON format': 'Exporter la session au format JSON',
+ 'Export session to JSONL format (one message per line)':
+ 'Exporter la session au format JSONL (un message par ligne)',
+ 'Export session to markdown format': 'Exporter la session au format markdown',
+
+ // ============================================================================
+ // Commandes - Insights
+ // ============================================================================
+ 'generate personalized programming insights from your chat history':
+ 'générer des insights de programmation personnalisés depuis votre historique de chat',
+
+ // ============================================================================
+ // Commandes - Historique de session
+ // ============================================================================
+ 'Resume a previous session': 'Reprendre une session précédente',
+ 'Restore a tool call. This will reset the conversation and file history to the state it was in when the tool call was suggested':
+ "Restaurer un appel d'outil. Cela réinitialisera la conversation et l'historique des fichiers à l'état où il se trouvait lors de la suggestion de l'appel d'outil",
+ 'Could not detect terminal type. Supported terminals: VS Code, Cursor, Windsurf, and Trae.':
+ 'Impossible de détecter le type de terminal. Terminaux pris en charge : VS Code, Cursor, Windsurf et Trae.',
+ 'Terminal "{{terminal}}" is not supported yet.':
+ 'Le terminal "{{terminal}}" n\'est pas encore pris en charge.',
+
+ // ============================================================================
+ // Commandes - Langue
+ // ============================================================================
+ 'Invalid language. Available: {{options}}':
+ 'Langue invalide. Disponibles : {{options}}',
+ 'Language subcommands do not accept additional arguments.':
+ "Les sous-commandes de langue n'acceptent pas d'arguments supplémentaires.",
+ 'Current UI language: {{lang}}': "Langue de l'interface actuelle : {{lang}}",
+ 'Current LLM output language: {{lang}}':
+ 'Langue de sortie LLM actuelle : {{lang}}',
+ 'LLM output language not set': 'Langue de sortie LLM non définie',
+ 'Set UI language': "Définir la langue de l'interface",
+ 'Set LLM output language': 'Définir la langue de sortie LLM',
+ 'Usage: /language ui [{{options}}]':
+ 'Utilisation : /language ui [{{options}}]',
+ 'Usage: /language output ':
+ 'Utilisation : /language output ',
+ 'Example: /language output 中文': 'Exemple : /language output 中文',
+ 'Example: /language output English': 'Exemple : /language output English',
+ 'Example: /language output 日本語': 'Exemple : /language output 日本語',
+ 'Example: /language output Português': 'Exemple : /language output Português',
+ 'UI language changed to {{lang}}':
+ "Langue de l'interface changée en {{lang}}",
+ 'LLM output language set to {{lang}}':
+ 'Langue de sortie LLM définie sur {{lang}}',
+ 'LLM output language rule file generated at {{path}}':
+ 'Fichier de règle de langue de sortie LLM généré dans {{path}}',
+ 'Please restart the application for the changes to take effect.':
+ "Veuillez redémarrer l'application pour que les modifications prennent effet.",
+ 'Failed to generate LLM output language rule file: {{error}}':
+ 'Échec de la génération du fichier de règle de langue de sortie LLM : {{error}}',
+ 'Invalid command. Available subcommands:':
+ 'Commande invalide. Sous-commandes disponibles :',
+ 'Available subcommands:': 'Sous-commandes disponibles :',
+ 'To request additional UI language packs, please open an issue on GitHub.':
+ "Pour demander des packs de langue d'interface supplémentaires, veuillez ouvrir un ticket sur GitHub.",
+ 'Available options:': 'Options disponibles :',
+ 'Set UI language to {{name}}':
+ "Définir la langue de l'interface sur {{name}}",
+
+ // ============================================================================
+ // Commandes - Mode d'approbation
+ // ============================================================================
+ 'Tool Approval Mode': "Mode d'approbation des outils",
+ 'Current approval mode: {{mode}}': "Mode d'approbation actuel : {{mode}}",
+ 'Available approval modes:': "Modes d'approbation disponibles :",
+ 'Approval mode changed to: {{mode}}':
+ "Mode d'approbation changé en : {{mode}}",
+ 'Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})':
+ "Mode d'approbation changé en : {{mode}} (enregistré dans les paramètres {{scope}}{{location}})",
+ 'Usage: /approval-mode [--session|--user|--project]':
+ 'Utilisation : /approval-mode [--session|--user|--project]',
+ 'Scope subcommands do not accept additional arguments.':
+ "Les sous-commandes de portée n'acceptent pas d'arguments supplémentaires.",
+ 'Plan mode - Analyze only, do not modify files or execute commands':
+ 'Mode plan - Analyser uniquement, ne pas modifier les fichiers ni exécuter des commandes',
+ 'Default mode - Require approval for file edits or shell commands':
+ "Mode par défaut - Demander l'approbation pour les modifications de fichiers ou les commandes shell",
+ 'Auto-edit mode - Automatically approve file edits':
+ 'Mode édition automatique - Approuver automatiquement les modifications de fichiers',
+ 'YOLO mode - Automatically approve all tools':
+ 'Mode YOLO - Approuver automatiquement tous les outils',
+ '{{mode}} mode': 'Mode {{mode}}',
+ 'Settings service is not available; unable to persist the approval mode.':
+ "Le service de paramètres n'est pas disponible ; impossible de persister le mode d'approbation.",
+ 'Failed to save approval mode: {{error}}':
+ "Échec de la sauvegarde du mode d'approbation : {{error}}",
+ 'Failed to change approval mode: {{error}}':
+ "Échec du changement du mode d'approbation : {{error}}",
+ 'Apply to current session only (temporary)':
+ 'Appliquer uniquement à la session actuelle (temporaire)',
+ 'Persist for this project/workspace':
+ 'Persister pour ce projet/espace de travail',
+ 'Persist for this user on this machine':
+ 'Persister pour cet utilisateur sur cette machine',
+ 'Analyze only, do not modify files or execute commands':
+ 'Analyser uniquement, ne pas modifier les fichiers ni exécuter des commandes',
+ 'Require approval for file edits or shell commands':
+ "Demander l'approbation pour les modifications de fichiers ou les commandes shell",
+ 'Automatically approve file edits':
+ 'Approuver automatiquement les modifications de fichiers',
+ 'Automatically approve all tools':
+ 'Approuver automatiquement tous les outils',
+ 'Workspace approval mode exists and takes priority. User-level change will have no effect.':
+ "Un mode d'approbation d'espace de travail existe et a la priorité. La modification au niveau utilisateur n'aura aucun effet.",
+ 'Apply To': 'Appliquer à',
+ 'Workspace Settings': "Paramètres de l'espace de travail",
+
+ // ============================================================================
+ // Commandes - Mémoire
+ // ============================================================================
+ 'Commands for interacting with memory.':
+ 'Commandes pour interagir avec la mémoire.',
+ 'Show the current memory contents.':
+ 'Afficher le contenu actuel de la mémoire.',
+ 'Show project-level memory contents.':
+ 'Afficher le contenu de la mémoire au niveau du projet.',
+ 'Show global memory contents.': 'Afficher le contenu de la mémoire globale.',
+ 'Add content to project-level memory.':
+ 'Ajouter du contenu à la mémoire au niveau du projet.',
+ 'Add content to global memory.': 'Ajouter du contenu à la mémoire globale.',
+ 'Refresh the memory from the source.':
+ 'Actualiser la mémoire depuis la source.',
+ 'Usage: /memory add --project ':
+ 'Utilisation : /memory add --project ',
+ 'Usage: /memory add --global ':
+ 'Utilisation : /memory add --global ',
+ 'Attempting to save to project memory: "{{text}}"':
+ 'Tentative de sauvegarde dans la mémoire du projet : "{{text}}"',
+ 'Attempting to save to global memory: "{{text}}"':
+ 'Tentative de sauvegarde dans la mémoire globale : "{{text}}"',
+ 'Current memory content from {{count}} file(s):':
+ 'Contenu actuel de la mémoire depuis {{count}} fichier(s) :',
+ 'Memory is currently empty.': 'La mémoire est actuellement vide.',
+ 'Project memory file not found or is currently empty.':
+ 'Fichier de mémoire du projet introuvable ou actuellement vide.',
+ 'Global memory file not found or is currently empty.':
+ 'Fichier de mémoire globale introuvable ou actuellement vide.',
+ 'Global memory is currently empty.':
+ 'La mémoire globale est actuellement vide.',
+ 'Global memory content:\n\n---\n{{content}}\n---':
+ 'Contenu de la mémoire globale :\n\n---\n{{content}}\n---',
+ 'Project memory content from {{path}}:\n\n---\n{{content}}\n---':
+ 'Contenu de la mémoire du projet depuis {{path}} :\n\n---\n{{content}}\n---',
+ 'Project memory is currently empty.':
+ 'La mémoire du projet est actuellement vide.',
+ 'Refreshing memory from source files...':
+ 'Actualisation de la mémoire depuis les fichiers sources...',
+ 'Add content to the memory. Use --global for global memory or --project for project memory.':
+ 'Ajouter du contenu à la mémoire. Utilisez --global pour la mémoire globale ou --project pour la mémoire du projet.',
+ 'Usage: /memory add [--global|--project] ':
+ 'Utilisation : /memory add [--global|--project] ',
+ 'Attempting to save to memory {{scope}}: "{{fact}}"':
+ 'Tentative de sauvegarde dans la mémoire {{scope}} : "{{fact}}"',
+
+ // ============================================================================
+ // Commandes - MCP
+ // ============================================================================
+ 'Authenticate with an OAuth-enabled MCP server':
+ 'Authentifier avec un serveur MCP compatible OAuth',
+ 'List configured MCP servers and tools':
+ 'Lister les serveurs MCP et outils configurés',
+ 'Restarts MCP servers.': 'Redémarre les serveurs MCP.',
+ 'Open MCP management dialog': 'Ouvrir le dialogue de gestion MCP',
+ 'Could not retrieve tool registry.':
+ 'Impossible de récupérer le registre des outils.',
+ 'No MCP servers configured with OAuth authentication.':
+ "Aucun serveur MCP configuré avec l'authentification OAuth.",
+ 'MCP servers with OAuth authentication:':
+ 'Serveurs MCP avec authentification OAuth :',
+ 'Use /mcp auth to authenticate.':
+ 'Utilisez /mcp auth pour vous authentifier.',
+ "MCP server '{{name}}' not found.": "Serveur MCP '{{name}}' introuvable.",
+ "Successfully authenticated and refreshed tools for '{{name}}'.":
+ "Authentification réussie et outils actualisés pour '{{name}}'.",
+ "Failed to authenticate with MCP server '{{name}}': {{error}}":
+ "Échec de l'authentification avec le serveur MCP '{{name}}' : {{error}}",
+ "Re-discovering tools from '{{name}}'...":
+ "Redécouverte des outils depuis '{{name}}'...",
+ "Discovered {{count}} tool(s) from '{{name}}'.":
+ "{{count}} outil(s) découvert(s) depuis '{{name}}'.",
+ 'Authentication complete. Returning to server details...':
+ 'Authentification terminée. Retour aux détails du serveur...',
+ 'Authentication successful.': 'Authentification réussie.',
+ 'If the browser does not open, copy and paste this URL into your browser:':
+ "Si le navigateur ne s'ouvre pas, copiez et collez cette URL dans votre navigateur :",
+ 'Make sure to copy the COMPLETE URL - it may wrap across multiple lines.':
+ "Assurez-vous de copier l'URL COMPLÈTE - elle peut s'étendre sur plusieurs lignes.",
+
+ // ============================================================================
+ // Boîte de dialogue de gestion MCP
+ // ============================================================================
+ 'Manage MCP servers': 'Gérer les serveurs MCP',
+ 'Server Detail': 'Détail du serveur',
+ 'Disable Server': 'Désactiver le serveur',
+ Tools: 'Outils',
+ 'Tool Detail': "Détail de l'outil",
+ 'MCP Management': 'Gestion MCP',
+ 'Loading...': 'Chargement...',
+ 'Unknown step': 'Étape inconnue',
+ 'Esc to back': 'Échap pour revenir',
+ '↑↓ to navigate · Enter to select · Esc to close':
+ '↑↓ pour naviguer · Entrée pour sélectionner · Échap pour fermer',
+ '↑↓ to navigate · Enter to select · Esc to back':
+ '↑↓ pour naviguer · Entrée pour sélectionner · Échap pour revenir',
+ '↑↓ to navigate · Enter to confirm · Esc to back':
+ '↑↓ pour naviguer · Entrée pour confirmer · Échap pour revenir',
+ 'User Settings (global)': 'Paramètres utilisateur (global)',
+ 'Workspace Settings (project-specific)':
+ 'Paramètres espace de travail (spécifique au projet)',
+ 'Disable server:': 'Désactiver le serveur :',
+ 'Select where to add the server to the exclude list:':
+ "Sélectionnez où ajouter le serveur à la liste d'exclusion :",
+ 'Press Enter to confirm, Esc to cancel':
+ 'Appuyez sur Entrée pour confirmer, Échap pour annuler',
+ 'View tools': 'Voir les outils',
+ Reconnect: 'Reconnecter',
+ Enable: 'Activer',
+ Disable: 'Désactiver',
+ Authenticate: 'Authentifier',
+ 'Re-authenticate': 'Réauthentifier',
+ 'Clear Authentication': "Effacer l'authentification",
+ 'Server:': 'Serveur :',
+ 'Command:': 'Commande :',
+ 'Working Directory:': 'Répertoire de travail :',
+ 'Capabilities:': 'Capacités :',
+ 'No server selected': 'Aucun serveur sélectionné',
+ prompts: 'invites',
+ '(disabled)': '(désactivé)',
+ 'Error:': 'Erreur :',
+ tool: 'outil',
+ tools: 'outils',
+ connected: 'connecté',
+ connecting: 'connexion en cours',
+ disconnected: 'déconnecté',
+ 'User MCPs': 'MCPs utilisateur',
+ 'Project MCPs': 'MCPs projet',
+ 'Extension MCPs': "MCPs d'extension",
+ server: 'serveur',
+ servers: 'serveurs',
+ 'Add MCP servers to your settings to get started.':
+ 'Ajoutez des serveurs MCP à vos paramètres pour commencer.',
+ 'Run qwen --debug to see error logs':
+ "Exécutez qwen --debug pour voir les journaux d'erreurs",
+ 'OAuth Authentication': 'Authentification OAuth',
+ 'Press Enter to start authentication, Esc to go back':
+ "Appuyez sur Entrée pour démarrer l'authentification, Échap pour revenir",
+ 'Authenticating... Please complete the login in your browser.':
+ 'Authentification... Veuillez compléter la connexion dans votre navigateur.',
+ 'Press Enter or Esc to go back': 'Appuyez sur Entrée ou Échap pour revenir',
+ 'No tools available for this server.':
+ 'Aucun outil disponible pour ce serveur.',
+ destructive: 'destructif',
+ 'read-only': 'lecture seule',
+ 'open-world': 'monde ouvert',
+ idempotent: 'idempotent',
+ 'Tools for {{name}}': 'Outils pour {{name}}',
+ 'Tools for {{serverName}}': 'Outils pour {{serverName}}',
+ '{{current}}/{{total}}': '{{current}}/{{total}}',
+ required: 'requis',
+ Type: 'Type',
+ Enum: 'Enum',
+ Parameters: 'Paramètres',
+ 'No tool selected': 'Aucun outil sélectionné',
+ Annotations: 'Annotations',
+ Title: 'Titre',
+ 'Read Only': 'Lecture seule',
+ Destructive: 'Destructif',
+ Idempotent: 'Idempotent',
+ 'Open World': 'Monde ouvert',
+ Server: 'Serveur',
+ '{{count}} invalid tools': '{{count}} outils invalides',
+ invalid: 'invalide',
+ 'invalid: {{reason}}': 'invalide : {{reason}}',
+ 'missing name': 'nom manquant',
+ 'missing description': 'description manquante',
+ '(unnamed)': '(sans nom)',
+ 'Warning: This tool cannot be called by the LLM':
+ 'Avertissement : Cet outil ne peut pas être appelé par le LLM',
+ Reason: 'Raison',
+ 'Tools must have both name and description to be used by the LLM.':
+ 'Les outils doivent avoir un nom et une description pour être utilisés par le LLM.',
+
+ // ============================================================================
+ // Commandes - Chat
+ // ============================================================================
+ 'Manage conversation history.': "Gérer l'historique des conversations.",
+ 'List saved conversation checkpoints':
+ 'Lister les points de contrôle de conversation sauvegardés',
+ 'No saved conversation checkpoints found.':
+ 'Aucun point de contrôle de conversation sauvegardé trouvé.',
+ 'List of saved conversations:': 'Liste des conversations sauvegardées :',
+ 'Note: Newest last, oldest first':
+ 'Note : Du plus récent au plus ancien en dernier, du plus ancien en premier',
+ 'Save the current conversation as a checkpoint. Usage: /chat save ':
+ 'Sauvegarder la conversation actuelle comme point de contrôle. Utilisation : /chat save <étiquette>',
+ 'Missing tag. Usage: /chat save ':
+ 'Étiquette manquante. Utilisation : /chat save <étiquette>',
+ 'Delete a conversation checkpoint. Usage: /chat delete ':
+ 'Supprimer un point de contrôle de conversation. Utilisation : /chat delete <étiquette>',
+ 'Missing tag. Usage: /chat delete ':
+ 'Étiquette manquante. Utilisation : /chat delete <étiquette>',
+ "Conversation checkpoint '{{tag}}' has been deleted.":
+ "Le point de contrôle de conversation '{{tag}}' a été supprimé.",
+ "Error: No checkpoint found with tag '{{tag}}'.":
+ "Erreur : Aucun point de contrôle trouvé avec l'étiquette '{{tag}}'.",
+ 'Resume a conversation from a checkpoint. Usage: /chat resume ':
+ 'Reprendre une conversation depuis un point de contrôle. Utilisation : /chat resume <étiquette>',
+ 'Missing tag. Usage: /chat resume ':
+ 'Étiquette manquante. Utilisation : /chat resume <étiquette>',
+ 'No saved checkpoint found with tag: {{tag}}.':
+ "Aucun point de contrôle sauvegardé trouvé avec l'étiquette : {{tag}}.",
+ 'A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?':
+ "Un point de contrôle avec l'étiquette {{tag}} existe déjà. Voulez-vous l'écraser ?",
+ 'No chat client available to save conversation.':
+ 'Aucun client de chat disponible pour sauvegarder la conversation.',
+ 'Conversation checkpoint saved with tag: {{tag}}.':
+ "Point de contrôle de conversation sauvegardé avec l'étiquette : {{tag}}.",
+ 'No conversation found to save.':
+ 'Aucune conversation trouvée à sauvegarder.',
+ 'No chat client available to share conversation.':
+ 'Aucun client de chat disponible pour partager la conversation.',
+ 'Invalid file format. Only .md and .json are supported.':
+ 'Format de fichier invalide. Seuls .md et .json sont pris en charge.',
+ 'Error sharing conversation: {{error}}':
+ 'Erreur lors du partage de la conversation : {{error}}',
+ 'Conversation shared to {{filePath}}':
+ 'Conversation partagée vers {{filePath}}',
+ 'No conversation found to share.': 'Aucune conversation trouvée à partager.',
+ 'Share the current conversation to a markdown or json file. Usage: /chat share ':
+ 'Partager la conversation actuelle vers un fichier markdown ou json. Utilisation : /chat share ',
+
+ // ============================================================================
+ // Commandes - Résumé
+ // ============================================================================
+ 'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md':
+ "Générer un résumé du projet et l'enregistrer dans .qwen/PROJECT_SUMMARY.md",
+ 'No chat client available to generate summary.':
+ 'Aucun client de chat disponible pour générer le résumé.',
+ 'Already generating summary, wait for previous request to complete':
+ 'Génération de résumé déjà en cours, attendez que la demande précédente se termine',
+ 'No conversation found to summarize.':
+ 'Aucune conversation trouvée à résumer.',
+ 'Failed to generate project context summary: {{error}}':
+ 'Échec de la génération du résumé du contexte du projet : {{error}}',
+ 'Saved project summary to {{filePathForDisplay}}.':
+ 'Résumé du projet enregistré dans {{filePathForDisplay}}.',
+ 'Saving project summary...': 'Enregistrement du résumé du projet...',
+ 'Generating project summary...': 'Génération du résumé du projet...',
+ 'Failed to generate summary - no text content received from LLM response':
+ 'Échec de la génération du résumé - aucun contenu texte reçu de la réponse LLM',
+
+ // ============================================================================
+ // Commandes - Modèle
+ // ============================================================================
+ 'Switch the model for this session (--fast for suggestion model)':
+ 'Changer le modèle pour cette session (--fast pour le modèle de suggestion)',
+ 'Set a lighter model for prompt suggestions and speculative execution':
+ "Définir un modèle plus léger pour les suggestions d'invite et l'exécution spéculative",
+ 'Content generator configuration not available.':
+ 'Configuration du générateur de contenu non disponible.',
+ 'Authentication type not available.':
+ "Type d'authentification non disponible.",
+ 'No models available for the current authentication type ({{authType}}).':
+ "Aucun modèle disponible pour le type d'authentification actuel ({{authType}}).",
+
+ // ============================================================================
+ // Commandes - Effacer
+ // ============================================================================
+ 'Starting a new session, resetting chat, and clearing terminal.':
+ "Démarrage d'une nouvelle session, réinitialisation du chat et effacement du terminal.",
+ 'Starting a new session and clearing.':
+ "Démarrage d'une nouvelle session et effacement.",
+
+ // ============================================================================
+ // Commandes - Compresser
+ // ============================================================================
+ 'Already compressing, wait for previous request to complete':
+ 'Compression déjà en cours, attendez que la demande précédente se termine',
+ 'Failed to compress chat history.':
+ "Échec de la compression de l'historique du chat.",
+ 'Failed to compress chat history: {{error}}':
+ "Échec de la compression de l'historique du chat : {{error}}",
+ 'Compressing chat history': "Compression de l'historique du chat",
+ 'Chat history compressed from {{originalTokens}} to {{newTokens}} tokens.':
+ "L'historique du chat a été compressé de {{originalTokens}} à {{newTokens}} tokens.",
+ 'Compression was not beneficial for this history size.':
+ "La compression n'était pas bénéfique pour cette taille d'historique.",
+ 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.':
+ "La compression de l'historique du chat n'a pas réduit la taille. Cela peut indiquer des problèmes avec l'invite de compression.",
+ 'Could not compress chat history due to a token counting error.':
+ "Impossible de compresser l'historique du chat en raison d'une erreur de comptage de tokens.",
+ 'Chat history is already compressed.':
+ "L'historique du chat est déjà compressé.",
+
+ // ============================================================================
+ // Commandes - Répertoire
+ // ============================================================================
+ 'Configuration is not available.': 'Configuration non disponible.',
+ 'Please provide at least one path to add.':
+ 'Veuillez fournir au moins un chemin à ajouter.',
+ 'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.':
+ "La commande /directory add n'est pas prise en charge dans les profils de bac à sable restrictifs. Utilisez plutôt --include-directories lors du démarrage de la session.",
+ "Error adding '{{path}}': {{error}}":
+ "Erreur lors de l'ajout de '{{path}}' : {{error}}",
+ 'Successfully added QWEN.md files from the following directories if there are:\n- {{directories}}':
+ "Fichiers QWEN.md ajoutés avec succès depuis les répertoires suivants s'ils existent :\n- {{directories}}",
+ 'Error refreshing memory: {{error}}':
+ "Erreur lors de l'actualisation de la mémoire : {{error}}",
+ 'Successfully added directories:\n- {{directories}}':
+ 'Répertoires ajoutés avec succès :\n- {{directories}}',
+ 'Current workspace directories:\n{{directories}}':
+ "Répertoires actuels de l'espace de travail :\n{{directories}}",
+
+ // ============================================================================
+ // Commandes - Documentation
+ // ============================================================================
+ 'Please open the following URL in your browser to view the documentation:\n{{url}}':
+ "Veuillez ouvrir l'URL suivante dans votre navigateur pour voir la documentation :\n{{url}}",
+ 'Opening documentation in your browser: {{url}}':
+ 'Ouverture de la documentation dans votre navigateur : {{url}}',
+
+ // ============================================================================
+ // Boîtes de dialogue - Confirmation d'outil
+ // ============================================================================
+ 'Do you want to proceed?': 'Voulez-vous continuer ?',
+ 'Yes, allow once': 'Oui, autoriser une fois',
+ 'Allow always': 'Toujours autoriser',
+ Yes: 'Oui',
+ No: 'Non',
+ 'No (esc)': 'Non (échap)',
+ 'Yes, allow always for this session':
+ 'Oui, toujours autoriser pour cette session',
+ 'Modify in progress:': 'Modification en cours :',
+ 'Save and close external editor to continue':
+ "Enregistrez et fermez l'éditeur externe pour continuer",
+ 'Apply this change?': 'Appliquer cette modification ?',
+ 'Yes, allow always': 'Oui, toujours autoriser',
+ 'Modify with external editor': "Modifier avec l'éditeur externe",
+ 'No, suggest changes (esc)': 'Non, suggérer des modifications (échap)',
+ "Allow execution of: '{{command}}'?":
+ "Autoriser l'exécution de : '{{command}}' ?",
+ 'Yes, allow always ...': 'Oui, toujours autoriser ...',
+ 'Always allow in this project': 'Toujours autoriser dans ce projet',
+ 'Always allow {{action}} in this project':
+ 'Toujours autoriser {{action}} dans ce projet',
+ 'Always allow for this user': 'Toujours autoriser pour cet utilisateur',
+ 'Always allow {{action}} for this user':
+ 'Toujours autoriser {{action}} pour cet utilisateur',
+ 'Yes, restore previous mode ({{mode}})':
+ 'Oui, restaurer le mode précédent ({{mode}})',
+ 'Yes, and auto-accept edits':
+ 'Oui, et accepter automatiquement les modifications',
+ 'Yes, and manually approve edits':
+ 'Oui, et approuver manuellement les modifications',
+ 'No, keep planning (esc)': 'Non, continuer la planification (échap)',
+ 'URLs to fetch:': 'URLs à récupérer :',
+ 'MCP Server: {{server}}': 'Serveur MCP : {{server}}',
+ 'Tool: {{tool}}': 'Outil : {{tool}}',
+ 'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?':
+ 'Autoriser l\'exécution de l\'outil MCP "{{tool}}" depuis le serveur "{{server}}" ?',
+ 'Yes, always allow tool "{{tool}}" from server "{{server}}"':
+ 'Oui, toujours autoriser l\'outil "{{tool}}" depuis le serveur "{{server}}"',
+ 'Yes, always allow all tools from server "{{server}}"':
+ 'Oui, toujours autoriser tous les outils depuis le serveur "{{server}}"',
+
+ // ============================================================================
+ // Boîtes de dialogue - Confirmation shell
+ // ============================================================================
+ 'Shell Command Execution': 'Exécution de commande shell',
+ 'A custom command wants to run the following shell commands:':
+ 'Une commande personnalisée veut exécuter les commandes shell suivantes :',
+
+ // ============================================================================
+ // Boîtes de dialogue - Quota Pro
+ // ============================================================================
+ 'Pro quota limit reached for {{model}}.':
+ 'Limite de quota Pro atteinte pour {{model}}.',
+ 'Change auth (executes the /auth command)':
+ "Changer l'authentification (exécute la commande /auth)",
+ 'Continue with {{model}}': 'Continuer avec {{model}}',
+
+ // ============================================================================
+ // Boîtes de dialogue - Bienvenue
+ // ============================================================================
+ 'Current Plan:': 'Plan actuel :',
+ 'Progress: {{done}}/{{total}} tasks completed':
+ 'Progression : {{done}}/{{total}} tâches terminées',
+ ', {{inProgress}} in progress': ', {{inProgress}} en cours',
+ 'Pending Tasks:': 'Tâches en attente :',
+ 'What would you like to do?': 'Que souhaitez-vous faire ?',
+ 'Choose how to proceed with your session:':
+ 'Choisissez comment poursuivre votre session :',
+ 'Start new chat session': 'Démarrer une nouvelle session de chat',
+ 'Continue previous conversation': 'Continuer la conversation précédente',
+ '👋 Welcome back! (Last updated: {{timeAgo}})':
+ '👋 Bon retour ! (Dernière mise à jour : {{timeAgo}})',
+ '🎯 Overall Goal:': '🎯 Objectif global :',
+
+ // ============================================================================
+ // Boîtes de dialogue - Authentification
+ // ============================================================================
+ 'Get started': 'Commencer',
+ 'Select Authentication Method': "Sélectionner la méthode d'authentification",
+ 'OpenAI API key is required to use OpenAI authentication.':
+ "Une clé API OpenAI est requise pour utiliser l'authentification OpenAI.",
+ 'You must select an auth method to proceed. Press Ctrl+C again to exit.':
+ "Vous devez sélectionner une méthode d'authentification pour continuer. Appuyez à nouveau sur Ctrl+C pour quitter.",
+ 'Terms of Services and Privacy Notice':
+ "Conditions d'utilisation et avis de confidentialité",
+ 'Qwen OAuth': 'Qwen OAuth',
+ 'Discontinued — switch to Coding Plan or API Key':
+ 'Abandonné — passez à Coding Plan ou API Key',
+ 'Qwen OAuth free tier was discontinued on 2026-04-15. Run /auth to switch provider.':
+ 'Le niveau gratuit Qwen OAuth a été abandonné le 2026-04-15. Exécutez /auth pour changer de fournisseur.',
+ 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select Coding Plan or API Key instead.':
+ 'Le niveau gratuit Qwen OAuth a été abandonné le 2026-04-15. Veuillez sélectionner Coding Plan ou API Key.',
+ 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select a model from another provider or run /auth to switch.':
+ "Le niveau gratuit de Qwen OAuth a été abandonné le 2026-04-15. Veuillez sélectionner un modèle d'un autre fournisseur ou exécuter /auth pour changer.",
+ '\n⚠ Qwen OAuth free tier was discontinued on 2026-04-15. Please select another option.\n':
+ '\n⚠ Le niveau gratuit Qwen OAuth a été abandonné le 2026-04-15. Veuillez sélectionner une autre option.\n',
+ 'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models':
+ "Payant · Jusqu'à 6 000 requêtes/5h · Tous les modèles Alibaba Cloud Coding Plan",
+ 'Alibaba Cloud Coding Plan': 'Plan de codage Alibaba Cloud',
+ 'Bring your own API key': 'Apportez votre propre clé API',
+ 'API-KEY': 'CLÉ-API',
+ 'Use coding plan credentials or your own api-keys/providers.':
+ 'Utilisez les identifiants du plan de codage ou vos propres clés API/fournisseurs.',
+ OpenAI: 'OpenAI',
+ 'Failed to login. Message: {{message}}':
+ 'Échec de la connexion. Message : {{message}}',
+ 'Authentication is enforced to be {{enforcedType}}, but you are currently using {{currentType}}.':
+ "L'authentification est imposée à {{enforcedType}}, mais vous utilisez actuellement {{currentType}}.",
+ 'Qwen OAuth authentication timed out. Please try again.':
+ "L'authentification Qwen OAuth a expiré. Veuillez réessayer.",
+ 'Qwen OAuth authentication cancelled.':
+ 'Authentification Qwen OAuth annulée.',
+ 'Qwen OAuth Authentication': 'Authentification Qwen OAuth',
+ 'Please visit this URL to authorize:':
+ 'Veuillez visiter cette URL pour autoriser :',
+ 'Or scan the QR code below:': 'Ou scannez le QR code ci-dessous :',
+ 'Waiting for authorization': "En attente d'autorisation",
+ 'Time remaining:': 'Temps restant :',
+ '(Press ESC or CTRL+C to cancel)':
+ '(Appuyez sur ÉCHAP ou CTRL+C pour annuler)',
+ 'Qwen OAuth Authentication Timeout': "Délai d'authentification Qwen OAuth",
+ 'OAuth token expired (over {{seconds}} seconds). Please select authentication method again.':
+ "Token OAuth expiré (plus de {{seconds}} secondes). Veuillez sélectionner à nouveau la méthode d'authentification.",
+ 'Press any key to return to authentication type selection.':
+ "Appuyez sur n'importe quelle touche pour revenir à la sélection du type d'authentification.",
+ 'Waiting for Qwen OAuth authentication...':
+ "En attente de l'authentification Qwen OAuth...",
+ 'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.':
+ "Remarque : Votre clé API existante dans settings.json ne sera pas effacée lors de l'utilisation de Qwen OAuth. Vous pouvez revenir à l'authentification OpenAI plus tard si nécessaire.",
+ 'Note: Your existing API key will not be cleared when using Qwen OAuth.':
+ "Remarque : Votre clé API existante ne sera pas effacée lors de l'utilisation de Qwen OAuth.",
+ 'Authentication timed out. Please try again.':
+ "L'authentification a expiré. Veuillez réessayer.",
+ 'Waiting for auth... (Press ESC or CTRL+C to cancel)':
+ "En attente d'authentification... (Appuyez sur ÉCHAP ou CTRL+C pour annuler)",
+ 'Missing API key for OpenAI-compatible auth. Set settings.security.auth.apiKey, or set the {{envKeyHint}} environment variable.':
+ "Clé API manquante pour l'authentification compatible OpenAI. Définissez settings.security.auth.apiKey ou la variable d'environnement {{envKeyHint}}.",
+ '{{envKeyHint}} environment variable not found.':
+ "Variable d'environnement {{envKeyHint}} introuvable.",
+ '{{envKeyHint}} environment variable not found. Please set it in your .env file or environment variables.':
+ "Variable d'environnement {{envKeyHint}} introuvable. Veuillez la définir dans votre fichier .env ou les variables d'environnement.",
+ '{{envKeyHint}} environment variable not found (or set settings.security.auth.apiKey). Please set it in your .env file or environment variables.':
+ "Variable d'environnement {{envKeyHint}} introuvable (ou définissez settings.security.auth.apiKey). Veuillez la définir dans votre fichier .env ou les variables d'environnement.",
+ 'Missing API key for OpenAI-compatible auth. Set the {{envKeyHint}} environment variable.':
+ "Clé API manquante pour l'authentification compatible OpenAI. Définissez la variable d'environnement {{envKeyHint}}.",
+ 'Anthropic provider missing required baseUrl in modelProviders[].baseUrl.':
+ 'Le fournisseur Anthropic manque le baseUrl requis dans modelProviders[].baseUrl.',
+ 'ANTHROPIC_BASE_URL environment variable not found.':
+ "Variable d'environnement ANTHROPIC_BASE_URL introuvable.",
+ 'Invalid auth method selected.':
+ "Méthode d'authentification invalide sélectionnée.",
+ 'Failed to authenticate. Message: {{message}}':
+ "Échec de l'authentification. Message : {{message}}",
+ 'Authenticated successfully with {{authType}} credentials.':
+ 'Authentification réussie avec les identifiants {{authType}}.',
+ 'Invalid QWEN_DEFAULT_AUTH_TYPE value: "{{value}}". Valid values are: {{validValues}}':
+ 'Valeur QWEN_DEFAULT_AUTH_TYPE invalide : "{{value}}". Valeurs valides : {{validValues}}',
+ 'OpenAI Configuration Required': 'Configuration OpenAI requise',
+ 'Please enter your OpenAI configuration. You can get an API key from':
+ 'Veuillez entrer votre configuration OpenAI. Vous pouvez obtenir une clé API depuis',
+ 'API Key:': 'Clé API :',
+ 'Invalid credentials: {{errorMessage}}':
+ 'Identifiants invalides : {{errorMessage}}',
+ 'Failed to validate credentials': 'Échec de la validation des identifiants',
+ 'Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel':
+ 'Appuyez sur Entrée pour continuer, Tab/↑↓ pour naviguer, Échap pour annuler',
+
+ // ============================================================================
+ // Boîtes de dialogue - Modèle
+ // ============================================================================
+ 'Select Model': 'Sélectionner un modèle',
+ '(Press Esc to close)': '(Appuyez sur Échap pour fermer)',
+ 'Current (effective) configuration': 'Configuration actuelle (effective)',
+ AuthType: "Type d'auth",
+ 'API Key': 'Clé API',
+ unset: 'non défini',
+ '(default)': '(par défaut)',
+ '(set)': '(défini)',
+ '(not set)': '(non défini)',
+ Modality: 'Modalité',
+ 'Context Window': 'Fenêtre de contexte',
+ text: 'texte',
+ 'text-only': 'texte uniquement',
+ image: 'image',
+ pdf: 'pdf',
+ audio: 'audio',
+ video: 'vidéo',
+ 'not set': 'non défini',
+ none: 'aucun',
+ unknown: 'inconnu',
+ "Failed to switch model to '{{modelId}}'.\n\n{{error}}":
+ "Échec du changement de modèle vers '{{modelId}}'.\n\n{{error}}",
+ 'Qwen 3.6 Plus — efficient hybrid model with leading coding performance':
+ 'Qwen 3.6 Plus — modèle hybride efficace avec des performances de codage de pointe',
+ 'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)':
+ "Le dernier modèle Qwen Vision d'Alibaba Cloud ModelStudio (version : qwen3-vl-plus-2025-09-23)",
+
+ // ============================================================================
+ // Boîtes de dialogue - Permissions
+ // ============================================================================
+ 'Manage folder trust settings':
+ 'Gérer les paramètres de confiance des dossiers',
+ 'Manage permission rules': 'Gérer les règles de permission',
+ Allow: 'Autoriser',
+ Ask: 'Demander',
+ Deny: 'Refuser',
+ Workspace: 'Espace de travail',
+ "Qwen Code won't ask before using allowed tools.":
+ "Qwen Code ne demandera pas avant d'utiliser les outils autorisés.",
+ 'Qwen Code will ask before using these tools.':
+ "Qwen Code demandera avant d'utiliser ces outils.",
+ 'Qwen Code is not allowed to use denied tools.':
+ "Qwen Code n'est pas autorisé à utiliser les outils refusés.",
+ 'Manage trusted directories for this workspace.':
+ 'Gérer les répertoires de confiance pour cet espace de travail.',
+ 'Any use of the {{tool}} tool': "Toute utilisation de l'outil {{tool}}",
+ "{{tool}} commands matching '{{pattern}}'":
+ "Commandes {{tool}} correspondant à '{{pattern}}'",
+ 'From user settings': 'Depuis les paramètres utilisateur',
+ 'From project settings': 'Depuis les paramètres du projet',
+ 'From session': 'Depuis la session',
+ 'Project settings (local)': 'Paramètres du projet (local)',
+ 'Saved in .qwen/settings.local.json':
+ 'Enregistré dans .qwen/settings.local.json',
+ 'Project settings': 'Paramètres du projet',
+ 'Checked in at .qwen/settings.json': 'Validé dans .qwen/settings.json',
+ 'User settings': 'Paramètres utilisateur',
+ 'Saved in at ~/.qwen/settings.json': 'Enregistré dans ~/.qwen/settings.json',
+ 'Add a new rule…': 'Ajouter une nouvelle règle…',
+ 'Add {{type}} permission rule': 'Ajouter une règle de permission {{type}}',
+ 'Permission rules are a tool name, optionally followed by a specifier in parentheses.':
+ "Les règles de permission sont un nom d'outil, suivi optionnellement d'un spécificateur entre parenthèses.",
+ 'e.g.,': 'ex.,',
+ or: 'ou',
+ 'Enter permission rule…': 'Entrer une règle de permission…',
+ 'Enter to submit · Esc to cancel':
+ 'Entrée pour soumettre · Échap pour annuler',
+ 'Where should this rule be saved?':
+ 'Où cette règle doit-elle être enregistrée ?',
+ 'Enter to confirm · Esc to cancel':
+ 'Entrée pour confirmer · Échap pour annuler',
+ 'Delete {{type}} rule?': 'Supprimer la règle {{type}} ?',
+ 'Are you sure you want to delete this permission rule?':
+ 'Êtes-vous sûr de vouloir supprimer cette règle de permission ?',
+ 'Permissions:': 'Permissions :',
+ '(←/→ or tab to cycle)': '(←/→ ou tab pour cycler)',
+ 'Press ↑↓ to navigate · Enter to select · Type to search · Esc to cancel':
+ 'Appuyez sur ↑↓ pour naviguer · Entrée pour sélectionner · Tapez pour rechercher · Échap pour annuler',
+ 'Search…': 'Rechercher…',
+ 'Use /trust to manage folder trust settings for this workspace.':
+ 'Utilisez /trust pour gérer les paramètres de confiance des dossiers pour cet espace de travail.',
+ 'Add directory…': 'Ajouter un répertoire…',
+ 'Add directory to workspace': "Ajouter un répertoire à l'espace de travail",
+ 'Qwen Code can read files in the workspace, and make edits when auto-accept edits is on.':
+ "Qwen Code peut lire les fichiers dans l'espace de travail et effectuer des modifications lorsque l'acceptation automatique est activée.",
+ 'Qwen Code will be able to read files in this directory and make edits when auto-accept edits is on.':
+ "Qwen Code pourra lire les fichiers dans ce répertoire et effectuer des modifications lorsque l'acceptation automatique est activée.",
+ 'Enter the path to the directory:': 'Entrez le chemin vers le répertoire :',
+ 'Enter directory path…': 'Entrez le chemin du répertoire…',
+ 'Tab to complete · Enter to add · Esc to cancel':
+ 'Tab pour compléter · Entrée pour ajouter · Échap pour annuler',
+ 'Remove directory?': 'Supprimer le répertoire ?',
+ 'Are you sure you want to remove this directory from the workspace?':
+ "Êtes-vous sûr de vouloir supprimer ce répertoire de l'espace de travail ?",
+ ' (Original working directory)': " (Répertoire de travail d'origine)",
+ ' (from settings)': ' (depuis les paramètres)',
+ 'Directory does not exist.': "Le répertoire n'existe pas.",
+ 'Path is not a directory.': "Le chemin n'est pas un répertoire.",
+ 'This directory is already in the workspace.':
+ "Ce répertoire est déjà dans l'espace de travail.",
+ 'Already covered by existing directory: {{dir}}':
+ 'Déjà couvert par le répertoire existant : {{dir}}',
+
+ // ============================================================================
+ // Barre de statut
+ // ============================================================================
+ 'Using:': 'Utilisation :',
+ '{{count}} open file': '{{count}} fichier ouvert',
+ '{{count}} open files': '{{count}} fichiers ouverts',
+ '(ctrl+g to view)': '(ctrl+g pour afficher)',
+ '{{count}} {{name}} file': '{{count}} fichier {{name}}',
+ '{{count}} {{name}} files': '{{count}} fichiers {{name}}',
+ '{{count}} MCP server': '{{count}} serveur MCP',
+ '{{count}} MCP servers': '{{count}} serveurs MCP',
+ '{{count}} Blocked': '{{count}} bloqué(s)',
+ '(ctrl+t to view)': '(ctrl+t pour afficher)',
+ '(ctrl+t to toggle)': '(ctrl+t pour basculer)',
+ 'Press Ctrl+C again to exit.': 'Appuyez à nouveau sur Ctrl+C pour quitter.',
+ 'Press Ctrl+D again to exit.': 'Appuyez à nouveau sur Ctrl+D pour quitter.',
+ 'Press Esc again to clear.': 'Appuyez à nouveau sur Échap pour effacer.',
+
+ // ============================================================================
+ // Statut MCP
+ // ============================================================================
+ 'No MCP servers configured.': 'Aucun serveur MCP configuré.',
+ '⏳ MCP servers are starting up ({{count}} initializing)...':
+ '⏳ Les serveurs MCP démarrent ({{count}} en initialisation)...',
+ 'Note: First startup may take longer. Tool availability will update automatically.':
+ 'Remarque : Le premier démarrage peut prendre plus de temps. La disponibilité des outils se mettra à jour automatiquement.',
+ 'Configured MCP servers:': 'Serveurs MCP configurés :',
+ Ready: 'Prêt',
+ 'Starting... (first startup may take longer)':
+ 'Démarrage... (le premier démarrage peut prendre plus de temps)',
+ Disconnected: 'Déconnecté',
+ '{{count}} tool': '{{count}} outil',
+ '{{count}} tools': '{{count}} outils',
+ '{{count}} prompt': '{{count}} invite',
+ '{{count}} prompts': '{{count}} invites',
+ '(from {{extensionName}})': '(depuis {{extensionName}})',
+ OAuth: 'OAuth',
+ 'OAuth expired': 'OAuth expiré',
+ 'OAuth not authenticated': 'OAuth non authentifié',
+ 'tools and prompts will appear when ready':
+ 'les outils et invites apparaîtront quand prêts',
+ '{{count}} tools cached': '{{count}} outils mis en cache',
+ 'Tools:': 'Outils :',
+ 'Parameters:': 'Paramètres :',
+ 'Prompts:': 'Invites :',
+ Blocked: 'Bloqué',
+ '💡 Tips:': '💡 Conseils :',
+ Use: 'Utilisez',
+ 'to show server and tool descriptions':
+ 'pour afficher les descriptions des serveurs et des outils',
+ 'to show tool parameter schemas':
+ 'pour afficher les schémas de paramètres des outils',
+ 'to hide descriptions': 'pour masquer les descriptions',
+ 'to authenticate with OAuth-enabled servers':
+ 'pour authentifier avec des serveurs compatibles OAuth',
+ Press: 'Appuyez sur',
+ 'to toggle tool descriptions on/off':
+ 'pour activer/désactiver les descriptions des outils',
+ "Starting OAuth authentication for MCP server '{{name}}'...":
+ "Démarrage de l'authentification OAuth pour le serveur MCP '{{name}}'...",
+ 'Restarting MCP servers...': 'Redémarrage des serveurs MCP...',
+
+ // ============================================================================
+ // Conseils de démarrage
+ // ============================================================================
+ 'Tips:': 'Conseils :',
+ 'Use /compress when the conversation gets long to summarize history and free up context.':
+ "Utilisez /compress quand la conversation devient longue pour résumer l'historique et libérer le contexte.",
+ 'Start a fresh idea with /clear or /new; the previous session stays available in history.':
+ "Commencez une nouvelle idée avec /clear ou /new ; la session précédente reste disponible dans l'historique.",
+ 'Use /bug to submit issues to the maintainers when something goes off.':
+ 'Utilisez /bug pour soumettre des problèmes aux mainteneurs quand quelque chose ne va pas.',
+ 'Switch auth type quickly with /auth.':
+ "Changez rapidement le type d'authentification avec /auth.",
+ 'You can run any shell commands from Qwen Code using ! (e.g. !ls).':
+ "Vous pouvez exécuter n'importe quelle commande shell depuis Qwen Code en utilisant ! (ex. !ls).",
+ 'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.':
+ 'Tapez / pour ouvrir le menu des commandes ; Tab autocompléte les commandes slash et les invites sauvegardées.',
+ 'You can resume a previous conversation by running qwen --continue or qwen --resume.':
+ 'Vous pouvez reprendre une conversation précédente en exécutant qwen --continue ou qwen --resume.',
+ 'You can switch permission mode quickly with Shift+Tab or /approval-mode.':
+ 'Vous pouvez changer rapidement le mode de permission avec Maj+Tab ou /approval-mode.',
+ 'You can switch permission mode quickly with Tab or /approval-mode.':
+ 'Vous pouvez changer rapidement le mode de permission avec Tab ou /approval-mode.',
+ 'Try /insight to generate personalized insights from your chat history.':
+ 'Essayez /insight pour générer des insights personnalisés depuis votre historique de chat.',
+
+ // ============================================================================
+ // Écran de sortie / Stats
+ // ============================================================================
+ 'Agent powering down. Goodbye!': "Agent en cours d'arrêt. Au revoir !",
+ 'To continue this session, run': 'Pour continuer cette session, exécutez',
+ 'Interaction Summary': "Résumé de l'interaction",
+ 'Session ID:': 'ID de session :',
+ 'Tool Calls:': "Appels d'outils :",
+ 'Success Rate:': 'Taux de succès :',
+ 'User Agreement:': "Accord de l'utilisateur :",
+ reviewed: 'révisé',
+ 'Code Changes:': 'Modifications du code :',
+ Performance: 'Performance',
+ 'Wall Time:': 'Temps réel :',
+ 'Agent Active:': 'Agent actif :',
+ 'API Time:': 'Temps API :',
+ 'Tool Time:': "Temps d'outil :",
+ 'Session Stats': 'Stats de session',
+ 'Model Usage': 'Utilisation du modèle',
+ Reqs: 'Req.',
+ 'Input Tokens': "Tokens d'entrée",
+ 'Output Tokens': 'Tokens de sortie',
+ 'Savings Highlight:': 'Économies notables :',
+ 'of input tokens were served from the cache, reducing costs.':
+ "des tokens d'entrée ont été servis depuis le cache, réduisant les coûts.",
+ 'Tip: For a full token breakdown, run `/stats model`.':
+ 'Conseil : Pour une décomposition complète des tokens, exécutez `/stats model`.',
+ 'Model Stats For Nerds': 'Stats du modèle pour les geeks',
+ 'Tool Stats For Nerds': 'Stats des outils pour les geeks',
+ Metric: 'Métrique',
+ API: 'API',
+ Requests: 'Requêtes',
+ Errors: 'Erreurs',
+ 'Avg Latency': 'Latence moyenne',
+ Tokens: 'Tokens',
+ Total: 'Total',
+ Prompt: 'Invite',
+ Cached: 'En cache',
+ Thoughts: 'Réflexions',
+ Tool: 'Outil',
+ Output: 'Sortie',
+ 'No API calls have been made in this session.':
+ "Aucun appel API n'a été effectué dans cette session.",
+ 'Tool Name': "Nom de l'outil",
+ Calls: 'Appels',
+ 'Success Rate': 'Taux de succès',
+ 'Avg Duration': 'Durée moyenne',
+ 'User Decision Summary': "Résumé des décisions de l'utilisateur",
+ 'Total Reviewed Suggestions:': 'Total des suggestions révisées :',
+ ' » Accepted:': ' » Acceptées :',
+ ' » Rejected:': ' » Rejetées :',
+ ' » Modified:': ' » Modifiées :',
+ ' Overall Agreement Rate:': " Taux d'accord global :",
+ 'No tool calls have been made in this session.':
+ "Aucun appel d'outil n'a été effectué dans cette session.",
+ 'Session start time is unavailable, cannot calculate stats.':
+ "L'heure de début de session est indisponible, impossible de calculer les stats.",
+
+ // ============================================================================
+ // Migration de format de commande
+ // ============================================================================
+ 'Command Format Migration': 'Migration du format de commande',
+ 'Found {{count}} TOML command file:':
+ 'Trouvé {{count}} fichier de commande TOML :',
+ 'Found {{count}} TOML command files:':
+ 'Trouvé {{count}} fichiers de commande TOML :',
+ '... and {{count}} more': '... et {{count}} de plus',
+ 'The TOML format is deprecated. Would you like to migrate them to Markdown format?':
+ 'Le format TOML est obsolète. Souhaitez-vous les migrer vers le format Markdown ?',
+ '(Backups will be created and original files will be preserved)':
+ '(Des sauvegardes seront créées et les fichiers originaux seront conservés)',
+
+ // ============================================================================
+ // Phrases de chargement
+ // ============================================================================
+ 'Waiting for user confirmation...':
+ "En attente de la confirmation de l'utilisateur...",
+ '(esc to cancel, {{time}})': '(échap pour annuler, {{time}})',
+
+ // ============================================================================
+ // Phrases de chargement amusantes
+ // ============================================================================
+ WITTY_LOADING_PHRASES: [
+ 'Je me sens chanceux',
+ "Livraison d'excellence...",
+ 'Repeignant les empattements...',
+ 'Navigation dans le moisissure numérique...',
+ 'Consultation des esprits numériques...',
+ 'Réticuler les splines...',
+ 'Réchauffement des hamsters IA...',
+ 'Consultation de la conque magique...',
+ "Génération d'une réplique spirituelle...",
+ 'Polissage des algorithmes...',
+ 'Ne précipitez pas la perfection (ni mon code)...',
+ 'Brassage de nouveaux octets...',
+ 'Comptage des électrons...',
+ 'Engagement des processeurs cognitifs...',
+ "Vérification des erreurs de syntaxe dans l'univers...",
+ "Un instant, optimisation de l'humour...",
+ 'Mélange des chutes de répliques...',
+ 'Démêlage des réseaux de neurones...',
+ 'Compilation de la brillance...',
+ 'Chargement de wit.exe...',
+ 'Invocation du nuage de sagesse...',
+ "Préparation d'une réponse spirituelle...",
+ 'Juste une seconde, je débogue la réalité...',
+ 'Confusion des options...',
+ 'Accord des fréquences cosmiques...',
+ "Création d'une réponse digne de votre patience...",
+ 'Compilation des 0 et des 1...',
+ 'Résolution des dépendances... et des crises existentielles...',
+ 'Défragmentation des mémoires... RAM et personnelles...',
+ 'Redémarrage du module humoristique...',
+ "Mise en cache de l'essentiel (surtout les mèmes de chats)...",
+ 'Optimisation pour une vitesse ludicrous',
+ 'Échange de bits... ne le dites pas aux octets...',
+ 'Nettoyage de la mémoire... je reviens...',
+ 'Assemblage des internets...',
+ 'Conversion de café en code...',
+ 'Mise à jour de la syntaxe de la réalité...',
+ 'Recâblage des synapses...',
+ "Recherche d'un point-virgule égaré...",
+ 'Graissage des rouages de la machine...',
+ 'Préchauffage des serveurs...',
+ 'Calibrage du condensateur de flux...',
+ "Engagement de l'entraînement de l'improbabilité...",
+ 'Canalisation de la Force...',
+ 'Alignement des étoiles pour une réponse optimale...',
+ "Qu'il en soit ainsi pour nous tous...",
+ 'Chargement de la prochaine grande idée...',
+ 'Juste un moment, je suis dans la zone...',
+ 'Préparation à vous éblouir de brillance...',
+ 'Juste un instant, je peaufine mon esprit...',
+ "Attendez, je crée un chef-d'œuvre...",
+ "Juste une seconde, je débogue l'univers...",
+ "Juste un moment, j'aligne les pixels...",
+ "Juste un instant, j'optimise l'humour...",
+ "Juste un moment, j'accorde les algorithmes...",
+ 'Vitesse warp enclenchée...',
+ 'Extraction de plus de cristaux de Dilithium...',
+ 'Pas de panique...',
+ 'Suivre le lapin blanc...',
+ 'La vérité est là... quelque part...',
+ 'Souffler sur la cartouche...',
+ 'Chargement... Faites un tonneau !',
+ 'En attente du respawn...',
+ 'Finir la course de Kessel en moins de 12 parsecs...',
+ "Le gâteau n'est pas un mensonge, il charge juste encore...",
+ "Bidouillage de l'écran de création de personnage...",
+ 'Juste un moment, je cherche le bon mème...',
+ "Appuyer sur 'A' pour continuer...",
+ 'Rassemblement de chats numériques...',
+ 'Polissage des pixels...',
+ "Recherche d'un jeu de mots d'écran de chargement approprié...",
+ 'Vous distraire avec cette phrase spirituelle...',
+ 'Presque là... probablement...',
+ "Nos hamsters travaillent aussi vite qu'ils peuvent...",
+ 'Donnant une tape dans le dos à Cloudy...',
+ 'Caressant le chat...',
+ 'Rickrolling mon patron...',
+ 'Je ne vais jamais vous abandonner, je ne vais jamais vous laisser tomber...',
+ 'Claquant la basse...',
+ 'Goûtant les snozberries...',
+ "Je vais jusqu'au bout, je vais à toute vitesse...",
+ 'Est-ce la vraie vie ? Est-ce juste une fantaisie ?...',
+ "J'ai un bon pressentiment à ce sujet...",
+ "Poking l'ours...",
+ 'Faire des recherches sur les derniers mèmes...',
+ 'Trouver comment rendre ça plus spirituel...',
+ 'Hmm... laissez-moi réfléchir...',
+ 'Comment appelle-t-on un poisson sans yeux ? Un posson...',
+ "Pourquoi l'ordinateur est-il allé en thérapie ? Il avait trop d'octets...",
+ "Pourquoi les programmeurs n'aiment pas la nature ? Elle a trop de bugs...",
+ 'Pourquoi les programmeurs préfèrent le mode sombre ? Parce que la lumière attire les bugs...',
+ "Pourquoi le développeur est-il fauché ? Parce qu'il a utilisé tout son cache...",
+ "Que peut-on faire avec un crayon cassé ? Rien, c'est inutile...",
+ 'Application de la maintenance percussive...',
+ 'Recherche de la bonne orientation USB...',
+ "S'assurer que la fumée magique reste à l'intérieur des câbles...",
+ 'Essai de quitter Vim...',
+ 'Mise en marche de la roue du hamster...',
+ "Ce n'est pas un bug, c'est une fonctionnalité non documentée...",
+ 'Engage.',
+ 'Je reviendrai... avec une réponse.',
+ 'Mon autre processus est un TARDIS...',
+ "Communion avec l'esprit machine...",
+ 'Laisser les pensées mariner...',
+ "Je viens de me souvenir où j'ai mis mes clés...",
+ "Contemplation de l'orbe...",
+ "J'ai vu des choses que vous ne croiriez pas... comme un utilisateur qui lit les messages de chargement.",
+ 'Initiation du regard pensif...',
+ "Quel est le goûter préféré d'un ordinateur ? Les microchips.",
+ "Pourquoi les développeurs Java portent-ils des lunettes ? Parce qu'ils ne C# pas.",
+ 'Chargement du laser... pew pew !',
+ 'Division par zéro... je plaisante !',
+ "Recherche d'un superviseur... je veux dire, traitement.",
+ 'Faire du bip boop.',
+ "Buffering... parce que même les IAs ont besoin d'un moment.",
+ 'Enchevêtrement de particules quantiques pour une réponse plus rapide...',
+ 'Polissage du chrome... sur les algorithmes.',
+ "N'êtes-vous pas diverti ? (On y travaille !)",
+ 'Invocation des lutins de code... pour aider, bien sûr.',
+ 'En attente de la tonalité du modem...',
+ "Recalibrage du sens de l'humour.",
+ 'Mon autre écran de chargement est encore plus drôle.',
+ "Je suis presque sûr qu'il y a un chat qui marche sur le clavier quelque part...",
+ 'Amélioration... Amélioration... Toujours en chargement.',
+ "Ce n'est pas un bug, c'est une caractéristique... de cet écran de chargement.",
+ "Avez-vous essayé de l'éteindre et de le rallumer ? (L'écran de chargement, pas moi.)",
+ 'Construction de pylônes supplémentaires...',
+ ],
+
+ // ============================================================================
+ // Paramètres d'extension - Saisie
+ // ============================================================================
+ 'Enter value...': 'Entrer une valeur...',
+ 'Enter sensitive value...': 'Entrer une valeur sensible...',
+ 'Press Enter to submit, Escape to cancel':
+ 'Appuyez sur Entrée pour soumettre, Échap pour annuler',
+
+ // ============================================================================
+ // Outil de migration de commandes
+ // ============================================================================
+ 'Markdown file already exists: {{filename}}':
+ 'Le fichier Markdown existe déjà : {{filename}}',
+ 'TOML Command Format Deprecation Notice':
+ "Avis d'obsolescence du format de commande TOML",
+ 'Found {{count}} command file(s) in TOML format:':
+ 'Trouvé {{count}} fichier(s) de commande au format TOML :',
+ 'The TOML format for commands is being deprecated in favor of Markdown format.':
+ "Le format TOML pour les commandes est en cours d'abandon au profit du format Markdown.",
+ 'Markdown format is more readable and easier to edit.':
+ 'Le format Markdown est plus lisible et plus facile à modifier.',
+ 'You can migrate these files automatically using:':
+ 'Vous pouvez migrer ces fichiers automatiquement en utilisant :',
+ 'Or manually convert each file:':
+ 'Ou convertir chaque fichier manuellement :',
+ 'TOML: prompt = "..." / description = "..."':
+ 'TOML : prompt = "..." / description = "..."',
+ 'Markdown: YAML frontmatter + content':
+ 'Markdown : YAML frontmatter + contenu',
+ 'The migration tool will:': "L'outil de migration va :",
+ 'Convert TOML files to Markdown': 'Convertir les fichiers TOML en Markdown',
+ 'Create backups of original files':
+ 'Créer des sauvegardes des fichiers originaux',
+ 'Preserve all command functionality':
+ 'Préserver toutes les fonctionnalités des commandes',
+ 'TOML format will continue to work for now, but migration is recommended.':
+ "Le format TOML continuera à fonctionner pour l'instant, mais la migration est recommandée.",
+
+ // ============================================================================
+ // Extensions - Commande Explore
+ // ============================================================================
+ 'Open extensions page in your browser':
+ 'Ouvrir la page des extensions dans votre navigateur',
+ 'Unknown extensions source: {{source}}.':
+ "Source d'extensions inconnue : {{source}}.",
+ 'Would open extensions page in your browser: {{url}} (skipped in test environment)':
+ 'Ouvrirait la page des extensions dans votre navigateur : {{url}} (ignoré en environnement de test)',
+ 'View available extensions at {{url}}':
+ 'Voir les extensions disponibles sur {{url}}',
+ 'Opening extensions page in your browser: {{url}}':
+ 'Ouverture de la page des extensions dans votre navigateur : {{url}}',
+ 'Failed to open browser. Check out the extensions gallery at {{url}}':
+ "Échec de l'ouverture du navigateur. Consultez la galerie d'extensions sur {{url}}",
+
+ // ============================================================================
+ // Réessai / Limite de débit
+ // ============================================================================
+ 'Rate limit error: {{reason}}': 'Erreur de limite de débit : {{reason}}',
+ 'Retrying in {{seconds}} seconds… (attempt {{attempt}}/{{maxRetries}})':
+ 'Nouvelle tentative dans {{seconds}} secondes… (tentative {{attempt}}/{{maxRetries}})',
+ 'Press Ctrl+Y to retry': 'Appuyez sur Ctrl+Y pour réessayer',
+ 'No failed request to retry.': 'Aucune requête échouée à réessayer.',
+ 'to retry last request': 'pour réessayer la dernière requête',
+
+ // ============================================================================
+ // Authentification du plan de codage
+ // ============================================================================
+ 'API key cannot be empty.': 'La clé API ne peut pas être vide.',
+ 'You can get your Coding Plan API key here':
+ 'Vous pouvez obtenir votre clé API Coding Plan ici',
+ 'API key is stored in settings.env. You can migrate it to a .env file for better security.':
+ 'La clé API est stockée dans settings.env. Vous pouvez la migrer vers un fichier .env pour une meilleure sécurité.',
+ 'New model configurations are available for Alibaba Cloud Coding Plan. Update now?':
+ 'De nouvelles configurations de modèle sont disponibles pour Alibaba Cloud Coding Plan. Mettre à jour maintenant ?',
+ 'Coding Plan configuration updated successfully. New models are now available.':
+ 'Configuration Coding Plan mise à jour avec succès. Les nouveaux modèles sont maintenant disponibles.',
+ 'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
+ 'Clé API Coding Plan introuvable. Veuillez vous réauthentifier avec Coding Plan.',
+ 'Failed to update Coding Plan configuration: {{message}}':
+ 'Échec de la mise à jour de la configuration Coding Plan : {{message}}',
+
+ // ============================================================================
+ // Configuration de clé API personnalisée
+ // ============================================================================
+ 'You can configure your API key and models in settings.json':
+ 'Vous pouvez configurer votre clé API et vos modèles dans settings.json',
+ 'Refer to the documentation for setup instructions':
+ 'Consultez la documentation pour les instructions de configuration',
+
+ // ============================================================================
+ // Boîte de dialogue Auth - Titres et étiquettes
+ // ============================================================================
+ 'Coding Plan': 'Plan de codage',
+ "Paste your api key of ModelStudio Coding Plan and you're all set!":
+ "Collez votre clé API de ModelStudio Coding Plan et c'est parti !",
+ Custom: 'Personnalisé',
+ 'More instructions about configuring `modelProviders` manually.':
+ "Plus d'instructions sur la configuration manuelle de `modelProviders`.",
+ 'Select API-KEY configuration mode:':
+ 'Sélectionner le mode de configuration API-KEY :',
+ '(Press Escape to go back)': '(Appuyez sur Échap pour revenir)',
+ '(Press Enter to submit, Escape to cancel)':
+ '(Appuyez sur Entrée pour soumettre, Échap pour annuler)',
+ 'Select Region for Coding Plan': 'Sélectionner la région pour Coding Plan',
+ 'Choose based on where your account is registered':
+ "Choisissez en fonction de l'endroit où votre compte est enregistré",
+ 'Enter Coding Plan API Key': 'Entrer la clé API Coding Plan',
+
+ // ============================================================================
+ // Mises à jour internationales Coding Plan
+ // ============================================================================
+ 'New model configurations are available for {{region}}. Update now?':
+ 'De nouvelles configurations de modèle sont disponibles pour {{region}}. Mettre à jour maintenant ?',
+ '{{region}} configuration updated successfully. Model switched to "{{model}}".':
+ 'Configuration {{region}} mise à jour avec succès. Modèle changé en "{{model}}".',
+ 'Authenticated successfully with {{region}}. API key and model configs saved to settings.json (backed up).':
+ 'Authentification réussie avec {{region}}. Clé API et configurations de modèle enregistrées dans settings.json (sauvegardé).',
+
+ // ============================================================================
+ // Composant d'utilisation du contexte
+ // ============================================================================
+ 'Context Usage': 'Utilisation du contexte',
+ 'No API response yet. Send a message to see actual usage.':
+ "Pas encore de réponse API. Envoyez un message pour voir l'utilisation réelle.",
+ 'Estimated pre-conversation overhead':
+ 'Surcharge estimée avant la conversation',
+ 'Context window': 'Fenêtre de contexte',
+ tokens: 'tokens',
+ Used: 'Utilisé',
+ Free: 'Libre',
+ 'Autocompact buffer': 'Tampon de compaction automatique',
+ 'Usage by category': 'Utilisation par catégorie',
+ 'System prompt': 'Invite système',
+ 'Built-in tools': 'Outils intégrés',
+ 'MCP tools': 'Outils MCP',
+ 'Memory files': 'Fichiers mémoire',
+ Skills: 'Compétences',
+ Messages: 'Messages',
+ 'Show context window usage breakdown.':
+ "Afficher la répartition de l'utilisation de la fenêtre de contexte.",
+ 'Run /context detail for per-item breakdown.':
+ 'Exécutez /context detail pour une répartition par élément.',
+ 'body loaded': 'corps chargé',
+ memory: 'mémoire',
+ '{{region}} configuration updated successfully.':
+ 'Configuration {{region}} mise à jour avec succès.',
+ 'Authenticated successfully with {{region}}. API key and model configs saved to settings.json.':
+ 'Authentification réussie avec {{region}}. Clé API et configurations de modèle enregistrées dans settings.json.',
+ 'Tip: Use /model to switch between available Coding Plan models.':
+ 'Conseil : Utilisez /model pour basculer entre les modèles Coding Plan disponibles.',
+
+ // ============================================================================
+ // Outil de question à l'utilisateur
+ // ============================================================================
+ 'Please answer the following question(s):':
+ 'Veuillez répondre à la (aux) question(s) suivante(s) :',
+ 'Cannot ask user questions in non-interactive mode. Please run in interactive mode to use this tool.':
+ "Impossible de poser des questions à l'utilisateur en mode non interactif. Veuillez exécuter en mode interactif pour utiliser cet outil.",
+ 'User declined to answer the questions.':
+ "L'utilisateur a refusé de répondre aux questions.",
+ 'User has provided the following answers:':
+ "L'utilisateur a fourni les réponses suivantes :",
+ 'Failed to process user answers:':
+ "Échec du traitement des réponses de l'utilisateur :",
+ 'Type something...': 'Tapez quelque chose...',
+ Submit: 'Soumettre',
+ 'Submit answers': 'Soumettre les réponses',
+ Cancel: 'Annuler',
+ 'Your answers:': 'Vos réponses :',
+ '(not answered)': '(sans réponse)',
+ 'Ready to submit your answers?': 'Prêt à soumettre vos réponses ?',
+ '↑/↓: Navigate | ←/→: Switch tabs | Enter: Select':
+ "↑/↓ : Naviguer | ←/→ : Changer d'onglet | Entrée : Sélectionner",
+ '↑/↓: Navigate | ←/→: Switch tabs | Space/Enter: Toggle | Esc: Cancel':
+ "↑/↓ : Naviguer | ←/→ : Changer d'onglet | Espace/Entrée : Basculer | Échap : Annuler",
+ '↑/↓: Navigate | Space/Enter: Toggle | Esc: Cancel':
+ '↑/↓ : Naviguer | Espace/Entrée : Basculer | Échap : Annuler',
+ '↑/↓: Navigate | Enter: Select | Esc: Cancel':
+ '↑/↓ : Naviguer | Entrée : Sélectionner | Échap : Annuler',
+
+ // ============================================================================
+ // Commandes - Auth
+ // ============================================================================
+ 'Configure Qwen authentication information with Qwen-OAuth or Alibaba Cloud Coding Plan':
+ "Configurer les informations d'authentification Qwen avec Qwen-OAuth ou Alibaba Cloud Coding Plan",
+ 'Authenticate using Qwen OAuth': 'Authentifier avec Qwen OAuth',
+ 'Authenticate using Alibaba Cloud Coding Plan':
+ 'Authentifier avec Alibaba Cloud Coding Plan',
+ 'Region for Coding Plan (china/global)':
+ 'Région pour Coding Plan (china/global)',
+ 'API key for Coding Plan': 'Clé API pour Coding Plan',
+ 'Show current authentication status':
+ "Afficher le statut d'authentification actuel",
+ 'Authentication completed successfully.':
+ 'Authentification terminée avec succès.',
+ 'Starting Qwen OAuth authentication...':
+ "Démarrage de l'authentification Qwen OAuth...",
+ 'Successfully authenticated with Qwen OAuth.':
+ 'Authentification réussie avec Qwen OAuth.',
+ 'Failed to authenticate with Qwen OAuth: {{error}}':
+ "Échec de l'authentification avec Qwen OAuth : {{error}}",
+ 'Processing Alibaba Cloud Coding Plan authentication...':
+ "Traitement de l'authentification Alibaba Cloud Coding Plan...",
+ 'Successfully authenticated with Alibaba Cloud Coding Plan.':
+ 'Authentification réussie avec Alibaba Cloud Coding Plan.',
+ 'Failed to authenticate with Coding Plan: {{error}}':
+ "Échec de l'authentification avec Coding Plan : {{error}}",
+ '中国 (China)': '中国 (Chine)',
+ '阿里云百炼 (aliyun.com)': '阿里云百炼 (aliyun.com)',
+ Global: 'Global',
+ 'Alibaba Cloud (alibabacloud.com)': 'Alibaba Cloud (alibabacloud.com)',
+ 'Select region for Coding Plan:': 'Sélectionner la région pour Coding Plan :',
+ 'Enter your Coding Plan API key: ': 'Entrez votre clé API Coding Plan : ',
+ 'Select authentication method:':
+ "Sélectionner la méthode d'authentification :",
+ '\n=== Authentication Status ===\n': "\n=== Statut d'authentification ===\n",
+ '⚠️ No authentication method configured.\n':
+ "⚠️ Aucune méthode d'authentification configurée.\n",
+ 'Run one of the following commands to get started:\n':
+ "Exécutez l'une des commandes suivantes pour commencer :\n",
+ ' qwen auth qwen-oauth - Authenticate with Qwen OAuth (discontinued)':
+ ' qwen auth qwen-oauth - Authentification avec Qwen OAuth (abandonné)',
+ ' qwen auth coding-plan - Authenticate with Alibaba Cloud Coding Plan\n':
+ ' qwen auth coding-plan - Authentifier avec Alibaba Cloud Coding Plan\n',
+ 'Or simply run:': 'Ou simplement exécutez :',
+ ' qwen auth - Interactive authentication setup\n':
+ " qwen auth - Configuration d'authentification interactive\n",
+ '✓ Authentication Method: Qwen OAuth':
+ "✓ Méthode d'authentification : Qwen OAuth",
+ ' Type: Free tier (discontinued 2026-04-15)':
+ ' Type : Niveau gratuit (abandonné 2026-04-15)',
+ ' Limit: No longer available': ' Limite : Plus disponible',
+ 'Qwen OAuth free tier was discontinued on 2026-04-15. Run /auth to switch to Coding Plan, OpenRouter, Fireworks AI, or another provider.':
+ 'Le niveau gratuit Qwen OAuth a été abandonné le 2026-04-15. Exécutez /auth pour passer à Coding Plan, OpenRouter, Fireworks AI ou un autre fournisseur.',
+ ' Models: Qwen latest models\n': ' Modèles : Derniers modèles Qwen\n',
+ '✓ Authentication Method: Alibaba Cloud Coding Plan':
+ "✓ Méthode d'authentification : Alibaba Cloud Coding Plan",
+ '中国 (China) - 阿里云百炼': '中国 (Chine) - 阿里云百炼',
+ 'Global - Alibaba Cloud': 'Global - Alibaba Cloud',
+ ' Region: {{region}}': ' Région : {{region}}',
+ ' Current Model: {{model}}': ' Modèle actuel : {{model}}',
+ ' Config Version: {{version}}': ' Version de config : {{version}}',
+ ' Status: API key configured\n': ' Statut : Clé API configurée\n',
+ '⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)':
+ "⚠️ Méthode d'authentification : Alibaba Cloud Coding Plan (Incomplète)",
+ ' Issue: API key not found in environment or settings\n':
+ " Problème : Clé API introuvable dans l'environnement ou les paramètres\n",
+ ' Run `qwen auth coding-plan` to re-configure.\n':
+ ' Exécutez `qwen auth coding-plan` pour reconfigurer.\n',
+ '✓ Authentication Method: {{type}}':
+ "✓ Méthode d'authentification : {{type}}",
+ ' Status: Configured\n': ' Statut : Configuré\n',
+ 'Failed to check authentication status: {{error}}':
+ "Échec de la vérification du statut d'authentification : {{error}}",
+ 'Select an option:': 'Sélectionner une option :',
+ 'Raw mode not available. Please run in an interactive terminal.':
+ 'Mode brut non disponible. Veuillez exécuter dans un terminal interactif.',
+ '(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
+ '(Utilisez les flèches ↑ ↓ pour naviguer, Entrée pour sélectionner, Ctrl+C pour quitter)\n',
+ compact: 'compact',
+ 'Hide tool output and thinking for a cleaner view (toggle with Ctrl+O).':
+ 'Masquer la sortie des outils et la réflexion pour une vue plus nette (basculer avec Ctrl+O).',
+ 'Press Ctrl+O to show full tool output':
+ 'Appuyez sur Ctrl+O pour afficher la sortie complète des outils',
+ 'Switch to plan mode or exit plan mode':
+ 'Passer en mode plan ou quitter le mode plan',
+ 'Exited plan mode. Previous approval mode restored.':
+ "Mode plan quitté. Mode d'approbation précédent restauré.",
+ 'Enabled plan mode. The agent will analyze and plan without executing tools.':
+ "Mode plan activé. L'agent analysera et planifiera sans exécuter d'outils.",
+ 'Already in plan mode. Use "/plan exit" to exit plan mode.':
+ 'Déjà en mode plan. Utilisez "/plan exit" pour quitter le mode plan.',
+ 'Not in plan mode. Use "/plan" to enter plan mode first.':
+ 'Pas en mode plan. Utilisez "/plan" pour entrer en mode plan d\'abord.',
+
+ "Set up Qwen Code's status line UI":
+ "Configurer l'interface de la barre de statut de Qwen Code",
+};
diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js
index f89b63b02..77a4a3605 100644
--- a/packages/cli/src/i18n/locales/ja.js
+++ b/packages/cli/src/i18n/locales/ja.js
@@ -315,6 +315,8 @@ export default {
'Vision Model Preview': 'ビジョンモデルプレビュー',
'Tool Schema Compliance': 'ツールスキーマ準拠',
'Auto (detect from system)': '自動(システムから検出)',
+ 'Auto (detect terminal theme)': '自動(端末テーマを検出)',
+ Auto: '自動',
'check session stats. Usage: /stats [model|tools]':
'セッション統計を確認。使い方: /stats [model|tools]',
'Show model-specific usage statistics.': 'モデル別の使用統計を表示',
diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js
index c569e58f9..45fed2700 100644
--- a/packages/cli/src/i18n/locales/pt.js
+++ b/packages/cli/src/i18n/locales/pt.js
@@ -373,6 +373,8 @@ export default {
// Settings enum options
'Auto (detect from system)': 'Automático (detectar do sistema)',
+ 'Auto (detect terminal theme)': 'Automático (detectar tema do terminal)',
+ Auto: 'Automático',
Text: 'Texto',
JSON: 'JSON',
Plan: 'Planejamento',
diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js
index 33fcfbb54..aaccd1522 100644
--- a/packages/cli/src/i18n/locales/ru.js
+++ b/packages/cli/src/i18n/locales/ru.js
@@ -368,6 +368,8 @@ export default {
'Tool Schema Compliance': 'Соответствие схеме инструмента',
// Варианты перечислений настроек
'Auto (detect from system)': 'Авто (определить из системы)',
+ 'Auto (detect terminal theme)': 'Авто (определить тему терминала)',
+ Auto: 'Авто',
Text: 'Текст',
JSON: 'JSON',
Plan: 'План',
diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js
index ccb3da499..f8abeb895 100644
--- a/packages/cli/src/i18n/locales/zh.js
+++ b/packages/cli/src/i18n/locales/zh.js
@@ -412,6 +412,8 @@ export default {
'Tool Schema Compliance': '工具 Schema 兼容性',
// Settings enum options
'Auto (detect from system)': '自动(从系统检测)',
+ 'Auto (detect terminal theme)': '自动(检测终端主题)',
+ Auto: '自动',
Text: '文本',
JSON: 'JSON',
Plan: '规划',
diff --git a/packages/cli/src/nonInteractiveCli.test.ts b/packages/cli/src/nonInteractiveCli.test.ts
index 8bd34ca22..438cb0cf2 100644
--- a/packages/cli/src/nonInteractiveCli.test.ts
+++ b/packages/cli/src/nonInteractiveCli.test.ts
@@ -146,6 +146,7 @@ describe('runNonInteractive', () => {
isInteractive: vi.fn().mockReturnValue(false),
isCronEnabled: vi.fn().mockReturnValue(false),
getCronScheduler: vi.fn().mockReturnValue(null),
+ getDisabledSlashCommands: vi.fn().mockReturnValue([]),
} as unknown as Config;
mockSettings = {
@@ -168,9 +169,8 @@ describe('runNonInteractive', () => {
computeMergedSettings: vi.fn(),
} as unknown as LoadedSettings;
- const { handleAtCommand } = await import(
- './ui/hooks/atCommandProcessor.js'
- );
+ const { handleAtCommand } =
+ await import('./ui/hooks/atCommandProcessor.js');
vi.mocked(handleAtCommand).mockImplementation(async ({ query }) => ({
processedQuery: [{ text: query }],
shouldProceed: true,
@@ -470,9 +470,8 @@ describe('runNonInteractive', () => {
it('should preprocess @include commands before sending to the model', async () => {
setupMetricsMock();
// 1. Mock the imported atCommandProcessor
- const { handleAtCommand } = await import(
- './ui/hooks/atCommandProcessor.js'
- );
+ const { handleAtCommand } =
+ await import('./ui/hooks/atCommandProcessor.js');
const mockHandleAtCommand = vi.mocked(handleAtCommand);
// 2. Define the raw input and the expected processed output
@@ -1095,62 +1094,65 @@ describe('runNonInteractive', () => {
});
});
- it.skip('should emit a single user envelope when userEnvelope is provided', async () => {
- (mockConfig.getOutputFormat as Mock).mockReturnValue('stream-json');
- (mockConfig.getIncludePartialMessages as Mock).mockReturnValue(false);
-
- const writes: string[] = [];
- processStdoutSpy.mockImplementation((chunk: string | Uint8Array) => {
- if (typeof chunk === 'string') {
- writes.push(chunk);
- } else {
- writes.push(Buffer.from(chunk).toString('utf8'));
- }
- return true;
- });
-
- mockGeminiClient.sendMessageStream.mockReturnValue(
- createStreamFromEvents([
- { type: GeminiEventType.Content, value: 'Handled once' },
- {
- type: GeminiEventType.Finished,
- value: { reason: undefined, usageMetadata: { totalTokenCount: 2 } },
- },
- ]),
- );
+ it.todo(
+ 'should emit a single user envelope when userEnvelope is provided',
+ async () => {
+ (mockConfig.getOutputFormat as Mock).mockReturnValue('stream-json');
+ (mockConfig.getIncludePartialMessages as Mock).mockReturnValue(false);
+
+ const writes: string[] = [];
+ processStdoutSpy.mockImplementation((chunk: string | Uint8Array) => {
+ if (typeof chunk === 'string') {
+ writes.push(chunk);
+ } else {
+ writes.push(Buffer.from(chunk).toString('utf8'));
+ }
+ return true;
+ });
- const userEnvelope = {
- type: 'user',
- message: {
- role: 'user',
- content: [
+ mockGeminiClient.sendMessageStream.mockReturnValue(
+ createStreamFromEvents([
+ { type: GeminiEventType.Content, value: 'Handled once' },
{
- type: 'text',
- text: '来自 envelope 的消息',
+ type: GeminiEventType.Finished,
+ value: { reason: undefined, usageMetadata: { totalTokenCount: 2 } },
},
- ],
- },
- } as unknown as CLIUserMessage;
+ ]),
+ );
- await runNonInteractive(
- mockConfig,
- mockSettings,
- 'ignored input',
- 'prompt-envelope',
- {
- userMessage: userEnvelope,
- },
- );
+ const userEnvelope = {
+ type: 'user',
+ message: {
+ role: 'user',
+ content: [
+ {
+ type: 'text',
+ text: '来自 envelope 的消息',
+ },
+ ],
+ },
+ } as unknown as CLIUserMessage;
- const envelopes = writes
- .join('')
- .split('\n')
- .filter((line) => line.trim().length > 0)
- .map((line) => JSON.parse(line));
+ await runNonInteractive(
+ mockConfig,
+ mockSettings,
+ 'ignored input',
+ 'prompt-envelope',
+ {
+ userMessage: userEnvelope,
+ },
+ );
- const userEnvelopes = envelopes.filter((env) => env.type === 'user');
- expect(userEnvelopes).toHaveLength(0);
- });
+ const envelopes = writes
+ .join('')
+ .split('\n')
+ .filter((line) => line.trim().length > 0)
+ .map((line) => JSON.parse(line));
+
+ const userEnvelopes = envelopes.filter((env) => env.type === 'user');
+ expect(userEnvelopes).toHaveLength(0);
+ },
+ );
it('should include usage metadata and API duration in stream-json result', async () => {
(mockConfig.getOutputFormat as Mock).mockReturnValue('stream-json');
diff --git a/packages/cli/src/nonInteractiveCliCommands.test.ts b/packages/cli/src/nonInteractiveCliCommands.test.ts
index c1c47c678..aed49c0b8 100644
--- a/packages/cli/src/nonInteractiveCliCommands.test.ts
+++ b/packages/cli/src/nonInteractiveCliCommands.test.ts
@@ -36,6 +36,7 @@ describe('handleSlashCommand', () => {
getFolderTrustFeature: vi.fn().mockReturnValue(false),
getFolderTrust: vi.fn().mockReturnValue(false),
getProjectRoot: vi.fn().mockReturnValue('/test/project'),
+ getDisabledSlashCommands: vi.fn().mockReturnValue([]),
storage: {},
} as unknown as Config;
@@ -122,6 +123,68 @@ describe('handleSlashCommand', () => {
}
});
+ it('should return unsupported (not no_command) for a disabled command so it is not forwarded to the model', async () => {
+ const mockInitCommand = {
+ name: 'init',
+ description: 'Initialize project',
+ kind: CommandKind.BUILT_IN,
+ action: vi.fn(),
+ };
+ mockGetCommands.mockReturnValue([mockInitCommand]);
+ vi.mocked(mockConfig.getDisabledSlashCommands).mockReturnValue(['init']);
+
+ const result = await handleSlashCommand(
+ '/init',
+ abortController,
+ mockConfig,
+ mockSettings,
+ ['init'], // Would normally be allowed; denylist must still block it.
+ );
+
+ expect(result.type).toBe('unsupported');
+ if (result.type === 'unsupported') {
+ expect(result.reason).toContain('/init');
+ expect(result.reason).toContain('disabled');
+ }
+ expect(mockInitCommand.action).not.toHaveBeenCalled();
+ });
+
+ it('should match disabled names case-insensitively', async () => {
+ const mockInitCommand = {
+ name: 'init',
+ description: 'Initialize project',
+ kind: CommandKind.BUILT_IN,
+ action: vi.fn(),
+ };
+ mockGetCommands.mockReturnValue([mockInitCommand]);
+ vi.mocked(mockConfig.getDisabledSlashCommands).mockReturnValue(['INIT']);
+
+ const result = await handleSlashCommand(
+ '/init',
+ abortController,
+ mockConfig,
+ mockSettings,
+ ['init'],
+ );
+
+ expect(result.type).toBe('unsupported');
+ expect(mockInitCommand.action).not.toHaveBeenCalled();
+ });
+
+ it('should still return no_command for truly unknown slash commands even when a denylist is set', async () => {
+ mockGetCommands.mockReturnValue([]);
+ vi.mocked(mockConfig.getDisabledSlashCommands).mockReturnValue(['help']);
+
+ const result = await handleSlashCommand(
+ '/does-not-exist',
+ abortController,
+ mockConfig,
+ mockSettings,
+ );
+
+ expect(result.type).toBe('no_command');
+ });
+
it('should execute allowed built-in commands', async () => {
const mockInitCommand = {
name: 'init',
diff --git a/packages/cli/src/nonInteractiveCliCommands.ts b/packages/cli/src/nonInteractiveCliCommands.ts
index e6344f5d0..3ce8d155a 100644
--- a/packages/cli/src/nonInteractiveCliCommands.ts
+++ b/packages/cli/src/nonInteractiveCliCommands.ts
@@ -37,6 +37,8 @@ const debugLogger = createDebugLogger('NON_INTERACTIVE_COMMANDS');
* - init: Initialize project configuration
* - summary: Generate session summary
* - compress: Compress conversation history
+ * - context: Show context window usage (read-only diagnostic)
+ * - doctor: Run installation and environment diagnostics (read-only diagnostic)
*/
export const ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE = [
'init',
@@ -44,6 +46,8 @@ export const ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE = [
'compress',
'btw',
'bug',
+ 'context',
+ 'doctor',
] as const;
/**
@@ -250,8 +254,19 @@ export const handleSlashCommand = async (
: 'non_interactive';
const allowedBuiltinSet = new Set(allowedBuiltinCommandNames ?? []);
+ const disabledSlashCommandsRaw = config.getDisabledSlashCommands();
+ const disabledNameSet = new Set();
+ for (const name of disabledSlashCommandsRaw) {
+ const trimmed = name.trim();
+ if (trimmed) disabledNameSet.add(trimmed.toLowerCase());
+ }
+ const isDisabled = (cmd: { name: string }) =>
+ disabledNameSet.has(cmd.name.toLowerCase());
- // Load all commands to check if the command exists but is not allowed
+ // Load the full command set (unfiltered by the denylist) so that the
+ // fallback existence check below can distinguish a disabled command from a
+ // truly unknown one. Without this, a disabled command would fall through to
+ // `no_command` and be forwarded to the model as plain prompt text.
const allLoaders = [
new BuiltinCommandLoader(config),
new BundledSkillLoader(config),
@@ -266,7 +281,7 @@ export const handleSlashCommand = async (
const filteredCommands = filterCommandsForNonInteractive(
allCommands,
allowedBuiltinSet,
- );
+ ).filter((cmd) => !isDisabled(cmd));
// First, try to parse with filtered commands
const { commandToExecute, args } = parseSlashCommand(
@@ -282,6 +297,16 @@ export const handleSlashCommand = async (
);
if (knownCommand) {
+ if (isDisabled(knownCommand)) {
+ return {
+ type: 'unsupported',
+ reason: t(
+ 'The command "/{{command}}" is disabled by the current configuration.',
+ { command: knownCommand.name },
+ ),
+ originalType: 'filtered_command',
+ };
+ }
// Command exists but is not allowed in non-interactive mode
return {
type: 'unsupported',
@@ -376,7 +401,14 @@ export const getAvailableCommands = async (
]
: [new BundledSkillLoader(config), new FileCommandLoader(config)];
- const commandService = await CommandService.create(loaders, abortSignal);
+ const disabledSlashCommands = config.getDisabledSlashCommands();
+ const commandService = await CommandService.create(
+ loaders,
+ abortSignal,
+ disabledSlashCommands.length > 0
+ ? new Set(disabledSlashCommands)
+ : undefined,
+ );
const commands = commandService.getCommands();
const filteredCommands = filterCommandsForNonInteractive(
commands,
diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts
index 45ccf706f..c6146b90c 100644
--- a/packages/cli/src/services/BuiltinCommandLoader.ts
+++ b/packages/cli/src/services/BuiltinCommandLoader.ts
@@ -19,6 +19,7 @@ import { compressCommand } from '../ui/commands/compressCommand.js';
import { contextCommand } from '../ui/commands/contextCommand.js';
import { copyCommand } from '../ui/commands/copyCommand.js';
import { docsCommand } from '../ui/commands/docsCommand.js';
+import { doctorCommand } from '../ui/commands/doctorCommand.js';
import { directoryCommand } from '../ui/commands/directoryCommand.js';
import { editorCommand } from '../ui/commands/editorCommand.js';
import { exportCommand } from '../ui/commands/exportCommand.js';
@@ -82,6 +83,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
contextCommand,
copyCommand,
docsCommand,
+ doctorCommand,
directoryCommand,
editorCommand,
exportCommand,
diff --git a/packages/cli/src/services/CommandService.test.ts b/packages/cli/src/services/CommandService.test.ts
index 51f962753..122a741ff 100644
--- a/packages/cli/src/services/CommandService.test.ts
+++ b/packages/cli/src/services/CommandService.test.ts
@@ -310,6 +310,80 @@ describe('CommandService', () => {
expect(deployExtension?.description).toBe('[gcp] Deploy to Google Cloud');
});
+ describe('disabledNames filtering', () => {
+ it('should omit commands whose names are in the disabled set', async () => {
+ const loader = new MockCommandLoader([
+ mockCommandA,
+ mockCommandB,
+ mockCommandC,
+ ]);
+ const service = await CommandService.create(
+ [loader],
+ new AbortController().signal,
+ new Set(['command-b']),
+ );
+ const names = service.getCommands().map((cmd) => cmd.name);
+ expect(names).toEqual(expect.arrayContaining(['command-a', 'command-c']));
+ expect(names).not.toContain('command-b');
+ });
+
+ it('should match disabled names case-insensitively', async () => {
+ const loader = new MockCommandLoader([mockCommandA, mockCommandB]);
+ const service = await CommandService.create(
+ [loader],
+ new AbortController().signal,
+ new Set(['COMMAND-A']),
+ );
+ const names = service.getCommands().map((cmd) => cmd.name);
+ expect(names).toEqual(['command-b']);
+ });
+
+ it('should ignore empty entries and whitespace in the disabled set', async () => {
+ const loader = new MockCommandLoader([mockCommandA, mockCommandB]);
+ const service = await CommandService.create(
+ [loader],
+ new AbortController().signal,
+ new Set(['', ' ', ' command-a ']),
+ );
+ const names = service.getCommands().map((cmd) => cmd.name);
+ expect(names).toEqual(['command-b']);
+ });
+
+ it('should be a no-op when disabledNames is undefined or empty', async () => {
+ const loader = new MockCommandLoader([mockCommandA, mockCommandB]);
+ const undefinedResult = await CommandService.create(
+ [loader],
+ new AbortController().signal,
+ );
+ expect(undefinedResult.getCommands()).toHaveLength(2);
+
+ const emptyResult = await CommandService.create(
+ [new MockCommandLoader([mockCommandA, mockCommandB])],
+ new AbortController().signal,
+ new Set(),
+ );
+ expect(emptyResult.getCommands()).toHaveLength(2);
+ });
+
+ it('should disable extension commands by their renamed (final) name', async () => {
+ const builtin = createMockCommand('deploy', CommandKind.BUILT_IN);
+ const extension = {
+ ...createMockCommand('deploy', CommandKind.FILE),
+ extensionName: 'firebase',
+ description: '[firebase] Deploy to Firebase',
+ };
+ const loader = new MockCommandLoader([builtin, extension]);
+ const service = await CommandService.create(
+ [loader],
+ new AbortController().signal,
+ new Set(['firebase.deploy']),
+ );
+ const names = service.getCommands().map((cmd) => cmd.name);
+ // Built-in /deploy remains; the renamed extension command is gone.
+ expect(names).toEqual(['deploy']);
+ });
+ });
+
it('should handle multiple secondary conflicts with incrementing suffixes', async () => {
// User has /deploy, /gcp.deploy, and /gcp.deploy1
const userCommand1 = createMockCommand('deploy', CommandKind.FILE);
diff --git a/packages/cli/src/services/CommandService.ts b/packages/cli/src/services/CommandService.ts
index 41086dac1..832267c7b 100644
--- a/packages/cli/src/services/CommandService.ts
+++ b/packages/cli/src/services/CommandService.ts
@@ -33,8 +33,9 @@ export class CommandService {
*
* This factory method orchestrates the entire command loading process. It
* runs all provided loaders in parallel, aggregates their results, handles
- * name conflicts for extension commands by renaming them, and then returns a
- * fully constructed `CommandService` instance.
+ * name conflicts for extension commands by renaming them, optionally filters
+ * out disabled commands, and then returns a fully constructed
+ * `CommandService` instance.
*
* Conflict resolution:
* - Extension commands that conflict with existing commands are renamed to
@@ -45,11 +46,16 @@ export class CommandService {
* @param loaders An array of objects that conform to the `ICommandLoader`
* interface. Built-in commands should come first, followed by FileCommandLoader.
* @param signal An AbortSignal to cancel the loading process.
+ * @param disabledNames Optional set of command names to exclude. Matched
+ * case-insensitively against the final (post-rename) command name. Intended
+ * for settings- or flag-driven denylists that gate the CLI surface (see
+ * `slashCommands.disabled` and `--disabled-slash-commands`).
* @returns A promise that resolves to a new, fully initialized `CommandService` instance.
*/
static async create(
loaders: ICommandLoader[],
signal: AbortSignal,
+ disabledNames?: ReadonlySet,
): Promise {
const results = await Promise.allSettled(
loaders.map((loader) => loader.loadCommands(signal)),
@@ -88,6 +94,21 @@ export class CommandService {
});
}
+ if (disabledNames && disabledNames.size > 0) {
+ const normalizedDisabled = new Set();
+ for (const entry of disabledNames) {
+ const trimmed = entry.trim();
+ if (trimmed) normalizedDisabled.add(trimmed.toLowerCase());
+ }
+ if (normalizedDisabled.size > 0) {
+ for (const name of Array.from(commandMap.keys())) {
+ if (normalizedDisabled.has(name.toLowerCase())) {
+ commandMap.delete(name);
+ }
+ }
+ }
+ }
+
const finalCommands = Object.freeze(Array.from(commandMap.values()));
return new CommandService(finalCommands);
}
diff --git a/packages/cli/src/ui/commands/doctorCommand.test.ts b/packages/cli/src/ui/commands/doctorCommand.test.ts
new file mode 100644
index 000000000..2e3e79892
--- /dev/null
+++ b/packages/cli/src/ui/commands/doctorCommand.test.ts
@@ -0,0 +1,142 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { doctorCommand } from './doctorCommand.js';
+import { type CommandContext } from './types.js';
+import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
+import * as doctorChecksModule from '../../utils/doctorChecks.js';
+import type { DoctorCheckResult } from '../types.js';
+
+vi.mock('../../utils/doctorChecks.js');
+
+describe('doctorCommand', () => {
+ let mockContext: CommandContext;
+
+ const mockChecks: DoctorCheckResult[] = [
+ {
+ category: 'System',
+ name: 'Node.js version',
+ status: 'pass',
+ message: 'v20.0.0',
+ },
+ {
+ category: 'Authentication',
+ name: 'API key',
+ status: 'fail',
+ message: 'not configured',
+ detail: 'Run /auth to configure authentication.',
+ },
+ ];
+
+ beforeEach(() => {
+ mockContext = createMockCommandContext({
+ executionMode: 'interactive',
+ ui: {
+ addItem: vi.fn(),
+ setPendingItem: vi.fn(),
+ },
+ } as unknown as CommandContext);
+
+ vi.mocked(doctorChecksModule.runDoctorChecks).mockResolvedValue(mockChecks);
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should have the correct name and description', () => {
+ expect(doctorCommand.name).toBe('doctor');
+ expect(doctorCommand.description).toBe(
+ 'Run installation and environment diagnostics',
+ );
+ });
+
+ it('should show pending item and then add doctor item in interactive mode', async () => {
+ await doctorCommand.action!(mockContext, '');
+
+ expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith(
+ expect.objectContaining({ text: 'Running diagnostics...' }),
+ );
+ expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith(null);
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'doctor',
+ checks: mockChecks,
+ summary: { pass: 1, warn: 0, fail: 1 },
+ }),
+ expect.any(Number),
+ );
+ });
+
+ it('should return JSON message in non-interactive mode', async () => {
+ mockContext = createMockCommandContext({
+ executionMode: 'non_interactive',
+ ui: {
+ addItem: vi.fn(),
+ setPendingItem: vi.fn(),
+ },
+ } as unknown as CommandContext);
+
+ const result = await doctorCommand.action!(mockContext, '');
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ type: 'message',
+ messageType: 'error',
+ }),
+ );
+ expect(mockContext.ui.addItem).not.toHaveBeenCalled();
+ });
+
+ it('should return info messageType when no failures', async () => {
+ vi.mocked(doctorChecksModule.runDoctorChecks).mockResolvedValue([
+ {
+ category: 'System',
+ name: 'Node.js version',
+ status: 'pass',
+ message: 'v20.0.0',
+ },
+ ]);
+
+ mockContext = createMockCommandContext({
+ executionMode: 'non_interactive',
+ ui: {
+ addItem: vi.fn(),
+ setPendingItem: vi.fn(),
+ },
+ } as unknown as CommandContext);
+
+ const result = await doctorCommand.action!(mockContext, '');
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ type: 'message',
+ messageType: 'info',
+ }),
+ );
+ });
+
+ it('should not add item when aborted', async () => {
+ const abortController = new AbortController();
+ abortController.abort();
+
+ mockContext = createMockCommandContext({
+ executionMode: 'interactive',
+ abortSignal: abortController.signal,
+ ui: {
+ addItem: vi.fn(),
+ setPendingItem: vi.fn(),
+ },
+ } as unknown as CommandContext);
+
+ await doctorCommand.action!(mockContext, '');
+
+ expect(mockContext.ui.addItem).not.toHaveBeenCalled();
+ // setPendingItem(null) should still be called via finally
+ expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith(null);
+ });
+});
diff --git a/packages/cli/src/ui/commands/doctorCommand.ts b/packages/cli/src/ui/commands/doctorCommand.ts
new file mode 100644
index 000000000..d44d9ed5c
--- /dev/null
+++ b/packages/cli/src/ui/commands/doctorCommand.ts
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type { SlashCommand } from './types.js';
+import { CommandKind } from './types.js';
+import type { HistoryItemDoctor } from '../types.js';
+import { runDoctorChecks } from '../../utils/doctorChecks.js';
+import { t } from '../../i18n/index.js';
+
+export const doctorCommand: SlashCommand = {
+ name: 'doctor',
+ get description() {
+ return t('Run installation and environment diagnostics');
+ },
+ kind: CommandKind.BUILT_IN,
+ action: async (context) => {
+ const executionMode = context.executionMode ?? 'interactive';
+ const abortSignal = context.abortSignal;
+
+ if (executionMode === 'interactive') {
+ context.ui.setPendingItem({
+ type: 'info',
+ text: t('Running diagnostics...'),
+ });
+ }
+
+ try {
+ const checks = await runDoctorChecks(context);
+
+ if (abortSignal?.aborted) {
+ return;
+ }
+
+ const summary = {
+ pass: checks.filter((c) => c.status === 'pass').length,
+ warn: checks.filter((c) => c.status === 'warn').length,
+ fail: checks.filter((c) => c.status === 'fail').length,
+ };
+
+ if (executionMode === 'interactive') {
+ const doctorItem: Omit = {
+ type: 'doctor',
+ checks,
+ summary,
+ };
+ context.ui.addItem(doctorItem, Date.now());
+ return;
+ }
+
+ return {
+ type: 'message' as const,
+ messageType: (summary.fail > 0 ? 'error' : 'info') as 'error' | 'info',
+ content: JSON.stringify({ checks, summary }, null, 2),
+ };
+ } finally {
+ if (executionMode === 'interactive') {
+ context.ui.setPendingItem(null);
+ }
+ }
+ },
+};
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
index 80529e55d..9730dc090 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -40,6 +40,7 @@ import { SkillsList } from './views/SkillsList.js';
import { ToolsList } from './views/ToolsList.js';
import { McpStatus } from './views/McpStatus.js';
import { ContextUsage } from './views/ContextUsage.js';
+import { DoctorReport } from './views/DoctorReport.js';
import { ArenaAgentCard, ArenaSessionCard } from './arena/ArenaCards.js';
import { InsightProgressMessage } from './messages/InsightProgressMessage.js';
import { BtwMessage } from './messages/BtwMessage.js';
@@ -213,6 +214,13 @@ const HistoryItemDisplayComponent: React.FC = ({
showDetails={itemForDisplay.showDetails}
/>
)}
+ {itemForDisplay.type === 'doctor' && (
+
+ )}
{itemForDisplay.type === 'arena_agent_complete' && (
)}
diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx
index a2ade610b..d5b71811d 100644
--- a/packages/cli/src/ui/components/ThemeDialog.tsx
+++ b/packages/cli/src/ui/components/ThemeDialog.tsx
@@ -8,7 +8,11 @@ import type React from 'react';
import { useCallback, useState } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
-import { themeManager, DEFAULT_THEME } from '../themes/theme-manager.js';
+import {
+ themeManager,
+ DEFAULT_THEME,
+ AUTO_THEME_NAME,
+} from '../themes/theme-manager.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { DiffRenderer } from './messages/DiffRenderer.js';
import { colorizeCode } from '../utils/CodeColorizer.js';
@@ -42,10 +46,11 @@ export function ThemeDialog({
SettingScope.User,
);
- // Track the currently highlighted theme name
+ // Track the currently highlighted theme name. An unset theme means
+ // auto-detection is in effect, so reflect that by highlighting Auto.
const [highlightedThemeName, setHighlightedThemeName] = useState<
string | undefined
- >(settings.merged.ui?.theme || DEFAULT_THEME.name);
+ >(settings.merged.ui?.theme || AUTO_THEME_NAME);
// Generate theme items filtered by selected scope
const customThemes =
@@ -57,8 +62,15 @@ export function ThemeDialog({
.filter((theme) => theme.type !== 'custom');
const customThemeNames = Object.keys(customThemes);
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
- // Generate theme items
+ // Generate theme items with "Auto" at the top
const themeItems = [
+ {
+ label: t('Auto (detect terminal theme)'),
+ value: AUTO_THEME_NAME,
+ themeNameDisplay: t('Auto'),
+ themeTypeDisplay: t('Auto'),
+ key: AUTO_THEME_NAME,
+ },
...builtInThemes.map((theme) => ({
label: theme.name,
value: theme.name,
@@ -224,10 +236,13 @@ export function ThemeDialog({
{/* Get the Theme object for the highlighted theme, fall back to default if not found */}
{(() => {
+ // For 'auto', show the currently resolved theme (set by onHighlight → applyTheme)
const previewTheme =
- themeManager.getTheme(
- highlightedThemeName || DEFAULT_THEME.name,
- ) || DEFAULT_THEME;
+ highlightedThemeName === AUTO_THEME_NAME
+ ? themeManager.getActiveTheme()
+ : themeManager.getTheme(
+ highlightedThemeName || DEFAULT_THEME.name,
+ ) || DEFAULT_THEME;
return (
should render correctly in theme selection mode
│ │
│ > Select Theme Preview │
│ ▲ ┌─────────────────────────────────────────────────┐ │
-│ 1. Qwen Light Light │ │ │
-│ › 2. Qwen Dark Dark │ 1 # function │ │
-│ 3. ANSI Dark │ 2 def fibonacci(n): │ │
-│ 4. Atom One Dark │ 3 a, b = 0, 1 │ │
-│ 5. Ayu Dark │ 4 for _ in range(n): │ │
-│ 6. Default Dark │ 5 a, b = b, a + b │ │
-│ 7. Dracula Dark │ 6 return a │ │
-│ 8. GitHub Dark │ │ │
-│ 9. Shades Of Purple Dark │ 1 - print("Hello, " + name) │ │
-│ 10. ANSI Light Light │ 1 + print(f"Hello, {name}!") │ │
-│ 11. Ayu Light Light │ │ │
-│ 12. Default Light Light └─────────────────────────────────────────────────┘ │
+│ › 1. Auto Auto │ │ │
+│ 2. Qwen Light Light │ 1 # function │ │
+│ 3. Qwen Dark Dark │ 2 def fibonacci(n): │ │
+│ 4. ANSI Dark │ 3 a, b = 0, 1 │ │
+│ 5. Atom One Dark │ 4 for _ in range(n): │ │
+│ 6. Ayu Dark │ 5 a, b = b, a + b │ │
+│ 7. Default Dark │ 6 return a │ │
+│ 8. Dracula Dark │ │ │
+│ 9. GitHub Dark │ 1 - print("Hello, " + name) │ │
+│ 10. Shades Of Purple Dark │ 1 + print(f"Hello, {name}!") │ │
+│ 11. ANSI Light Light │ │ │
+│ 12. Ayu Light Light └─────────────────────────────────────────────────┘ │
│ ▼ │
│ │
│ (Use Enter to select, Tab to configure scope) │
diff --git a/packages/cli/src/ui/components/views/DoctorReport.tsx b/packages/cli/src/ui/components/views/DoctorReport.tsx
new file mode 100644
index 000000000..7102d73da
--- /dev/null
+++ b/packages/cli/src/ui/components/views/DoctorReport.tsx
@@ -0,0 +1,131 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type React from 'react';
+import { Box, Text } from 'ink';
+import { theme } from '../../semantic-colors.js';
+import type { DoctorCheckResult, DoctorCheckStatus } from '../../types.js';
+import { t } from '../../../i18n/index.js';
+
+interface DoctorReportProps {
+ checks: DoctorCheckResult[];
+ summary: { pass: number; warn: number; fail: number };
+ width?: number;
+}
+
+const STATUS_ICONS: Record = {
+ pass: '\u2713', // checkmark
+ warn: '\u26A0', // warning triangle
+ fail: '\u2717', // X mark
+};
+
+function getStatusColor(status: DoctorCheckStatus): string {
+ switch (status) {
+ case 'pass':
+ return theme.status.success;
+ case 'warn':
+ return theme.status.warning;
+ case 'fail':
+ return theme.status.error;
+ default:
+ return theme.text.primary;
+ }
+}
+
+/**
+ * Group checks by category, preserving insertion order.
+ */
+function groupByCategory(
+ checks: DoctorCheckResult[],
+): Map {
+ const groups = new Map();
+ for (const check of checks) {
+ const group = groups.get(check.category);
+ if (group) {
+ group.push(check);
+ } else {
+ groups.set(check.category, [check]);
+ }
+ }
+ return groups;
+}
+
+export const DoctorReport: React.FC = ({
+ checks,
+ summary,
+ width,
+}) => {
+ const groups = groupByCategory(checks);
+ const categoryEntries = Array.from(groups.entries());
+
+ // Compute the widest check name so the message column aligns consistently.
+ const nameColWidth = Math.max(20, ...checks.map((c) => c.name.length + 2));
+
+ return (
+
+
+ {t('Doctor Report')}
+
+
+
+ {categoryEntries.map(([category, items], groupIdx) => (
+ 0 ? 1 : 0}
+ >
+
+ {category}
+
+ {items.map((check) => (
+
+
+
+ {' '}
+ {STATUS_ICONS[check.status]}{' '}
+
+
+ {check.name}
+
+ {check.message}
+
+ {check.detail && (
+
+
+ {'-> '}
+ {check.detail}
+
+
+ )}
+
+ ))}
+
+ ))}
+
+
+ {'-- '}
+
+ {summary.pass} {t('passed')}
+
+ {', '}
+
+ {summary.warn} {t('warnings')}
+
+ {', '}
+
+ {summary.fail} {t('failures')}
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
index c2283af03..360c0e709 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
@@ -337,17 +337,26 @@ export const useSlashCommandProcessor = (
useEffect(() => {
const controller = new AbortController();
const load = async () => {
- const loaders = [
- new McpPromptLoader(config),
- new BuiltinCommandLoader(config),
- new BundledSkillLoader(config),
- new FileCommandLoader(config),
- ];
- const commandService = await CommandService.create(
- loaders,
- controller.signal,
- );
- setCommands(commandService.getCommands());
+ try {
+ const loaders = [
+ new McpPromptLoader(config),
+ new BuiltinCommandLoader(config),
+ new BundledSkillLoader(config),
+ new FileCommandLoader(config),
+ ];
+ const disabled = config?.getDisabledSlashCommands() ?? [];
+ const commandService = await CommandService.create(
+ loaders,
+ controller.signal,
+ disabled.length > 0 ? new Set(disabled) : undefined,
+ );
+ // Avoid overwriting newer results from a subsequent effect run
+ if (!controller.signal.aborted) {
+ setCommands(commandService.getCommands());
+ }
+ } catch (error) {
+ debugLogger.error('Failed to load slash commands:', error);
+ }
};
load();
diff --git a/packages/cli/src/ui/hooks/useThemeCommand.ts b/packages/cli/src/ui/hooks/useThemeCommand.ts
index 467ef313e..8034f2fc4 100644
--- a/packages/cli/src/ui/hooks/useThemeCommand.ts
+++ b/packages/cli/src/ui/hooks/useThemeCommand.ts
@@ -5,7 +5,7 @@
*/
import { useState, useCallback } from 'react';
-import { themeManager } from '../themes/theme-manager.js';
+import { themeManager, AUTO_THEME_NAME } from '../themes/theme-manager.js';
import type { LoadedSettings, SettingScope } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting
import { type HistoryItem, MessageType } from '../types.js';
import process from 'node:process';
@@ -78,10 +78,11 @@ export const useThemeCommand = (
...(loadedSettings.user.settings.ui?.customThemes || {}),
...(loadedSettings.workspace.settings.ui?.customThemes || {}),
};
- // Only allow selecting themes available in the merged custom themes or built-in themes
+ // Only allow selecting themes available in the merged custom themes, built-in themes, or 'auto'
+ const isAuto = themeName === AUTO_THEME_NAME;
const isBuiltIn = themeManager.findThemeByName(themeName);
const isCustom = themeName && mergedCustomThemes[themeName];
- if (!isBuiltIn && !isCustom) {
+ if (!isAuto && !isBuiltIn && !isCustom) {
setThemeError(
t('Theme "{{themeName}}" not found in selected scope.', {
themeName: themeName ?? '',
diff --git a/packages/cli/src/ui/themes/detect-terminal-theme.test.ts b/packages/cli/src/ui/themes/detect-terminal-theme.test.ts
new file mode 100644
index 000000000..7e84be354
--- /dev/null
+++ b/packages/cli/src/ui/themes/detect-terminal-theme.test.ts
@@ -0,0 +1,369 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import * as childProcess from 'node:child_process';
+
+vi.mock('node:child_process');
+
+describe('detectTerminalTheme', () => {
+ const originalPlatform = process.platform;
+ const originalEnv = { ...process.env };
+
+ beforeEach(() => {
+ vi.resetModules();
+ vi.restoreAllMocks();
+ process.env = { ...originalEnv };
+ delete process.env['COLORFGBG'];
+ });
+
+ afterEach(() => {
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
+ process.env = originalEnv;
+ });
+
+ // ---------------------------------------------------------------------------
+ // parseOscRgb + themeFromOscColor (pure, synchronous)
+ // ---------------------------------------------------------------------------
+
+ describe('parseOscRgb', () => {
+ it('should parse rgb:RRRR/GGGG/BBBB format', async () => {
+ const { parseOscRgb } = await import('./detect-terminal-theme.js');
+ const rgb = parseOscRgb('rgb:0000/0000/0000');
+ expect(rgb).toEqual({ r: 0, g: 0, b: 0 });
+ });
+
+ it('should parse short hex components (rgb:RR/GG/BB)', async () => {
+ const { parseOscRgb } = await import('./detect-terminal-theme.js');
+ const rgb = parseOscRgb('rgb:ff/ff/ff');
+ expect(rgb).toEqual({ r: 1, g: 1, b: 1 });
+ });
+
+ it('should parse #RRGGBB format', async () => {
+ const { parseOscRgb } = await import('./detect-terminal-theme.js');
+ const rgb = parseOscRgb('#000000');
+ expect(rgb).toEqual({ r: 0, g: 0, b: 0 });
+ });
+
+ it('should parse #RRRRGGGGBBBB format', async () => {
+ const { parseOscRgb } = await import('./detect-terminal-theme.js');
+ const rgb = parseOscRgb('#ffffffffffff');
+ expect(rgb).toEqual({ r: 1, g: 1, b: 1 });
+ });
+
+ it('should return undefined for invalid data', async () => {
+ const { parseOscRgb } = await import('./detect-terminal-theme.js');
+ expect(parseOscRgb('garbage')).toBeUndefined();
+ expect(parseOscRgb('')).toBeUndefined();
+ });
+ });
+
+ describe('themeFromOscColor', () => {
+ it('should return "dark" for a dark background', async () => {
+ const { themeFromOscColor } = await import('./detect-terminal-theme.js');
+ // Pure black background
+ expect(themeFromOscColor('rgb:0000/0000/0000')).toBe('dark');
+ // Typical dark terminal (e.g., #1e1e2e)
+ expect(themeFromOscColor('rgb:1e1e/1e1e/2e2e')).toBe('dark');
+ });
+
+ it('should return "light" for a light background', async () => {
+ const { themeFromOscColor } = await import('./detect-terminal-theme.js');
+ // Pure white background
+ expect(themeFromOscColor('rgb:ffff/ffff/ffff')).toBe('light');
+ // Typical light terminal (e.g., #fafafa)
+ expect(themeFromOscColor('rgb:fafa/fafa/fafa')).toBe('light');
+ });
+
+ it('should return undefined for unparseable data', async () => {
+ const { themeFromOscColor } = await import('./detect-terminal-theme.js');
+ expect(themeFromOscColor('not-a-color')).toBeUndefined();
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // detectOsc11Theme (async, TTY interaction)
+ // ---------------------------------------------------------------------------
+
+ describe('detectOsc11Theme', () => {
+ const forceTTY = () => {
+ const origStdinTTY = process.stdin.isTTY;
+ const origStdoutTTY = process.stdout.isTTY;
+ Object.defineProperty(process.stdin, 'isTTY', {
+ value: true,
+ configurable: true,
+ });
+ Object.defineProperty(process.stdout, 'isTTY', {
+ value: true,
+ configurable: true,
+ });
+ return () => {
+ Object.defineProperty(process.stdin, 'isTTY', {
+ value: origStdinTTY,
+ configurable: true,
+ });
+ Object.defineProperty(process.stdout, 'isTTY', {
+ value: origStdoutTTY,
+ configurable: true,
+ });
+ };
+ };
+
+ it('should return undefined when stdin is not a TTY', async () => {
+ const origIsTTY = process.stdin.isTTY;
+ Object.defineProperty(process.stdin, 'isTTY', {
+ value: false,
+ configurable: true,
+ });
+
+ const { detectOsc11Theme } = await import('./detect-terminal-theme.js');
+ const result = await detectOsc11Theme();
+ expect(result).toBeUndefined();
+
+ Object.defineProperty(process.stdin, 'isTTY', {
+ value: origIsTTY,
+ configurable: true,
+ });
+ });
+
+ it('should resolve "dark" when terminal reports a dark background', async () => {
+ const restoreTTY = forceTTY();
+ const writeSpy = vi
+ .spyOn(process.stdout, 'write')
+ .mockImplementation(() => true);
+ const baseline = process.stdin.listenerCount('data');
+
+ try {
+ const { detectOsc11Theme } = await import('./detect-terminal-theme.js');
+ const promise = detectOsc11Theme();
+ // Listener must be attached synchronously so the response is captured.
+ expect(process.stdin.listenerCount('data')).toBe(baseline + 1);
+ expect(writeSpy).toHaveBeenCalledWith('\x1b]11;?\x07');
+
+ process.stdin.emit(
+ 'data',
+ Buffer.from('\x1b]11;rgb:0000/0000/0000\x07'),
+ );
+
+ await expect(promise).resolves.toBe('dark');
+ // Regression guard: listener must be removed on every exit path.
+ expect(process.stdin.listenerCount('data')).toBe(baseline);
+ } finally {
+ restoreTTY();
+ }
+ });
+
+ it('should resolve undefined on timeout and remove its data listener', async () => {
+ vi.useFakeTimers();
+ const restoreTTY = forceTTY();
+ vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
+ const baseline = process.stdin.listenerCount('data');
+
+ try {
+ const { detectOsc11Theme } = await import('./detect-terminal-theme.js');
+ const promise = detectOsc11Theme();
+ expect(process.stdin.listenerCount('data')).toBe(baseline + 1);
+
+ await vi.advanceTimersByTimeAsync(250);
+
+ await expect(promise).resolves.toBeUndefined();
+ // Regression guard: the listener-leak that motivated earlier fixes
+ // in this PR (OSC 11 bytes bleeding into the input box) only
+ // happens when the timeout path forgets to detach.
+ expect(process.stdin.listenerCount('data')).toBe(baseline);
+ } finally {
+ restoreTTY();
+ vi.useRealTimers();
+ }
+ });
+
+ it('should reassemble OSC 11 responses split across multiple data events', async () => {
+ const restoreTTY = forceTTY();
+ vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
+
+ try {
+ const { detectOsc11Theme } = await import('./detect-terminal-theme.js');
+ const promise = detectOsc11Theme();
+ // Split a pure-white response across two chunks.
+ process.stdin.emit('data', Buffer.from('\x1b]11;rgb:ffff/'));
+ process.stdin.emit('data', Buffer.from('ffff/ffff\x07'));
+
+ await expect(promise).resolves.toBe('light');
+ } finally {
+ restoreTTY();
+ }
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // detectMacOSTheme (sync)
+ // ---------------------------------------------------------------------------
+
+ describe('detectMacOSTheme', () => {
+ it('should return "dark" when macOS dark mode is active', async () => {
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
+ vi.mocked(childProcess.execSync).mockReturnValue('Dark\n');
+
+ const { detectMacOSTheme } = await import('./detect-terminal-theme.js');
+ expect(detectMacOSTheme()).toBe('dark');
+ });
+
+ it('should return "light" when macOS light mode is active', async () => {
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
+ vi.mocked(childProcess.execSync).mockImplementation(() => {
+ throw new Error('The domain/default pair does not exist');
+ });
+
+ const { detectMacOSTheme } = await import('./detect-terminal-theme.js');
+ expect(detectMacOSTheme()).toBe('light');
+ });
+
+ it('should return "light" when the "does not exist" message is on stderr only', async () => {
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
+ vi.mocked(childProcess.execSync).mockImplementation(() => {
+ const err = new Error('Command failed') as Error & {
+ stderr?: string;
+ };
+ err.stderr =
+ 'The domain/default pair of (kCFPreferencesAnyApplication, AppleInterfaceStyle) does not exist\n';
+ throw err;
+ });
+
+ const { detectMacOSTheme } = await import('./detect-terminal-theme.js');
+ expect(detectMacOSTheme()).toBe('light');
+ });
+
+ it('should return undefined on timeout (do not assume Light Mode)', async () => {
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
+ vi.mocked(childProcess.execSync).mockImplementation(() => {
+ throw new Error('Command failed: defaults read -g AppleInterfaceStyle');
+ });
+
+ const { detectMacOSTheme } = await import('./detect-terminal-theme.js');
+ expect(detectMacOSTheme()).toBeUndefined();
+ });
+
+ it('should return undefined when `defaults` is not on PATH', async () => {
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
+ vi.mocked(childProcess.execSync).mockImplementation(() => {
+ const err = new Error('spawnSync defaults ENOENT') as Error & {
+ code?: string;
+ };
+ err.code = 'ENOENT';
+ throw err;
+ });
+
+ const { detectMacOSTheme } = await import('./detect-terminal-theme.js');
+ expect(detectMacOSTheme()).toBeUndefined();
+ });
+
+ it('should return undefined on non-macOS platforms', async () => {
+ Object.defineProperty(process, 'platform', { value: 'linux' });
+
+ const { detectMacOSTheme } = await import('./detect-terminal-theme.js');
+ expect(detectMacOSTheme()).toBeUndefined();
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // detectFromColorFgBg (sync)
+ // ---------------------------------------------------------------------------
+
+ describe('detectFromColorFgBg', () => {
+ it('should return "dark" when background is dark (COLORFGBG=15;0)', async () => {
+ process.env['COLORFGBG'] = '15;0';
+ const { detectFromColorFgBg } =
+ await import('./detect-terminal-theme.js');
+ expect(detectFromColorFgBg()).toBe('dark');
+ });
+
+ it('should return "light" when background is light (COLORFGBG=0;15)', async () => {
+ process.env['COLORFGBG'] = '0;15';
+ const { detectFromColorFgBg } =
+ await import('./detect-terminal-theme.js');
+ expect(detectFromColorFgBg()).toBe('light');
+ });
+
+ it('should return "light" when background is 7 (light gray)', async () => {
+ process.env['COLORFGBG'] = '0;7';
+ const { detectFromColorFgBg } =
+ await import('./detect-terminal-theme.js');
+ expect(detectFromColorFgBg()).toBe('light');
+ });
+
+ it('should return "dark" when background is 8 (dark gray)', async () => {
+ process.env['COLORFGBG'] = '15;8';
+ const { detectFromColorFgBg } =
+ await import('./detect-terminal-theme.js');
+ expect(detectFromColorFgBg()).toBe('dark');
+ });
+
+ it('should handle three-part format (fg;extra;bg)', async () => {
+ process.env['COLORFGBG'] = '15;0;0';
+ const { detectFromColorFgBg } =
+ await import('./detect-terminal-theme.js');
+ expect(detectFromColorFgBg()).toBe('dark');
+ });
+
+ it('should return undefined when COLORFGBG is not set', async () => {
+ delete process.env['COLORFGBG'];
+ const { detectFromColorFgBg } =
+ await import('./detect-terminal-theme.js');
+ expect(detectFromColorFgBg()).toBeUndefined();
+ });
+
+ it('should return undefined when COLORFGBG has invalid value', async () => {
+ process.env['COLORFGBG'] = 'invalid';
+ const { detectFromColorFgBg } =
+ await import('./detect-terminal-theme.js');
+ expect(detectFromColorFgBg()).toBeUndefined();
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // detectTerminalTheme (sync entry point)
+ // ---------------------------------------------------------------------------
+
+ describe('detectTerminalTheme (sync)', () => {
+ it('should prefer COLORFGBG over macOS detection', async () => {
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
+ vi.mocked(childProcess.execSync).mockReturnValue('Dark\n');
+ process.env['COLORFGBG'] = '0;15';
+
+ const { detectTerminalTheme } =
+ await import('./detect-terminal-theme.js');
+ expect(detectTerminalTheme()).toBe('light');
+ });
+
+ it('should fall back to macOS when COLORFGBG is not set', async () => {
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
+ vi.mocked(childProcess.execSync).mockReturnValue('Dark\n');
+ delete process.env['COLORFGBG'];
+
+ const { detectTerminalTheme } =
+ await import('./detect-terminal-theme.js');
+ expect(detectTerminalTheme()).toBe('dark');
+ });
+
+ it('should fall back to COLORFGBG on non-macOS', async () => {
+ Object.defineProperty(process, 'platform', { value: 'linux' });
+ process.env['COLORFGBG'] = '0;15';
+
+ const { detectTerminalTheme } =
+ await import('./detect-terminal-theme.js');
+ expect(detectTerminalTheme()).toBe('light');
+ });
+
+ it('should default to dark when no detection method works', async () => {
+ Object.defineProperty(process, 'platform', { value: 'linux' });
+ delete process.env['COLORFGBG'];
+
+ const { detectTerminalTheme } =
+ await import('./detect-terminal-theme.js');
+ expect(detectTerminalTheme()).toBe('dark');
+ });
+ });
+});
diff --git a/packages/cli/src/ui/themes/detect-terminal-theme.ts b/packages/cli/src/ui/themes/detect-terminal-theme.ts
new file mode 100644
index 000000000..eee30ae1b
--- /dev/null
+++ b/packages/cli/src/ui/themes/detect-terminal-theme.ts
@@ -0,0 +1,274 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { execSync } from 'node:child_process';
+import process from 'node:process';
+import { createDebugLogger } from '@qwen-code/qwen-code-core';
+
+const debugLogger = createDebugLogger('THEME_DETECT');
+
+export type DetectedTheme = 'dark' | 'light';
+
+// ---------------------------------------------------------------------------
+// OSC 11 – query terminal background color
+// ---------------------------------------------------------------------------
+
+/** Timeout (ms) for the OSC 11 query. */
+const OSC11_TIMEOUT_MS = 200;
+
+interface Rgb {
+ r: number;
+ g: number;
+ b: number;
+}
+
+/**
+ * Normalises a variable-length hex colour component (1–4 hex digits) to
+ * the [0, 1] range. For example "ff" → 1, "8000" → 0.5 (≈ 32768/65535).
+ */
+function hexComponent(hex: string): number {
+ const max = 16 ** hex.length - 1; // 1-digit → 15, 4-digit → 65535
+ return parseInt(hex, 16) / max;
+}
+
+/**
+ * Parses an XParseColor RGB string returned by OSC 11.
+ *
+ * Accepted formats:
+ * - `rgb:RRRR/GGGG/BBBB` (1–4 hex digits per component)
+ * - `#RRGGBB` or `#RRRRGGGGBBBB` (equal-length triplets)
+ */
+export function parseOscRgb(data: string): Rgb | undefined {
+ // rgb:R/G/B
+ const rgbMatch =
+ /^rgba?:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})/i.exec(data);
+ if (rgbMatch) {
+ return {
+ r: hexComponent(rgbMatch[1]!),
+ g: hexComponent(rgbMatch[2]!),
+ b: hexComponent(rgbMatch[3]!),
+ };
+ }
+
+ // #RRGGBB or #RRRRGGGGBBBB
+ const hashMatch = /^#([0-9a-f]+)$/i.exec(data);
+ if (hashMatch && hashMatch[1]!.length % 3 === 0) {
+ const hex = hashMatch[1]!;
+ const n = hex.length / 3;
+ return {
+ r: hexComponent(hex.slice(0, n)),
+ g: hexComponent(hex.slice(n, 2 * n)),
+ b: hexComponent(hex.slice(2 * n)),
+ };
+ }
+
+ return undefined;
+}
+
+/**
+ * Converts an OSC 11 colour response into a dark/light theme decision
+ * using ITU-R BT.709 relative luminance.
+ */
+export function themeFromOscColor(data: string): DetectedTheme | undefined {
+ const rgb = parseOscRgb(data);
+ if (!rgb) return undefined;
+ const luminance = 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
+ return luminance > 0.5 ? 'light' : 'dark';
+}
+
+/**
+ * Sends an OSC 11 query (`ESC ] 11 ; ? BEL`) to the terminal and waits
+ * for the response containing the background colour.
+ *
+ * The caller is responsible for having stdin in raw mode with an active
+ * consumer (so the stream is in flowing mode). This probe only attaches
+ * an extra listener to parse the OSC 11 response — it does NOT flip raw
+ * mode or resume/pause stdin, because doing so interleaves with other
+ * early-startup stdin consumers (kitty protocol detection, early input
+ * capture) and causes terminal response bytes to leak into the TUI.
+ *
+ * Returns `undefined` when stdin/stdout is not a TTY or when no response
+ * arrives within {@link OSC11_TIMEOUT_MS}.
+ */
+export function detectOsc11Theme(): Promise {
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
+ return Promise.resolve(undefined);
+ }
+
+ return new Promise((resolve) => {
+ const stdin = process.stdin;
+ let resolved = false;
+ let buffer = '';
+
+ const finish = (result: DetectedTheme | undefined) => {
+ if (resolved) return;
+ resolved = true;
+ clearTimeout(timer);
+ stdin.removeListener('data', onData);
+ resolve(result);
+ };
+
+ const timer = setTimeout(() => finish(undefined), OSC11_TIMEOUT_MS);
+
+ const onData = (data: Buffer) => {
+ buffer += data.toString();
+ // OSC response: ESC ] 11 ; BEL or ESC ] 11 ; ST
+ // eslint-disable-next-line no-control-regex
+ const match = /\x1b\]11;(.*?)(?:\x07|\x1b\\)/.exec(buffer);
+ if (match) {
+ finish(themeFromOscColor(match[1]!));
+ }
+ };
+
+ stdin.on('data', onData);
+ process.stdout.write('\x1b]11;?\x07');
+ });
+}
+
+// ---------------------------------------------------------------------------
+// Synchronous detection helpers
+// ---------------------------------------------------------------------------
+
+/**
+ * Detects the macOS system appearance using `defaults read -g AppleInterfaceStyle`.
+ * Returns 'dark' if Dark Mode is active, 'light' when `defaults` reports the key
+ * is missing (the canonical macOS Light Mode signal), and undefined for any
+ * other failure (timeout, `defaults` not on PATH, killed by signal, …) so the
+ * caller can continue its fallback chain instead of pinning to Light.
+ * Returns undefined on non-macOS platforms.
+ */
+export function detectMacOSTheme(): DetectedTheme | undefined {
+ if (process.platform !== 'darwin') {
+ return undefined;
+ }
+
+ try {
+ const result = execSync('defaults read -g AppleInterfaceStyle', {
+ encoding: 'utf-8',
+ timeout: 3000,
+ stdio: ['pipe', 'pipe', 'pipe'],
+ }).trim();
+
+ return result.toLowerCase() === 'dark' ? 'dark' : 'light';
+ } catch (error) {
+ const err = error as { stderr?: string | Buffer; message?: string };
+ const stderr =
+ typeof err.stderr === 'string'
+ ? err.stderr
+ : (err.stderr?.toString?.() ?? '');
+ const message = err.message ?? '';
+ // Only the explicit "… does not exist" error confirms Light Mode. Any
+ // other failure is inconclusive — returning undefined lets the caller
+ // fall through to the next detection layer (or the default-dark).
+ if (/does not exist/i.test(stderr) || /does not exist/i.test(message)) {
+ return 'light';
+ }
+ return undefined;
+ }
+}
+
+/**
+ * Detects theme from the COLORFGBG environment variable.
+ *
+ * COLORFGBG is set by some terminals (e.g., rxvt, xterm, iTerm2, Konsole)
+ * in the format "foreground;background" where values are ANSI color indices (0-15).
+ *
+ * A dark background (0-6, 8) → dark theme.
+ * A light background (7, 9-15) → light theme.
+ */
+export function detectFromColorFgBg(): DetectedTheme | undefined {
+ const colorFgBg = process.env['COLORFGBG'];
+ if (!colorFgBg) {
+ return undefined;
+ }
+
+ const parts = colorFgBg.split(';');
+ const bgStr = parts[parts.length - 1];
+ if (bgStr === undefined) {
+ return undefined;
+ }
+
+ const bg = parseInt(bgStr, 10);
+ if (isNaN(bg)) {
+ return undefined;
+ }
+
+ if (bg === 7 || (bg >= 9 && bg <= 15)) {
+ return 'light';
+ }
+
+ return 'dark';
+}
+
+// ---------------------------------------------------------------------------
+// Public entry points
+// ---------------------------------------------------------------------------
+
+/**
+ * Synchronous theme detection (for theme dialog live-preview).
+ *
+ * Order: COLORFGBG → macOS system appearance → default dark.
+ */
+export function detectTerminalTheme(): DetectedTheme {
+ const colorFgBgResult = detectFromColorFgBg();
+ if (colorFgBgResult) {
+ debugLogger.info(`Detected theme from COLORFGBG: ${colorFgBgResult}`);
+ return colorFgBgResult;
+ }
+
+ const macResult = detectMacOSTheme();
+ if (macResult) {
+ debugLogger.info(
+ `Detected theme from macOS system appearance: ${macResult}`,
+ );
+ return macResult;
+ }
+
+ debugLogger.info('Could not detect terminal theme, defaulting to dark');
+ return 'dark';
+}
+
+/**
+ * Asynchronous theme detection (for startup).
+ *
+ * Checks cheap synchronous sources first (COLORFGBG) so we never pay the
+ * ~200 ms OSC 11 timeout when a fast answer is already available. OSC 11 is
+ * tried only when no synchronous source provides an answer.
+ *
+ * Order: COLORFGBG → OSC 11 → macOS system appearance → default dark.
+ */
+export async function detectTerminalThemeAsync(): Promise {
+ // Fast path: COLORFGBG is instant and terminal-specific.
+ const colorFgBgResult = detectFromColorFgBg();
+ if (colorFgBgResult) {
+ debugLogger.info(
+ `Detected theme from COLORFGBG (async path): ${colorFgBgResult}`,
+ );
+ return colorFgBgResult;
+ }
+
+ // OSC 11 directly reads the terminal background colour. It is the most
+ // universal method but requires a TTY and may block up to OSC11_TIMEOUT_MS.
+ const osc11Result = await detectOsc11Theme();
+ if (osc11Result) {
+ debugLogger.info(
+ `Detected theme from OSC 11 background query: ${osc11Result}`,
+ );
+ return osc11Result;
+ }
+
+ // Remaining synchronous fallbacks (macOS → default dark).
+ const macResult = detectMacOSTheme();
+ if (macResult) {
+ debugLogger.info(
+ `Detected theme from macOS system appearance: ${macResult}`,
+ );
+ return macResult;
+ }
+
+ debugLogger.info('Could not detect terminal theme, defaulting to dark');
+ return 'dark';
+}
diff --git a/packages/cli/src/ui/themes/theme-manager.test.ts b/packages/cli/src/ui/themes/theme-manager.test.ts
index 75c6b761d..df9a59613 100644
--- a/packages/cli/src/ui/themes/theme-manager.test.ts
+++ b/packages/cli/src/ui/themes/theme-manager.test.ts
@@ -10,11 +10,16 @@ if (process.env['NO_COLOR'] !== undefined) {
}
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
-import { themeManager, DEFAULT_THEME } from './theme-manager.js';
+import {
+ themeManager,
+ DEFAULT_THEME,
+ AUTO_THEME_NAME,
+} from './theme-manager.js';
import type { CustomTheme } from './theme.js';
import * as fs from 'node:fs';
import * as os from 'node:os';
import type * as osActual from 'node:os';
+import * as detectModule from './detect-terminal-theme.js';
vi.mock('node:fs');
vi.mock('node:os', async (importOriginal) => {
@@ -25,6 +30,10 @@ vi.mock('node:os', async (importOriginal) => {
platform: vi.fn(() => 'linux'),
};
});
+vi.mock('./detect-terminal-theme.js', () => ({
+ detectTerminalTheme: vi.fn(() => 'dark'),
+ detectTerminalThemeAsync: vi.fn(async () => 'dark'),
+}));
const validCustomTheme: CustomTheme = {
type: 'custom',
@@ -46,9 +55,14 @@ const validCustomTheme: CustomTheme = {
describe('ThemeManager', () => {
beforeEach(() => {
- // Reset themeManager state
+ // Reset themeManager state. themeManager is a module-level singleton,
+ // so the cached async auto-detection result would otherwise leak across
+ // tests and make ordering load-bearing.
themeManager.loadCustomThemes({});
themeManager.setActiveTheme(DEFAULT_THEME.name);
+ (
+ themeManager as unknown as { cachedAutoDetection: unknown }
+ ).cachedAutoDetection = undefined;
});
afterEach(() => {
@@ -114,6 +128,63 @@ describe('ThemeManager', () => {
}
});
+ describe('auto theme detection', () => {
+ it('should select Qwen Dark when terminal is detected as dark', () => {
+ vi.mocked(detectModule.detectTerminalTheme).mockReturnValue('dark');
+ const result = themeManager.setActiveTheme(AUTO_THEME_NAME);
+ expect(result).toBe(true);
+ expect(themeManager.getActiveTheme().name).toBe('Qwen Dark');
+ });
+
+ it('should select Qwen Light when terminal is detected as light', () => {
+ vi.mocked(detectModule.detectTerminalTheme).mockReturnValue('light');
+ const result = themeManager.setActiveTheme(AUTO_THEME_NAME);
+ expect(result).toBe(true);
+ expect(themeManager.getActiveTheme().name).toBe('Qwen Light');
+ });
+
+ it('should always return true for auto theme', () => {
+ expect(themeManager.setActiveTheme(AUTO_THEME_NAME)).toBe(true);
+ });
+
+ it('should resolve async auto theme with Qwen Light for light', async () => {
+ vi.mocked(detectModule.detectTerminalThemeAsync).mockResolvedValue(
+ 'light',
+ );
+ await themeManager.resolveAutoThemeAsync();
+ expect(themeManager.getActiveTheme().name).toBe('Qwen Light');
+ });
+
+ it('should resolve async auto theme with Qwen Dark for dark', async () => {
+ vi.mocked(detectModule.detectTerminalThemeAsync).mockResolvedValue(
+ 'dark',
+ );
+ await themeManager.resolveAutoThemeAsync();
+ expect(themeManager.getActiveTheme().name).toBe('Qwen Dark');
+ });
+
+ it('should reuse the async-detected value when auto is re-selected', async () => {
+ // Startup: async probe (e.g. OSC 11) reports light.
+ vi.mocked(detectModule.detectTerminalThemeAsync).mockResolvedValue(
+ 'light',
+ );
+ await themeManager.resolveAutoThemeAsync();
+ expect(themeManager.getActiveTheme().name).toBe('Qwen Light');
+
+ // User switches to another theme via /theme.
+ themeManager.setActiveTheme('Ayu');
+ expect(themeManager.getActiveTheme().name).toBe('Ayu');
+
+ // Switching back to Auto must not regress: even if the sync detector
+ // disagrees (OSC 11 is unavailable in-session), the cached async
+ // result wins so the preview stays consistent with startup.
+ vi.mocked(detectModule.detectTerminalTheme).mockReturnValue('dark');
+ themeManager.setActiveTheme(AUTO_THEME_NAME);
+ expect(themeManager.getActiveTheme().name).toBe('Qwen Light');
+ expect(detectModule.detectTerminalTheme).not.toHaveBeenCalled();
+ });
+ });
+
describe('when loading a theme from a file', () => {
const mockThemePath = './my-theme.json';
const mockTheme: CustomTheme = {
diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts
index e4d8c3dfa..e8806dba3 100644
--- a/packages/cli/src/ui/themes/theme-manager.ts
+++ b/packages/cli/src/ui/themes/theme-manager.ts
@@ -28,6 +28,10 @@ import { ANSILight } from './ansi-light.js';
import { NoColorTheme } from './no-color.js';
import process from 'node:process';
import { createDebugLogger } from '@qwen-code/qwen-code-core';
+import {
+ detectTerminalTheme,
+ detectTerminalThemeAsync,
+} from './detect-terminal-theme.js';
const debugLogger = createDebugLogger('THEME_MANAGER');
@@ -38,6 +42,7 @@ export interface ThemeDisplay {
}
export const DEFAULT_THEME: Theme = QwenDark;
+export const AUTO_THEME_NAME = 'auto';
class ThemeManager {
private readonly availableThemes: Theme[];
@@ -114,9 +119,16 @@ class ThemeManager {
/**
* Sets the active theme.
* @param themeName The name of the theme to set as active.
+ * If themeName is 'auto', detects the terminal theme and selects
+ * Qwen Dark or Qwen Light accordingly.
* @returns True if the theme was successfully set, false otherwise.
*/
setActiveTheme(themeName: string | undefined): boolean {
+ if (themeName === AUTO_THEME_NAME) {
+ this.activeTheme = this.resolveAutoTheme();
+ debugLogger.info(`Auto-detected theme: ${this.activeTheme.name}`);
+ return true;
+ }
const theme = this.findThemeByName(themeName);
if (!theme) {
return false;
@@ -125,6 +137,39 @@ class ThemeManager {
return true;
}
+ /**
+ * Cached auto-detection result. Populated by the async probe at startup
+ * (which includes OSC 11) and reused by subsequent sync resolutions so
+ * reselecting Auto in the /theme dialog never contradicts what was shown
+ * when the app first rendered.
+ */
+ private cachedAutoDetection: 'dark' | 'light' | undefined;
+
+ /**
+ * Detects the terminal's dark/light preference (synchronous) and returns
+ * the corresponding Qwen theme.
+ * Used by the theme dialog for instant preview. Prefers the cached
+ * async-detected value when available so we stay consistent with the
+ * OSC 11 probe performed at startup.
+ */
+ private resolveAutoTheme(): Theme {
+ const detected = this.cachedAutoDetection ?? detectTerminalTheme();
+ return detected === 'light' ? QwenLight : QwenDark;
+ }
+
+ /**
+ * Asynchronous auto-detection that includes an OSC 11 probe.
+ * Intended for startup where a short async delay (~200 ms) is acceptable.
+ * The resolved value is cached so later sync resolutions (e.g. the /theme
+ * dialog reselecting Auto) stay in sync with what the probe detected.
+ */
+ async resolveAutoThemeAsync(): Promise {
+ const detected = await detectTerminalThemeAsync();
+ this.cachedAutoDetection = detected;
+ this.activeTheme = detected === 'light' ? QwenLight : QwenDark;
+ debugLogger.info(`Auto-detected theme (async): ${this.activeTheme.name}`);
+ }
+
/**
* Gets the currently active theme.
* @returns The active theme.
diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts
index 6810456e2..3fc9ffa17 100644
--- a/packages/cli/src/ui/types.ts
+++ b/packages/cli/src/ui/types.ts
@@ -370,6 +370,24 @@ export type HistoryItemRecap = HistoryItemBase & {
text: string;
};
+// --- Doctor diagnostics types ---
+
+export type DoctorCheckStatus = 'pass' | 'warn' | 'fail';
+
+export interface DoctorCheckResult {
+ category: string;
+ name: string;
+ status: DoctorCheckStatus;
+ message: string;
+ detail?: string;
+}
+
+export type HistoryItemDoctor = HistoryItemBase & {
+ type: 'doctor';
+ checks: DoctorCheckResult[];
+ summary: { pass: number; warn: number; fail: number };
+};
+
// Using Omit seems to have some issues with typescript's
// type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that
// 'tools' in historyItem.
@@ -405,7 +423,8 @@ export type HistoryItemWithoutId =
| HistoryItemArenaSessionComplete
| HistoryItemInsightProgress
| HistoryItemBtw
- | HistoryItemRecap;
+ | HistoryItemRecap
+ | HistoryItemDoctor;
export type HistoryItem = HistoryItemWithoutId & { id: number };
diff --git a/packages/cli/src/utils/doctorChecks.test.ts b/packages/cli/src/utils/doctorChecks.test.ts
new file mode 100644
index 000000000..262fa63fc
--- /dev/null
+++ b/packages/cli/src/utils/doctorChecks.test.ts
@@ -0,0 +1,261 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { runDoctorChecks } from './doctorChecks.js';
+import { type CommandContext } from '../ui/commands/types.js';
+import { createMockCommandContext } from '../test-utils/mockCommandContext.js';
+import * as systemInfoUtils from './systemInfo.js';
+import * as authModule from '../config/auth.js';
+
+vi.mock('./systemInfo.js');
+vi.mock('../config/auth.js');
+vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
+ const actual =
+ (await importOriginal()) as typeof import('@qwen-code/qwen-code-core');
+ return {
+ ...actual,
+ canUseRipgrep: vi.fn().mockResolvedValue(true),
+ getMCPServerStatus: vi.fn().mockReturnValue('connected'),
+ MCPServerStatus: {
+ CONNECTED: 'connected',
+ CONNECTING: 'connecting',
+ DISCONNECTED: 'disconnected',
+ },
+ };
+});
+
+describe('runDoctorChecks', () => {
+ let mockContext: CommandContext;
+
+ beforeEach(() => {
+ mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getAuthType: vi.fn().mockReturnValue('openai'),
+ getGeminiClient: vi.fn().mockReturnValue({
+ isInitialized: vi.fn().mockReturnValue(true),
+ }),
+ getModel: vi.fn().mockReturnValue('gpt-4'),
+ getMcpServers: vi.fn().mockReturnValue({}),
+ getToolRegistry: vi.fn().mockReturnValue({
+ getAllTools: vi.fn().mockReturnValue([{ name: 'tool1' }]),
+ }),
+ getUseBuiltinRipgrep: vi.fn().mockReturnValue(false),
+ },
+ settings: {
+ merged: {},
+ },
+ git: {} as never,
+ },
+ } as unknown as CommandContext);
+
+ vi.mocked(systemInfoUtils.getNpmVersion).mockResolvedValue('10.0.0');
+ vi.mocked(systemInfoUtils.getGitVersion).mockResolvedValue(
+ 'git version 2.39.0',
+ );
+ vi.mocked(authModule.validateAuthMethod).mockReturnValue(null);
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should return results for all categories', async () => {
+ const results = await runDoctorChecks(mockContext);
+
+ const categories = [...new Set(results.map((r) => r.category))];
+ expect(categories).toContain('System');
+ expect(categories).toContain('Authentication');
+ expect(categories).toContain('Configuration');
+ expect(categories).toContain('Tools');
+ expect(categories).toContain('Git');
+ });
+
+ it('should pass Node.js version check for v20+', async () => {
+ const results = await runDoctorChecks(mockContext);
+ const nodeCheck = results.find((r) => r.name === 'Node.js version');
+ expect(nodeCheck).toBeDefined();
+ expect(nodeCheck!.status).toBe('pass');
+ });
+
+ it('should pass npm check when npm is available', async () => {
+ const results = await runDoctorChecks(mockContext);
+ const npmCheck = results.find((r) => r.name === 'npm version');
+ expect(npmCheck).toBeDefined();
+ expect(npmCheck!.status).toBe('pass');
+ expect(npmCheck!.message).toBe('10.0.0');
+ });
+
+ it('should warn when npm is not available', async () => {
+ vi.mocked(systemInfoUtils.getNpmVersion).mockResolvedValue('unknown');
+ const results = await runDoctorChecks(mockContext);
+ const npmCheck = results.find((r) => r.name === 'npm version');
+ expect(npmCheck!.status).toBe('warn');
+ });
+
+ it('should fail auth check when auth is not configured', async () => {
+ mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getAuthType: vi.fn().mockReturnValue(undefined),
+ getGeminiClient: vi.fn().mockReturnValue({
+ isInitialized: vi.fn().mockReturnValue(false),
+ }),
+ getModel: vi.fn().mockReturnValue('gpt-4'),
+ getMcpServers: vi.fn().mockReturnValue({}),
+ getToolRegistry: vi.fn().mockReturnValue({
+ getAllTools: vi.fn().mockReturnValue([]),
+ }),
+ getUseBuiltinRipgrep: vi.fn().mockReturnValue(false),
+ },
+ settings: { merged: {} },
+ git: {} as never,
+ },
+ } as unknown as CommandContext);
+
+ const results = await runDoctorChecks(mockContext);
+ const authCheck = results.find((r) => r.name === 'API key');
+ expect(authCheck!.status).toBe('fail');
+ });
+
+ it('should pass auth check when credentials are valid', async () => {
+ const results = await runDoctorChecks(mockContext);
+ const authCheck = results.find((r) => r.name === 'API key');
+ expect(authCheck!.status).toBe('pass');
+ });
+
+ it('should pass tool registry check when registry is loaded', async () => {
+ const results = await runDoctorChecks(mockContext);
+ const toolCheck = results.find((r) => r.name === 'Tool registry');
+ expect(toolCheck!.status).toBe('pass');
+ expect(toolCheck!.message).toContain('1');
+ });
+
+ it('should pass git check when git service exists', async () => {
+ const results = await runDoctorChecks(mockContext);
+ const gitCheck = results.find((r) => r.name === 'Git');
+ expect(gitCheck!.status).toBe('pass');
+ });
+
+ it('should warn git check when git service is missing and git binary is unavailable', async () => {
+ mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getAuthType: vi.fn().mockReturnValue('openai'),
+ getGeminiClient: vi.fn().mockReturnValue({
+ isInitialized: vi.fn().mockReturnValue(true),
+ }),
+ getModel: vi.fn().mockReturnValue('gpt-4'),
+ getMcpServers: vi.fn().mockReturnValue({}),
+ getToolRegistry: vi.fn().mockReturnValue({
+ getAllTools: vi.fn().mockReturnValue([]),
+ }),
+ getUseBuiltinRipgrep: vi.fn().mockReturnValue(false),
+ },
+ settings: { merged: {} },
+ git: undefined,
+ },
+ } as unknown as CommandContext);
+
+ vi.mocked(systemInfoUtils.getGitVersion).mockResolvedValue('unknown');
+
+ const results = await runDoctorChecks(mockContext);
+ const gitCheck = results.find((r) => r.name === 'Git');
+ expect(gitCheck!.status).toBe('warn');
+ });
+
+ it('should pass git check when git service is missing but git binary is available', async () => {
+ mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getAuthType: vi.fn().mockReturnValue('openai'),
+ getGeminiClient: vi.fn().mockReturnValue({
+ isInitialized: vi.fn().mockReturnValue(true),
+ }),
+ getModel: vi.fn().mockReturnValue('gpt-4'),
+ getMcpServers: vi.fn().mockReturnValue({}),
+ getToolRegistry: vi.fn().mockReturnValue({
+ getAllTools: vi.fn().mockReturnValue([]),
+ }),
+ getUseBuiltinRipgrep: vi.fn().mockReturnValue(false),
+ },
+ settings: { merged: {} },
+ git: undefined,
+ },
+ } as unknown as CommandContext);
+
+ vi.mocked(systemInfoUtils.getGitVersion).mockResolvedValue(
+ 'git version 2.39.0',
+ );
+
+ const results = await runDoctorChecks(mockContext);
+ const gitCheck = results.find((r) => r.name === 'Git');
+ expect(gitCheck!.status).toBe('pass');
+ expect(gitCheck!.message).toBe('git version 2.39.0');
+ });
+
+ it('should report disabled MCP servers as pass instead of fail', async () => {
+ mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getAuthType: vi.fn().mockReturnValue('openai'),
+ getGeminiClient: vi.fn().mockReturnValue({
+ isInitialized: vi.fn().mockReturnValue(true),
+ }),
+ getModel: vi.fn().mockReturnValue('gpt-4'),
+ getMcpServers: vi
+ .fn()
+ .mockReturnValue({ 'my-server': { command: 'node' } }),
+ isMcpServerDisabled: vi.fn().mockReturnValue(true),
+ getToolRegistry: vi.fn().mockReturnValue({
+ getAllTools: vi.fn().mockReturnValue([]),
+ }),
+ getUseBuiltinRipgrep: vi.fn().mockReturnValue(false),
+ },
+ settings: { merged: {} },
+ git: {} as never,
+ },
+ } as unknown as CommandContext);
+
+ const results = await runDoctorChecks(mockContext);
+ const mcpCheck = results.find((r) => r.name === 'my-server');
+ expect(mcpCheck).toBeDefined();
+ expect(mcpCheck!.status).toBe('pass');
+ expect(mcpCheck!.message).toBe('disabled');
+ });
+
+ it('should not report MCP connection status in non-interactive mode', async () => {
+ mockContext = createMockCommandContext({
+ executionMode: 'non_interactive',
+ services: {
+ config: {
+ getAuthType: vi.fn().mockReturnValue('openai'),
+ getGeminiClient: vi.fn().mockReturnValue({
+ isInitialized: vi.fn().mockReturnValue(true),
+ }),
+ getModel: vi.fn().mockReturnValue('gpt-4'),
+ getMcpServers: vi
+ .fn()
+ .mockReturnValue({ 'my-server': { command: 'node' } }),
+ isMcpServerDisabled: vi.fn().mockReturnValue(false),
+ getToolRegistry: vi.fn().mockReturnValue({
+ getAllTools: vi.fn().mockReturnValue([]),
+ }),
+ getUseBuiltinRipgrep: vi.fn().mockReturnValue(false),
+ },
+ settings: { merged: {} },
+ git: {} as never,
+ },
+ } as unknown as CommandContext);
+
+ const results = await runDoctorChecks(mockContext);
+ const mcpCheck = results.find((r) => r.name === 'my-server');
+ expect(mcpCheck).toBeDefined();
+ // In non-interactive mode, servers are never connected — must not report as fail
+ expect(mcpCheck!.status).toBe('pass');
+ });
+});
diff --git a/packages/cli/src/utils/doctorChecks.ts b/packages/cli/src/utils/doctorChecks.ts
new file mode 100644
index 000000000..6422bb5c1
--- /dev/null
+++ b/packages/cli/src/utils/doctorChecks.ts
@@ -0,0 +1,374 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import process from 'node:process';
+import os from 'node:os';
+import { getNpmVersion, getGitVersion } from './systemInfo.js';
+import { validateAuthMethod } from '../config/auth.js';
+import type { CommandContext } from '../ui/commands/types.js';
+import type { DoctorCheckResult } from '../ui/types.js';
+import {
+ canUseRipgrep,
+ getMCPServerStatus,
+ MCPServerStatus,
+} from '@qwen-code/qwen-code-core';
+import { t } from '../i18n/index.js';
+
+const MIN_NODE_MAJOR = 20;
+
+function checkNodeVersion(): DoctorCheckResult {
+ const version = process.version;
+ const major = parseInt(version.replace(/^v/, '').split('.')[0]!, 10);
+ if (isNaN(major) || major < MIN_NODE_MAJOR) {
+ return {
+ category: t('System'),
+ name: t('Node.js version'),
+ status: 'fail',
+ message: version,
+ detail: t('Node.js v{{min}}+ is required. Current: {{version}}', {
+ min: String(MIN_NODE_MAJOR),
+ version,
+ }),
+ };
+ }
+ return {
+ category: t('System'),
+ name: t('Node.js version'),
+ status: 'pass',
+ message: version,
+ };
+}
+
+async function checkNpmVersion(): Promise {
+ const version = await getNpmVersion();
+ if (version === 'unknown') {
+ return {
+ category: t('System'),
+ name: t('npm version'),
+ status: 'warn',
+ message: t('not found'),
+ detail: t('npm is not available. Some features may not work.'),
+ };
+ }
+ return {
+ category: t('System'),
+ name: t('npm version'),
+ status: 'pass',
+ message: version,
+ };
+}
+
+function checkPlatform(): DoctorCheckResult {
+ return {
+ category: t('System'),
+ name: t('Platform'),
+ status: 'pass',
+ message: `${process.platform}/${process.arch} (${os.release()})`,
+ };
+}
+
+function checkAuth(context: CommandContext): DoctorCheckResult {
+ const authType = context.services.config?.getAuthType();
+ if (!authType) {
+ return {
+ category: t('Authentication'),
+ name: t('API key'),
+ status: 'fail',
+ message: t('not configured'),
+ detail: t('Run /auth to configure authentication.'),
+ };
+ }
+
+ const error = validateAuthMethod(
+ authType,
+ context.services.config ?? undefined,
+ );
+ if (error) {
+ return {
+ category: t('Authentication'),
+ name: t('API key'),
+ status: 'fail',
+ message: t('invalid ({{authType}})', { authType }),
+ detail: error,
+ };
+ }
+
+ return {
+ category: t('Authentication'),
+ name: t('API key'),
+ status: 'pass',
+ message: t('configured ({{authType}})', { authType }),
+ };
+}
+
+async function checkApiClient(
+ context: CommandContext,
+): Promise {
+ const config = context.services.config;
+ if (!config) {
+ return {
+ category: t('Authentication'),
+ name: t('API client'),
+ status: 'fail',
+ message: t('config not loaded'),
+ };
+ }
+
+ try {
+ const client = config.getGeminiClient();
+ if (client.isInitialized()) {
+ return {
+ category: t('Authentication'),
+ name: t('API client'),
+ status: 'pass',
+ message: t('client initialized'),
+ };
+ }
+ return {
+ category: t('Authentication'),
+ name: t('API client'),
+ status: 'warn',
+ message: t('client not initialized'),
+ detail: t('The API client has not been initialized yet.'),
+ };
+ } catch (error) {
+ const errorMsg = error instanceof Error ? error.message : String(error);
+ return {
+ category: t('Authentication'),
+ name: t('API client'),
+ status: 'warn',
+ message: t('error'),
+ detail: errorMsg,
+ };
+ }
+}
+
+function checkSettings(context: CommandContext): DoctorCheckResult {
+ const settings = context.services.settings;
+ if (!settings) {
+ return {
+ category: t('Configuration'),
+ name: t('Settings'),
+ status: 'fail',
+ message: t('not loaded'),
+ detail: t(
+ 'Settings could not be loaded. Check your settings files for syntax errors.',
+ ),
+ };
+ }
+ return {
+ category: t('Configuration'),
+ name: t('Settings'),
+ status: 'pass',
+ message: t('loaded'),
+ };
+}
+
+function checkModel(context: CommandContext): DoctorCheckResult {
+ const model = context.services.config?.getModel();
+ if (!model) {
+ return {
+ category: t('Configuration'),
+ name: t('Model'),
+ status: 'fail',
+ message: t('not configured'),
+ detail: t('Run /model to select a model.'),
+ };
+ }
+ return {
+ category: t('Configuration'),
+ name: t('Model'),
+ status: 'pass',
+ message: model,
+ };
+}
+
+function checkMcpServers(context: CommandContext): DoctorCheckResult[] {
+ const config = context.services.config;
+ const servers = config?.getMcpServers();
+ if (!servers || Object.keys(servers).length === 0) {
+ return [
+ {
+ category: t('MCP Servers'),
+ name: t('MCP servers'),
+ status: 'pass',
+ message: t('none configured'),
+ },
+ ];
+ }
+
+ // In non-interactive mode MCP connections are never established, so querying
+ // getMCPServerStatus would always return DISCONNECTED and produce false failures.
+ // Report configured servers as unchecked instead.
+ if (context.executionMode !== 'interactive') {
+ return Object.keys(servers).map((name) => ({
+ category: t('MCP Servers'),
+ name,
+ status: 'pass' as const,
+ message: config?.isMcpServerDisabled(name)
+ ? t('disabled')
+ : t('configured (not checked in non-interactive mode)'),
+ }));
+ }
+
+ return Object.keys(servers).map((name) => {
+ // Skip disabled servers — report as informational pass
+ if (config?.isMcpServerDisabled(name)) {
+ return {
+ category: t('MCP Servers'),
+ name,
+ status: 'pass' as const,
+ message: t('disabled'),
+ };
+ }
+
+ const status = getMCPServerStatus(name);
+ switch (status) {
+ case MCPServerStatus.CONNECTED:
+ return {
+ category: t('MCP Servers'),
+ name,
+ status: 'pass' as const,
+ message: t('connected'),
+ };
+ case MCPServerStatus.CONNECTING:
+ return {
+ category: t('MCP Servers'),
+ name,
+ status: 'warn' as const,
+ message: t('connecting'),
+ detail: t('Server is still starting up.'),
+ };
+ case MCPServerStatus.DISCONNECTED:
+ default:
+ return {
+ category: t('MCP Servers'),
+ name,
+ status: 'fail' as const,
+ message: t('disconnected'),
+ detail: t(
+ 'Check that the server process is running and configuration is correct.',
+ ),
+ };
+ }
+ });
+}
+
+function checkToolRegistry(context: CommandContext): DoctorCheckResult {
+ const registry = context.services.config?.getToolRegistry();
+ if (!registry) {
+ return {
+ category: t('Tools'),
+ name: t('Tool registry'),
+ status: 'fail',
+ message: t('not loaded'),
+ };
+ }
+ const count = registry.getAllTools().length;
+ return {
+ category: t('Tools'),
+ name: t('Tool registry'),
+ status: 'pass',
+ message: t('{{count}} tools registered', { count: String(count) }),
+ };
+}
+
+async function checkRipgrep(
+ context: CommandContext,
+): Promise {
+ try {
+ const useBuiltin = context.services.config?.getUseBuiltinRipgrep() ?? false;
+ const result = await canUseRipgrep(useBuiltin);
+ if (result) {
+ return {
+ category: t('Tools'),
+ name: t('Ripgrep'),
+ status: 'pass',
+ message: t('available'),
+ };
+ }
+ return {
+ category: t('Tools'),
+ name: t('Ripgrep'),
+ status: 'warn',
+ message: t('not available'),
+ detail: t(
+ 'Install ripgrep for faster file search: https://github.com/BurntSushi/ripgrep',
+ ),
+ };
+ } catch {
+ return {
+ category: t('Tools'),
+ name: t('Ripgrep'),
+ status: 'warn',
+ message: t('check failed'),
+ };
+ }
+}
+
+async function checkGit(context: CommandContext): Promise {
+ if (context.services.git) {
+ return {
+ category: t('Git'),
+ name: t('Git'),
+ status: 'pass',
+ message: t('available'),
+ };
+ }
+ // services.git is undefined in non-interactive mode — probe the binary directly
+ const version = await getGitVersion();
+ if (version === 'unknown') {
+ return {
+ category: t('Git'),
+ name: t('Git'),
+ status: 'warn',
+ message: t('not available'),
+ detail: t('Git features will be limited.'),
+ };
+ }
+ return {
+ category: t('Git'),
+ name: t('Git'),
+ status: 'pass',
+ message: version,
+ };
+}
+
+/**
+ * Run all doctor diagnostic checks.
+ */
+export async function runDoctorChecks(
+ context: CommandContext,
+): Promise {
+ // Run async checks in parallel
+ const [npmResult, ripgrepResult, apiClientResult, gitResult] =
+ await Promise.all([
+ checkNpmVersion(),
+ checkRipgrep(context),
+ checkApiClient(context),
+ checkGit(context),
+ ]);
+
+ return [
+ // System
+ checkNodeVersion(),
+ npmResult,
+ checkPlatform(),
+ // Authentication
+ checkAuth(context),
+ apiClientResult,
+ // Configuration
+ checkSettings(context),
+ checkModel(context),
+ // MCP Servers
+ ...checkMcpServers(context),
+ // Tools
+ checkToolRegistry(context),
+ ripgrepResult,
+ // Git
+ gitResult,
+ ];
+}
diff --git a/packages/cli/src/utils/systemInfo.ts b/packages/cli/src/utils/systemInfo.ts
index 856da53d7..ec0f9e25c 100644
--- a/packages/cli/src/utils/systemInfo.ts
+++ b/packages/cli/src/utils/systemInfo.ts
@@ -56,6 +56,18 @@ export async function getNpmVersion(): Promise {
}
}
+/**
+ * Gets the Git version, handling cases where git might not be available.
+ * Returns 'unknown' if git command fails or is not found.
+ */
+export async function getGitVersion(): Promise {
+ try {
+ return execSync('git --version', { encoding: 'utf-8' }).trim();
+ } catch {
+ return 'unknown';
+ }
+}
+
/**
* Gets the IDE client name if IDE mode is enabled.
* Returns empty string if IDE mode is disabled or IDE client is not detected.
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 36c5da80b..bed9f8974 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -402,6 +402,14 @@ export interface ConfigParameters {
coreTools?: string[];
allowedTools?: string[];
excludeTools?: string[];
+ /**
+ * Pre-merged list of slash command names that should be hidden from the
+ * CLI surface. Matched case-insensitively on the final (post-rename)
+ * command name. Sourced from settings (`slashCommands.disabled`, UNION
+ * merged across scopes), the `--disabled-slash-commands` CLI flag, and
+ * the `QWEN_DISABLED_SLASH_COMMANDS` environment variable.
+ */
+ disabledSlashCommands?: string[];
/** Merged permission rules from all sources (settings + CLI args). */
permissions?: {
allow?: string[];
@@ -580,6 +588,7 @@ export class Config {
private readonly coreTools: string[] | undefined;
private readonly allowedTools: string[] | undefined;
private readonly excludeTools: string[] | undefined;
+ private readonly disabledSlashCommands: readonly string[];
private readonly permissionsAllow: string[];
private readonly permissionsAsk: string[];
private readonly permissionsDeny: string[];
@@ -710,6 +719,9 @@ export class Config {
this.coreTools = params.coreTools;
this.allowedTools = params.allowedTools;
this.excludeTools = params.excludeTools;
+ this.disabledSlashCommands = Object.freeze([
+ ...(params.disabledSlashCommands ?? []),
+ ]);
this.permissionsAllow = params.permissions?.allow || [];
this.permissionsAsk = params.permissions?.ask || [];
this.permissionsDeny = params.permissions?.deny || [];
@@ -1588,6 +1600,18 @@ export class Config {
return merged;
}
+ /**
+ * Returns the pre-merged list of slash command names that should be hidden
+ * from the CLI surface. Callers should treat this as a case-insensitive
+ * denylist; `CommandService.create` handles the normalization.
+ *
+ * CLI callers (loadCliConfig) populate this from settings, the
+ * `--disabled-slash-commands` flag, and `QWEN_DISABLED_SLASH_COMMANDS`.
+ */
+ getDisabledSlashCommands(): readonly string[] {
+ return this.disabledSlashCommands;
+ }
+
getToolDiscoveryCommand(): string | undefined {
return this.toolDiscoveryCommand;
}